add somenew UI editor feature for more effêcncy

This commit is contained in:
taDuc
2026-05-20 02:14:56 +07:00
parent 488eee1a25
commit 194b3ad3c2
36 changed files with 2608 additions and 597 deletions
+141 -62
View File
@@ -1,6 +1,7 @@
import maplibregl from "maplibre-gl";
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
import { buildCircleRing, destinationPoint, distanceMeters } from "@/uhm/lib/map/geo/geoMath";
import { snapToNearestGeometry } from "@/uhm/lib/map/engines/snapUtils";
export type EditingHandle = {
id: string | number;
@@ -25,12 +26,16 @@ export function createEditingEngine(options: {
const { mapRef, onUpdate } = options;
const editingRef = { current: null as EditingHandle | null };
const dragStateRef = { current: null as { idx: number } | null };
const modifierRef = { current: { ctrl: false, meta: false } };
const deleteVertexModeRef = { current: false };
let contextMenu: HTMLDivElement | null = null;
let docClickHandler: ((ev: MouseEvent) => void) | null = null;
// Hủy trạng thái chỉnh sửa hiện tại và dọn hai source edit.
const clearEditing = () => {
editingRef.current = null;
dragStateRef.current = null;
setDeleteVertexMode(false);
hideContextMenu();
const map = mapRef.current;
if (!map) return;
const empty: GeoJSON.FeatureCollection = { type: "FeatureCollection", features: [] };
@@ -135,6 +140,14 @@ export function createEditingEngine(options: {
clearEditing();
};
const setDeleteVertexMode = (enabled: boolean) => {
deleteVertexModeRef.current = enabled;
const map = mapRef.current;
if (!map?.getLayer("edit-handles-circle")) return;
map.setPaintProperty("edit-handles-circle", "circle-color", enabled ? "#ef4444" : "#f97316");
map.setPaintProperty("edit-handles-circle", "circle-stroke-color", enabled ? "#7f1d1d" : "#0f172a");
};
// Bắt đầu chỉnh sửa từ feature polygon được chọn.
const beginEditing = (feature: maplibregl.MapGeoJSONFeature) => {
if (feature.geometry.type !== "Polygon") return;
@@ -154,18 +167,19 @@ export function createEditingEngine(options: {
circleCenter: geom.circle_center,
circleRadius: geom.circle_radius,
};
setDeleteVertexMode(false);
updateEditSources();
};
// Kiểm tra trạng thái nhấn phím modifier để bật thao tác chèn đỉnh.
const isModifierPressed = (e?: maplibregl.MapLayerMouseEvent | maplibregl.MapMouseEvent) => {
const oe = e?.originalEvent as MouseEvent | undefined;
return (
modifierRef.current.ctrl ||
modifierRef.current.meta ||
!!oe?.ctrlKey ||
!!oe?.metaKey
);
const hideContextMenu = () => {
if (contextMenu) {
contextMenu.remove();
contextMenu = null;
}
if (docClickHandler) {
document.removeEventListener("click", docClickHandler);
docClickHandler = null;
}
};
// Gắn toàn bộ sự kiện phục vụ chỉnh sửa hình.
@@ -173,10 +187,16 @@ export function createEditingEngine(options: {
// Bắt đầu kéo một handle point.
const onHandleDown = (e: maplibregl.MapLayerMouseEvent) => {
if (!editingRef.current) return;
if (e.originalEvent.button === 2) return;
const feature = e.features?.[0];
const idx = feature?.properties?.idx;
if (idx === undefined) return;
const idx = Number(feature?.properties?.idx);
if (!Number.isInteger(idx)) return;
e.preventDefault();
if (deleteVertexModeRef.current) {
e.originalEvent.stopPropagation();
deleteVertex(idx);
return;
}
dragStateRef.current = { idx };
map.getCanvas().style.cursor = "grabbing";
map.dragPan.disable();
@@ -188,19 +208,21 @@ export function createEditingEngine(options: {
const editing = editingRef.current;
if (!drag || !editing) return;
const lngLat = e.originalEvent.shiftKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
const nextCoordinate: [number, number] = [lngLat.lng, lngLat.lat];
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
if (drag.idx === 0) {
// Move center
editing.circleCenter = [e.lngLat.lng, e.lngLat.lat];
editing.circleCenter = nextCoordinate;
} else if (drag.idx === 1) {
// Change radius
editing.circleRadius = distanceMeters(editing.circleCenter, [
e.lngLat.lng,
e.lngLat.lat,
]);
editing.circleRadius = distanceMeters(editing.circleCenter, nextCoordinate);
}
} else {
editing.ring[drag.idx] = [e.lngLat.lng, e.lngLat.lat];
editing.ring[drag.idx] = nextCoordinate;
}
updateEditSources();
};
@@ -212,55 +234,39 @@ export function createEditingEngine(options: {
map.dragPan.enable();
};
// Bắt phím điều khiển phiên chỉnh sửa (Enter/Escape + modifier flags).
// Bắt phím điều khiển phiên chỉnh sửa.
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Control") {
modifierRef.current.ctrl = true;
} else if (e.key === "Meta") {
modifierRef.current.meta = true;
}
if (!editingRef.current) return;
const editing = editingRef.current;
if (!editing) return;
if (e.key === "Enter") {
finishEditing();
} else if (e.key === "Delete" && !editing.isCircle) {
e.preventDefault();
setDeleteVertexMode(!deleteVertexModeRef.current);
} else if (e.key === "Escape") {
if (deleteVertexModeRef.current) {
e.preventDefault();
setDeleteVertexMode(false);
return;
}
cancelEditing();
}
};
// Hạ cờ modifier khi nhả phím.
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === "Control") {
modifierRef.current.ctrl = false;
} else if (e.key === "Meta") {
modifierRef.current.meta = false;
}
};
// Chèn thêm một đỉnh mới vào ring tại vị trí gần điểm click nhất.
const onInsertHandle = (e: maplibregl.MapLayerMouseEvent) => {
if (!editingRef.current || editingRef.current.isCircle) return;
if (!isModifierPressed(e)) return;
e.preventDefault();
// Chuột phải vào handle để mở menu xóa/thêm đỉnh.
const onHandleContextMenu = (e: maplibregl.MapLayerMouseEvent) => {
const editing = editingRef.current;
const ring = editing.ring;
const click = [e.lngLat.lng, e.lngLat.lat] as [number, number];
let nearestIdx = 0;
let bestDist = Number.POSITIVE_INFINITY;
ring.forEach((pt, idx) => {
const dx = pt[0] - click[0];
const dy = pt[1] - click[1];
const d = dx * dx + dy * dy; // Dùng khoảng cách Euclid bình phương để so sánh nhanh, không cần sqrt.
if (d < bestDist) {
bestDist = d;
nearestIdx = idx;
}
});
const insertIdx = nearestIdx + 1;
ring.splice(insertIdx, 0, click);
dragStateRef.current = { idx: insertIdx };
map.getCanvas().style.cursor = "grabbing";
map.dragPan.disable();
updateEditSources();
if (!editing || editing.isCircle) return;
e.preventDefault();
e.originalEvent.stopPropagation();
const feature = e.features?.[0];
const idx = Number(feature?.properties?.idx);
if (!Number.isInteger(idx)) return;
showHandleContextMenu(
e.originalEvent.clientX,
e.originalEvent.clientY,
idx
);
};
// Ngắt kéo nếu con trỏ rời canvas.
@@ -269,24 +275,97 @@ export function createEditingEngine(options: {
};
map.on("mousedown", "edit-handles-circle", onHandleDown);
map.on("mousedown", "edit-shape-line", onInsertHandle);
map.on("contextmenu", "edit-handles-circle", onHandleContextMenu);
map.on("mousemove", onHandleMove);
map.on("mouseup", stopDragging);
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
map.getCanvas().addEventListener("mouseleave", onCanvasLeave);
map.on("remove", () => {
map.off("mousedown", "edit-handles-circle", onHandleDown);
map.off("mousedown", "edit-shape-line", onInsertHandle);
map.off("contextmenu", "edit-handles-circle", onHandleContextMenu);
map.off("mousemove", onHandleMove);
map.off("mouseup", stopDragging);
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
map.getCanvas().removeEventListener("mouseleave", onCanvasLeave);
hideContextMenu();
});
};
const showHandleContextMenu = (x: number, y: number, idx: number) => {
hideContextMenu();
const menu = document.createElement("div");
menu.style.position = "fixed";
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
menu.style.background = "#0f172a";
menu.style.color = "white";
menu.style.border = "1px solid #1f2937";
menu.style.borderRadius = "6px";
menu.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)";
menu.style.zIndex = "9999";
menu.style.minWidth = "120px";
menu.style.fontSize = "14px";
menu.style.padding = "4px 0";
const createItem = (label: string, onClick: () => void, disabled = false) => {
const item = document.createElement("div");
item.textContent = label;
item.style.padding = "8px 12px";
item.style.cursor = disabled ? "not-allowed" : "pointer";
item.style.opacity = disabled ? "0.45" : "1";
item.onmouseenter = () => {
if (!disabled) item.style.background = "#1f2937";
};
item.onmouseleave = () => (item.style.background = "transparent");
item.onclick = () => {
if (disabled) return;
onClick();
hideContextMenu();
};
return item;
};
const editing = editingRef.current;
const canDelete = Boolean(editing && !editing.isCircle && editing.ring.length > 3);
menu.appendChild(createItem("Xóa đỉnh", () => deleteVertex(idx), !canDelete));
menu.appendChild(createItem("Thêm đỉnh", () => insertVertexAfter(idx)));
document.body.appendChild(menu);
contextMenu = menu;
const onDocClick = (ev: MouseEvent) => {
if (!menu.contains(ev.target as Node)) {
hideContextMenu();
}
};
docClickHandler = onDocClick;
setTimeout(() => document.addEventListener("click", onDocClick), 0);
};
const deleteVertex = (idx: number) => {
const editing = editingRef.current;
if (!editing || editing.isCircle || editing.ring.length <= 3) return;
if (idx < 0 || idx >= editing.ring.length) return;
editing.ring.splice(idx, 1);
updateEditSources();
};
const insertVertexAfter = (idx: number) => {
const editing = editingRef.current;
if (!editing || editing.isCircle || editing.ring.length < 2) return;
if (idx < 0 || idx >= editing.ring.length) return;
const current = editing.ring[idx];
const next = editing.ring[(idx + 1) % editing.ring.length];
const midpoint: [number, number] = [
(current[0] + next[0]) / 2,
(current[1] + next[1]) / 2,
];
editing.ring.splice(idx + 1, 0, midpoint);
updateEditSources();
};
return {
beginEditing,
clearEditing,
+11 -2
View File
@@ -1,6 +1,7 @@
import maplibregl from "maplibre-gl";
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
import { snapToNearestGeometry } from "@/uhm/lib/map/engines/snapUtils";
const EMPTY_PREVIEW: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
@@ -74,7 +75,11 @@ export function initLine(
const onClick = (e: maplibregl.MapLayerMouseEvent) => {
if (getMode() !== "add-line") return;
coords.push([e.lngLat.lng, e.lngLat.lat]);
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
coords.push([lngLat.lng, lngLat.lat]);
updatePreview(coords);
};
@@ -94,7 +99,11 @@ export function initLine(
canvas.style.cursor = "crosshair";
if (coords.length === 0) return;
updatePreview([...coords, [e.lngLat.lng, e.lngLat.lat]]);
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
updatePreview([...coords, [lngLat.lng, lngLat.lat]]);
};
// Xử lý phím nóng Enter/Escape/Backspace cho chế độ vẽ line.
+10 -2
View File
@@ -1,6 +1,7 @@
import maplibregl from "maplibre-gl";
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
import { snapToNearestGeometry } from "@/uhm/lib/map/engines/snapUtils";
const EMPTY_PREVIEW: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
@@ -75,7 +76,11 @@ export function initPath(
const onClick = (e: maplibregl.MapLayerMouseEvent) => {
if (getMode() !== "add-path") return;
coords.push([e.lngLat.lng, e.lngLat.lat]);
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
coords.push([lngLat.lng, lngLat.lat]);
updatePreview(coords);
};
@@ -96,7 +101,10 @@ export function initPath(
canvas.style.cursor = "crosshair";
if (coords.length === 0) return;
updatePreview([...coords, [e.lngLat.lng, e.lngLat.lat]]);
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
updatePreview([...coords, [lngLat.lng, lngLat.lat]]);
};
// Xử lý phím nóng Enter/Escape/Backspace cho chế độ vẽ path.
+6 -1
View File
@@ -1,6 +1,7 @@
import maplibregl from "maplibre-gl";
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
import { snapToNearestGeometry } from "@/uhm/lib/map/engines/snapUtils";
// Khởi tạo engine thêm point bằng click đơn.
export function initPoint(
@@ -12,9 +13,13 @@ export function initPoint(
function onClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "add-point") return;
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
? snapToNearestGeometry(map, e.lngLat, e.point)
: e.lngLat;
const geometry: Geometry = {
type: "Point",
coordinates: [e.lngLat.lng, e.lngLat.lat],
coordinates: [lngLat.lng, lngLat.lat],
};
onComplete?.(geometry);
+28 -3
View File
@@ -7,8 +7,11 @@ export function initSelect(
getMode: ModeGetter,
onDelete?: (id: string | number) => void,
onEdit?: (feature: maplibregl.MapGeoJSONFeature) => void,
onDuplicate?: (id: string | number) => void,
onHide?: (id: string | number) => void,
onSelectIds?: (ids: (string | number)[]) => void,
onReplayEdit?: (id: string | number) => void
onReplayEdit?: (id: string | number) => void,
isEditSessionActive?: () => boolean
) {
const FEATURE_STATE_SOURCES = [
@@ -17,7 +20,7 @@ export function initSelect(
"path-arrow-shapes",
] as const;
const selectedIds = new Set<number | string>();
const hasContextActions = Boolean(onDelete || onEdit || onReplayEdit);
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit);
let contextMenu: HTMLDivElement | null = null;
let docClickHandler: ((ev: MouseEvent) => void) | null = null;
@@ -56,6 +59,7 @@ export function initSelect(
// Chọn feature theo click trái, hỗ trợ additive bằng Alt.
function onClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select" && getMode() !== "replay") return;
if (isEditSessionActive?.()) return;
const selectableLayers = getSelectableLayers();
if (!selectableLayers.length) return;
@@ -81,6 +85,7 @@ export function initSelect(
e.preventDefault(); // block browser menu
if (getMode() === "replay") return;
if (isEditSessionActive?.()) return;
const features = map.queryRenderedFeatures(e.point, {
layers: selectableLayers,
@@ -125,7 +130,11 @@ export function initSelect(
const style = map.getStyle();
if (!style || !style.layers) return [];
return style.layers
.filter((layer) => "source" in layer && FEATURE_STATE_SOURCES.includes(layer.source as any))
.filter((layer) =>
"source" in layer &&
typeof layer.source === "string" &&
FEATURE_STATE_SOURCES.includes(layer.source as (typeof FEATURE_STATE_SOURCES)[number])
)
.map((layer) => layer.id);
}
@@ -236,6 +245,22 @@ export function initSelect(
hasMenuItems = true;
}
if (selectedCount === 1 && onDuplicate) {
const featureId = clickedFeature.id ?? clickedFeature.properties?.id;
if (featureId !== undefined && featureId !== null) {
menu.appendChild(createItem("Duplicate", () => onDuplicate(featureId)));
hasMenuItems = true;
}
}
if (selectedCount === 1 && onHide) {
const featureId = clickedFeature.id ?? clickedFeature.properties?.id;
if (featureId !== undefined && featureId !== null) {
menu.appendChild(createItem("Hide", () => onHide(featureId)));
hasMenuItems = true;
}
}
if (onReplayEdit) {
const featureId = clickedFeature.id ?? clickedFeature.properties?.id;
if (featureId) {
+99 -17
View File
@@ -1,6 +1,16 @@
import maplibregl from "maplibre-gl";
import { PATH_ARROW_SOURCE_ID } from "@/uhm/lib/map/constants";
const SNAP_THRESHOLD_PX = 15;
// SHIFT/ALT snap should be forgiving while drawing quickly.
// Vertices get a larger radius and always win over edges when both are available.
const VERTEX_SNAP_THRESHOLD_PX = 34;
const EDGE_SNAP_THRESHOLD_PX = 24;
const QUERY_THRESHOLD_PX = Math.max(VERTEX_SNAP_THRESHOLD_PX, EDGE_SNAP_THRESHOLD_PX);
type Coordinate = [number, number];
type GeometryWithCoordinates = Exclude<GeoJSON.Geometry, GeoJSON.GeometryCollection> & {
coordinates: unknown;
};
export function snapToNearestGeometry(
map: maplibregl.Map,
@@ -8,14 +18,21 @@ export function snapToNearestGeometry(
pointPx: maplibregl.Point
): maplibregl.LngLat {
const bbox: [maplibregl.PointLike, maplibregl.PointLike] = [
[pointPx.x - SNAP_THRESHOLD_PX, pointPx.y - SNAP_THRESHOLD_PX],
[pointPx.x + SNAP_THRESHOLD_PX, pointPx.y + SNAP_THRESHOLD_PX],
[pointPx.x - QUERY_THRESHOLD_PX, pointPx.y - QUERY_THRESHOLD_PX],
[pointPx.x + QUERY_THRESHOLD_PX, pointPx.y + QUERY_THRESHOLD_PX],
];
const features = map.queryRenderedFeatures(bbox);
const snapLayerIds = getSnapLayerIds(map);
if (!snapLayerIds.length) return lngLat;
let nearestDist = Infinity;
let nearestLngLat: maplibregl.LngLat | null = null;
const features = map.queryRenderedFeatures(bbox, {
layers: snapLayerIds,
});
let nearestVertexDist = Infinity;
let nearestVertexLngLat: maplibregl.LngLat | null = null;
let nearestEdgeDist = Infinity;
let nearestEdgeLngLat: maplibregl.LngLat | null = null;
const getDistSq = (p1: maplibregl.Point, p2: maplibregl.Point) => {
return (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2;
@@ -34,24 +51,49 @@ export function snapToNearestGeometry(
return new maplibregl.Point(a.x + atob.x * t, a.y + atob.y * t);
};
const processVertex = (coordinate: Coordinate) => {
const vertexLngLat = new maplibregl.LngLat(coordinate[0], coordinate[1]);
const vertexPx = map.project(vertexLngLat);
const distSq = getDistSq(pointPx, vertexPx);
if (
distSq < nearestVertexDist &&
distSq <= VERTEX_SNAP_THRESHOLD_PX ** 2
) {
nearestVertexDist = distSq;
nearestVertexLngLat = vertexLngLat;
}
};
const processLineString = (line: number[][]) => {
if (!line || line.length < 2) return;
for (let i = 0; i < line.length - 1; i++) {
const p1LngLat = new maplibregl.LngLat(line[i][0], line[i][1]);
const p2LngLat = new maplibregl.LngLat(line[i + 1][0], line[i + 1][1]);
const start = toCoordinate(line[i]);
const end = toCoordinate(line[i + 1]);
if (!start || !end) continue;
processVertex(start);
if (i === line.length - 2) processVertex(end);
const p1LngLat = new maplibregl.LngLat(start[0], start[1]);
const p2LngLat = new maplibregl.LngLat(end[0], end[1]);
const p1 = map.project(p1LngLat);
const p2 = map.project(p2LngLat);
const closestPx = getClosestPointOnSegment(pointPx, p1, p2);
const distSq = getDistSq(pointPx, closestPx);
if (distSq < nearestDist && distSq <= SNAP_THRESHOLD_PX ** 2) {
nearestDist = distSq;
nearestLngLat = map.unproject(closestPx);
if (distSq < nearestEdgeDist && distSq <= EDGE_SNAP_THRESHOLD_PX ** 2) {
nearestEdgeDist = distSq;
nearestEdgeLngLat = map.unproject(closestPx);
}
}
};
const processPoint = (coordinate: unknown) => {
const point = toCoordinate(coordinate);
if (point) processVertex(point);
};
for (const feature of features) {
if (!feature.geometry) continue;
@@ -62,21 +104,61 @@ export function snapToNearestGeometry(
const type = feature.geometry.type;
if (type === "GeometryCollection") continue;
const coords = (feature.geometry as any).coordinates;
const coords = (feature.geometry as GeometryWithCoordinates).coordinates;
// Xử lý cả Polygon và LineString vì viền bản đồ (border) đôi khi được render dưới dạng LineString
if (type === "Polygon") {
for (const ring of coords) processLineString(ring);
for (const ring of asCoordinateMatrix(coords)) processLineString(ring);
} else if (type === "MultiPolygon") {
for (const poly of coords) {
for (const poly of asCoordinateTensor(coords)) {
for (const ring of poly) processLineString(ring);
}
} else if (type === "LineString") {
processLineString(coords);
processLineString(asCoordinateArray(coords));
} else if (type === "MultiLineString") {
for (const line of coords) processLineString(line);
for (const line of asCoordinateMatrix(coords)) processLineString(line);
} else if (type === "Point") {
processPoint(coords);
} else if (type === "MultiPoint") {
for (const point of asCoordinateArray(coords)) processPoint(point);
}
}
return nearestLngLat || lngLat;
return nearestVertexLngLat || nearestEdgeLngLat || lngLat;
}
function getSnapLayerIds(map: maplibregl.Map): string[] {
const systemGeometrySources = new Set(["countries", "places", PATH_ARROW_SOURCE_ID]);
const style = map.getStyle();
if (!style?.layers?.length) return [];
return style.layers
.filter((layer) => {
if (!("source" in layer)) return false;
if (!systemGeometrySources.has(String(layer.source))) return false;
if (layer.id.includes("preview") || layer.id.includes("edit-")) return false;
return true;
})
.map((layer) => layer.id)
.filter((layerId) => Boolean(map.getLayer(layerId)));
}
function toCoordinate(value: unknown): Coordinate | null {
if (!Array.isArray(value) || value.length < 2) return null;
const lng = Number(value[0]);
const lat = Number(value[1]);
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
return [lng, lat];
}
function asCoordinateArray(value: unknown): number[][] {
return Array.isArray(value) ? value as number[][] : [];
}
function asCoordinateMatrix(value: unknown): number[][][] {
return Array.isArray(value) ? value as number[][][] : [];
}
function asCoordinateTensor(value: unknown): number[][][][] {
return Array.isArray(value) ? value as number[][][][] : [];
}
+2 -2
View File
@@ -8,10 +8,11 @@
{ "type_key": "trade_route", "geo_type_code": 7 },
{ "type_key": "shipping_route", "geo_type_code": 8 },
{ "type_key": "country", "geo_type_code": 9 },
{ "type_key": "country", "geo_type_code": 9, "fixed": true },
{ "type_key": "state", "geo_type_code": 10 },
{ "type_key": "empire", "geo_type_code": 11 },
{ "type_key": "kingdom", "geo_type_code": 12 },
{ "type_key": "faction", "geo_type_code": 28 },
{ "type_key": "war", "geo_type_code": 13 },
{ "type_key": "battle", "geo_type_code": 14 },
@@ -30,4 +31,3 @@
{ "type_key": "port", "geo_type_code": 26 },
{ "type_key": "bridge", "geo_type_code": 27 }
]
@@ -75,6 +75,7 @@ const RAW_GEOMETRY_TYPE_OPTIONS: Array<{
{ value: "state", label: "State", groupId: "polygon", geometryPreset: "polygon" },
{ value: "empire", label: "Empire", groupId: "polygon", geometryPreset: "polygon" },
{ value: "kingdom", label: "Kingdom", groupId: "polygon", geometryPreset: "polygon" },
{ value: "faction", label: "Faction", groupId: "polygon", geometryPreset: "polygon" },
{ value: "war", label: "War", groupId: "circle", geometryPreset: "circle-area" },
{ value: "battle", label: "Battle", groupId: "circle", geometryPreset: "circle-area" },
+2
View File
@@ -14,6 +14,7 @@ import { getCountryLayers } from "./geotypes/country";
import { getStateLayers } from "./geotypes/state";
import { getEmpireLayers } from "./geotypes/empire";
import { getKingdomLayers } from "./geotypes/kingdom";
import { getFactionLayers } from "./geotypes/faction";
import { getWarLayers } from "./geotypes/war";
import { getBattleLayers } from "./geotypes/battle";
import { getCivilizationLayers } from "./geotypes/civilization";
@@ -40,6 +41,7 @@ export function getAllGeotypeLayers(sourceId: string, pathArrowSourceId?: string
...getStateLayers(sourceId, pathArrowSourceId, pointSourceId),
...getEmpireLayers(sourceId, pathArrowSourceId, pointSourceId),
...getKingdomLayers(sourceId, pathArrowSourceId, pointSourceId),
...getFactionLayers(sourceId, pathArrowSourceId, pointSourceId),
...getWarLayers(sourceId, pathArrowSourceId, pointSourceId),
...getBattleLayers(sourceId, pathArrowSourceId, pointSourceId),
...getCivilizationLayers(sourceId, pathArrowSourceId, pointSourceId),
@@ -0,0 +1,15 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getFactionLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId;
void pointSourceId;
return buildPolygonGeotypeLayers(sourceId, {
typeId: "faction",
fillColor: "#f97316",
strokeColor: "#9a3412",
fillOpacity: 0.3,
strokeWidth: { z1: 1.6, z4: 2.3, z6: 3.1 },
dasharray: [2, 1.5],
});
}