feat: add region and location geometry types and extend editing engine to support line and point modifications

This commit is contained in:
taDuc
2026-06-01 16:01:37 +07:00
parent d270d9435b
commit 1a77d471ad
14 changed files with 623 additions and 114 deletions
-1
View File
@@ -41,7 +41,6 @@ export function ModeHint({ mode }: { mode: EditorMode }) {
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
<div style={{ marginBottom: 4 }}>Click vào hình trên map đ Chọn (Select).</div>
<ul style={{ paddingLeft: 16, margin: 0, opacity: 0.85 }}>
<li>(Khi đã chọn) Nhấp biểu tượng <b>Cây Bút</b> đ sửa đnh.</li>
<li>Trong chế đ Sửa đnh:
<ul style={{ paddingLeft: 16, margin: "2px 0 0 0" }}>
<li><b>Enter</b>: Lưu hình đã sửa</li>
+145 -46
View File
@@ -496,8 +496,10 @@ export function buildPathArrowFeatureCollection(fc: FeatureCollection): FeatureC
if (!arrowGeometries) {
arrowGeometries = [];
const coordinateGroups = getLineCoordinateGroups(feature.geometry);
const featureType = getFeatureSemanticType(feature);
const isRetreat = featureType === "retreat_route";
for (const coordinates of coordinateGroups) {
const geometry = buildPathArrowGeometry(coordinates);
const geometry = buildPathArrowGeometry(coordinates, isRetreat);
if (geometry) arrowGeometries.push(geometry);
}
pathArrowGeometriesCache.set(feature.geometry, arrowGeometries);
@@ -528,7 +530,7 @@ export function getFeatureSemanticType(feature: Feature): string | null {
return normalizeGeoTypeKey(value);
}
export function buildPathArrowGeometry(coords: [number, number][]): Geometry | null {
export function buildPathArrowGeometry(coords: [number, number][], isRetreatRoute = false): Geometry | null {
const sourceCoords = removeDuplicatePathCoords(coords);
if (sourceCoords.length < 2) return null;
@@ -553,54 +555,141 @@ export function buildPathArrowGeometry(coords: [number, number][]): Geometry | n
const shoulderWidth = clampNumber(totalLength * 0.1, 10, 100000);
const headWidth = shoulderWidth * 2.0;
const leftBody: ProjectedPoint[] = [];
const rightBody: ProjectedPoint[] = [];
for (let i = 0; i < bodyPoints.length; i += 1) {
const point = bodyPoints[i];
const normal = normalAt(bodyPoints, i);
const progress = bodyEndDistance > 0
? Math.pow(clampNumber(point.distance / bodyEndDistance, 0, 1), 0.9)
: 0;
const width = tailWidth + (shoulderWidth - tailWidth) * progress;
const half = width / 2;
leftBody.push({
x: point.x + normal.x * half,
y: point.y + normal.y * half,
});
rightBody.push({
x: point.x - normal.x * half,
y: point.y - normal.y * half,
});
}
const base = bodyPoints[bodyPoints.length - 1];
const tip = pointAtDistance(measured, totalLength);
const headNormal = normalFromSegment(base, tip) || normalAt(bodyPoints, bodyPoints.length - 1);
const headHalf = headWidth / 2;
const headBaseLeft = {
x: base.x + headNormal.x * headHalf,
y: base.y + headNormal.y * headHalf,
};
const headBaseRight = {
x: base.x - headNormal.x * headHalf,
y: base.y - headNormal.y * headHalf,
};
const ring = [
...leftBody,
headBaseLeft,
{ x: tip.x, y: tip.y },
headBaseRight,
...rightBody.reverse(),
leftBody[0],
].map((point) => unprojectLngLat(point, origin, cosOriginLat));
if (isRetreatRoute) {
// Segmented Arrow (MultiPolygon)
const rings: [number, number][][] = [];
if (ring.length < 4) return null;
return {
type: "Polygon",
coordinates: [ring],
};
// 1. Generate body segments
const segmentLength = totalLength * 0.10; // Dash length
const gapLength = totalLength * 0.04; // Gap length
let currentD = 0;
while (currentD < bodyEndDistance) {
const startD = currentD;
const endD = Math.min(startD + segmentLength, bodyEndDistance - gapLength);
if (endD - startD > totalLength * 0.01) {
const segmentPoints: MeasuredPoint[] = [];
segmentPoints.push(pointAtDistance(measured, startD));
for (const p of bodyPoints) {
if (p.distance > startD && p.distance < endD) {
segmentPoints.push(p);
}
}
segmentPoints.push(pointAtDistance(measured, endD));
const leftBody: ProjectedPoint[] = [];
const rightBody: ProjectedPoint[] = [];
for (let i = 0; i < segmentPoints.length; i += 1) {
const point = segmentPoints[i];
const normal = normalAt(segmentPoints, i);
const progress = bodyEndDistance > 0
? Math.pow(clampNumber(point.distance / bodyEndDistance, 0, 1), 0.9)
: 0;
const width = tailWidth + (shoulderWidth - tailWidth) * progress;
const half = width / 2;
leftBody.push({
x: point.x + normal.x * half,
y: point.y + normal.y * half,
});
rightBody.push({
x: point.x - normal.x * half,
y: point.y - normal.y * half,
});
}
const ring = [
...leftBody,
...rightBody.reverse(),
leftBody[0],
].map((point) => unprojectLngLat(point, origin, cosOriginLat));
if (ring.length >= 4) {
rings.push(ring);
}
}
currentD += segmentLength + gapLength;
}
// 2. Generate head segment (standalone arrowhead chevron/triangle)
const headBaseLeft = {
x: base.x + headNormal.x * headHalf,
y: base.y + headNormal.y * headHalf,
};
const headBaseRight = {
x: base.x - headNormal.x * headHalf,
y: base.y - headNormal.y * headHalf,
};
const headRing = [
{ x: base.x, y: base.y },
headBaseLeft,
{ x: tip.x, y: tip.y },
headBaseRight,
{ x: base.x, y: base.y },
].map((point) => unprojectLngLat(point, origin, cosOriginLat));
rings.push(headRing);
return {
type: "MultiPolygon",
coordinates: rings.map(r => [r]),
};
} else {
// Continuous Arrow (Polygon)
const leftBody: ProjectedPoint[] = [];
const rightBody: ProjectedPoint[] = [];
for (let i = 0; i < bodyPoints.length; i += 1) {
const point = bodyPoints[i];
const normal = normalAt(bodyPoints, i);
const progress = bodyEndDistance > 0
? Math.pow(clampNumber(point.distance / bodyEndDistance, 0, 1), 0.9)
: 0;
const width = tailWidth + (shoulderWidth - tailWidth) * progress;
const half = width / 2;
leftBody.push({
x: point.x + normal.x * half,
y: point.y + normal.y * half,
});
rightBody.push({
x: point.x - normal.x * half,
y: point.y - normal.y * half,
});
}
const headBaseLeft = {
x: base.x + headNormal.x * headHalf,
y: base.y + headNormal.y * headHalf,
};
const headBaseRight = {
x: base.x - headNormal.x * headHalf,
y: base.y - headNormal.y * headHalf,
};
const ring = [
...leftBody,
headBaseLeft,
{ x: tip.x, y: tip.y },
headBaseRight,
...rightBody.reverse(),
leftBody[0],
].map((point) => unprojectLngLat(point, origin, cosOriginLat));
if (ring.length < 4) return null;
return {
type: "Polygon",
coordinates: [ring],
};
}
}
export type ProjectedPoint = {
@@ -833,11 +922,21 @@ function createFeatureLabelResolver(
return (feature) => {
const featureId = String(feature.properties.id);
const directEntityIds = getFeatureEntityIds(feature);
let label: string | null = null;
if (directEntityIds.length > 0) {
return directLabelsByFeatureId.get(featureId)?.label || null;
label = directLabelsByFeatureId.get(featureId)?.label || null;
} else {
label = inheritedLabelsByChildId.get(featureId)?.label || null;
}
return inheritedLabelsByChildId.get(featureId)?.label || null;
if (!label) {
const geotype = feature.properties?.type || feature.properties?.entity_type_id;
if (geotype === "region") {
return "__Missing__";
}
}
return label;
};
}
+3 -2
View File
@@ -333,8 +333,9 @@ export function useMapInteraction({
drawingEngine.cleanup
);
if (allowGeometryEditing) {
editingEngineRef.current?.bindEditEvents(map);
const editCleanup = editingEngineRef.current?.bindEditEvents(map);
if (editCleanup) {
mapCleanupFnsRef.current.push(editCleanup);
}
};
+26
View File
@@ -187,6 +187,19 @@ export function setupMapLayers(
data: { type: "FeatureCollection", features: [] },
});
// Glowing halo under the edit shape line
map.addLayer({
id: "edit-shape-glow",
type: "line",
source: "edit-shape",
paint: {
"line-color": "#38bdf8",
"line-width": 8,
"line-opacity": 0.35,
"line-blur": 1.5,
},
});
map.addLayer({
id: "edit-shape-line",
type: "line",
@@ -197,6 +210,19 @@ export function setupMapLayers(
},
});
// Glowing halo under the edit handles
map.addLayer({
id: "edit-handles-glow",
type: "circle",
source: "edit-handles",
paint: {
"circle-color": "#f97316",
"circle-radius": 22,
"circle-opacity": 0.35,
"circle-blur": 0.85,
},
});
map.addLayer({
id: "edit-handles-circle",
type: "circle",
+1 -1
View File
@@ -200,7 +200,7 @@ export function useMapSync({
applyRenderDraftToMap(renderDraft, labelContextDraft, selectedFeatureIds);
const editingId = editingEngineRef.current?.editingRef?.current?.id;
if (allowGeometryEditing && editingId !== undefined && editingId !== null) {
const stillExists = renderDraft.features.some((f) => f.properties.id === editingId);
const stillExists = renderDraft.features.some((f) => String(f.properties.id) === String(editingId));
if (!stillExists) {
editingEngineRef.current?.clearEditing();
}
+179 -60
View File
@@ -10,15 +10,16 @@ export type EditingHandle = {
isCircle?: boolean;
circleCenter?: [number, number];
circleRadius?: number;
geometryType?: "Point" | "LineString" | "Polygon";
};
export type EditingAPI = {
beginEditing: (feature: maplibregl.MapGeoJSONFeature) => void;
clearEditing: () => void;
bindEditEvents: (map: maplibregl.Map) => void;
bindEditEvents: (map: maplibregl.Map) => (() => void);
};
// Tạo engine chỉnh sửa polygon đã có (kéo đỉnh, thêm đỉnh, commit/cancel).
// Tạo engine chỉnh sửa polygon, line, point đã có (kéo đỉnh, thêm đỉnh, commit/cancel).
export function createEditingEngine(options: {
mapRef: React.MutableRefObject<maplibregl.Map | null>;
onUpdate: (id: string | number, geometry: Geometry) => void;
@@ -43,54 +44,79 @@ export function createEditingEngine(options: {
(map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(empty);
};
// Đồng bộ polygon tạm và các handle point lên map source.
// Đồng bộ polygon/line/point tạm và các handle point lên map source.
const updateEditSources = () => {
const editing = editingRef.current;
const map = mapRef.current;
console.log("updateEditSources: editing:", editing, "map loaded:", map?.isStyleLoaded());
if (!editing || !map || !map.isStyleLoaded()) return;
let shape: GeoJSON.FeatureCollection<GeoJSON.Polygon>;
let shape: GeoJSON.FeatureCollection<GeoJSON.Polygon | GeoJSON.LineString | GeoJSON.Point>;
let handles: GeoJSON.FeatureCollection<GeoJSON.Point>;
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
const ring = buildCircleRing(editing.circleCenter, editing.circleRadius);
const closedRing = [...ring, ring[0]];
shape = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Polygon", coordinates: [closedRing] },
properties: {},
},
],
};
const geomType = editing.geometryType || "Polygon";
// Circle handles: 0 = center, 1 = radius control
const radiusHandlePoint = destinationPoint(editing.circleCenter, editing.circleRadius, 90);
handles = {
type: "FeatureCollection",
features: [
{
if (geomType === "Polygon") {
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
const ring = buildCircleRing(editing.circleCenter, editing.circleRadius);
const closedRing = [...ring, ring[0]];
shape = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Polygon", coordinates: [closedRing] },
properties: {},
},
],
};
// Circle handles: 0 = center, 1 = radius control
const radiusHandlePoint = destinationPoint(editing.circleCenter, editing.circleRadius, 90);
handles = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: editing.circleCenter },
properties: { idx: 0, type: "center" },
},
{
type: "Feature",
geometry: { type: "Point", coordinates: radiusHandlePoint },
properties: { idx: 1, type: "radius" },
},
],
};
} else {
const closedRing = [...editing.ring, editing.ring[0]];
shape = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Polygon", coordinates: [closedRing] },
properties: {},
},
],
};
handles = {
type: "FeatureCollection",
features: editing.ring.map((c, idx) => ({
type: "Feature",
geometry: { type: "Point", coordinates: editing.circleCenter },
properties: { idx: 0, type: "center" },
},
{
type: "Feature",
geometry: { type: "Point", coordinates: radiusHandlePoint },
properties: { idx: 1, type: "radius" },
},
],
};
} else {
const closedRing = [...editing.ring, editing.ring[0]];
geometry: { type: "Point", coordinates: c },
properties: { idx },
})),
};
}
} else if (geomType === "LineString") {
shape = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Polygon", coordinates: [closedRing] },
geometry: { type: "LineString", coordinates: editing.ring },
properties: {},
},
],
@@ -104,6 +130,23 @@ export function createEditingEngine(options: {
properties: { idx },
})),
};
} else {
// Point
shape = {
type: "FeatureCollection",
features: [],
};
handles = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: editing.ring[0] },
properties: { idx: 0 },
},
],
};
}
(map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(shape);
@@ -116,18 +159,33 @@ export function createEditingEngine(options: {
if (!editing) return;
let geometry: Geometry;
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
const ring = buildCircleRing(editing.circleCenter, editing.circleRadius);
const geomType = editing.geometryType || "Polygon";
if (geomType === "Polygon") {
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
const ring = buildCircleRing(editing.circleCenter, editing.circleRadius);
geometry = {
type: "Polygon",
coordinates: [[...ring, ring[0]]],
circle_center: editing.circleCenter,
circle_radius: editing.circleRadius,
};
} else {
geometry = {
type: "Polygon",
coordinates: [[...editing.ring, editing.ring[0]]],
};
}
} else if (geomType === "LineString") {
geometry = {
type: "Polygon",
coordinates: [[...ring, ring[0]]],
circle_center: editing.circleCenter,
circle_radius: editing.circleRadius,
type: "LineString",
coordinates: editing.ring,
};
} else {
// Point
geometry = {
type: "Polygon",
coordinates: [[...editing.ring, editing.ring[0]]],
type: "Point",
coordinates: editing.ring[0],
};
}
@@ -146,19 +204,56 @@ export function createEditingEngine(options: {
if (!map || !map.isStyleLoaded() || !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");
if (map.getLayer("edit-handles-glow")) {
map.setPaintProperty("edit-handles-glow", "circle-color", enabled ? "#ef4444" : "#f97316");
}
};
// Bắt đầu chỉnh sửa từ feature polygon được chọn.
// Bắt đầu chỉnh sửa từ feature polygon/line/point được chọn.
const beginEditing = (feature: maplibregl.MapGeoJSONFeature) => {
if (feature.geometry.type !== "Polygon") return;
console.log("beginEditing called with feature:", feature);
if (!feature || !feature.geometry) {
console.warn("beginEditing: feature or feature.geometry is missing");
return;
}
const geom = feature.geometry as Geometry;
const coords = (geom.coordinates?.[0] ?? []) as [number, number][];
if (coords.length < 4) return;
const type = geom.type;
console.log("beginEditing: geometry type is", type);
if (type !== "Polygon" && type !== "LineString" && type !== "Point") {
console.warn("beginEditing: unsupported geometry type:", type);
return;
}
const isCircle = !!geom.circle_center;
// remove duplicated closing point
const ring = coords.slice(0, -1).map((c) => [c[0], c[1]] as [number, number]);
let ring: [number, number][] = [];
if (type === "Polygon") {
const coords = (geom.coordinates?.[0] ?? []) as [number, number][];
console.log("beginEditing Polygon coords:", coords);
if (coords.length < 4) {
console.warn("beginEditing: Polygon coords length is less than 4");
return;
}
// remove duplicated closing point
ring = coords.slice(0, -1).map((c) => [c[0], c[1]] as [number, number]);
} else if (type === "LineString") {
const coords = (geom.coordinates ?? []) as [number, number][];
console.log("beginEditing LineString coords:", coords);
if (coords.length < 2) {
console.warn("beginEditing: LineString coords length is less than 2");
return;
}
ring = coords.map((c) => [c[0], c[1]] as [number, number]);
} else if (type === "Point") {
const coords = (geom.coordinates ?? []) as [number, number];
console.log("beginEditing Point coords:", coords);
if (coords.length < 2) {
console.warn("beginEditing: Point coords length is less than 2");
return;
}
ring = [[coords[0], coords[1]]];
}
editingRef.current = {
id: feature.id ?? feature.properties?.id,
ring,
@@ -166,7 +261,9 @@ export function createEditingEngine(options: {
isCircle,
circleCenter: geom.circle_center,
circleRadius: geom.circle_radius,
geometryType: type,
};
console.log("beginEditing: initialized editingRef.current:", editingRef.current);
setDeleteVertexMode(false);
updateEditSources();
};
@@ -192,8 +289,8 @@ export function createEditingEngine(options: {
const idx = Number(feature?.properties?.idx);
if (!Number.isInteger(idx)) return;
e.preventDefault();
e.originalEvent.stopPropagation(); // Chặn sự kiện lan ra bản đồ tránh gây kéo/pan bản đồ
if (deleteVertexModeRef.current) {
e.originalEvent.stopPropagation();
deleteVertex(idx);
return;
}
@@ -240,7 +337,7 @@ export function createEditingEngine(options: {
if (!editing) return;
if (e.key === "Enter") {
finishEditing();
} else if (e.key === "Delete" && !editing.isCircle) {
} else if (e.key === "Delete" && editing.geometryType !== "Point" && !editing.isCircle) {
e.preventDefault();
setDeleteVertexMode(!deleteVertexModeRef.current);
} else if (e.key === "Escape") {
@@ -256,7 +353,7 @@ export function createEditingEngine(options: {
// Chuột phải vào handle để mở menu xóa/thêm đỉnh.
const onHandleContextMenu = (e: maplibregl.MapLayerMouseEvent) => {
const editing = editingRef.current;
if (!editing || editing.isCircle) return;
if (!editing || editing.geometryType === "Point" || editing.isCircle) return;
e.preventDefault();
e.originalEvent.stopPropagation();
const feature = e.features?.[0];
@@ -281,15 +378,24 @@ export function createEditingEngine(options: {
document.addEventListener("keydown", onKeyDown);
map.getCanvas().addEventListener("mouseleave", onCanvasLeave);
map.on("remove", () => {
const cleanup = () => {
map.off("mousedown", "edit-handles-circle", onHandleDown);
map.off("contextmenu", "edit-handles-circle", onHandleContextMenu);
map.off("mousemove", onHandleMove);
map.off("mouseup", stopDragging);
document.removeEventListener("keydown", onKeyDown);
map.getCanvas().removeEventListener("mouseleave", onCanvasLeave);
try {
map.getCanvas()?.removeEventListener("mouseleave", onCanvasLeave);
} catch {
// ignore
}
hideContextMenu();
});
map.off("remove", cleanup);
};
map.on("remove", cleanup);
return cleanup;
};
const showHandleContextMenu = (x: number, y: number, idx: number) => {
@@ -328,9 +434,16 @@ export function createEditingEngine(options: {
};
const editing = editingRef.current;
const canDelete = Boolean(editing && !editing.isCircle && editing.ring.length > 3);
if (!editing) return;
const isLine = editing.geometryType === "LineString";
const canDelete = isLine ? editing.ring.length > 2 : editing.ring.length > 3;
const canInsert = isLine ? idx < editing.ring.length - 1 : true;
menu.appendChild(createItem("Xóa đỉnh", () => deleteVertex(idx), !canDelete));
menu.appendChild(createItem("Thêm đỉnh", () => insertVertexAfter(idx)));
if (canInsert) {
menu.appendChild(createItem("Thêm đỉnh", () => insertVertexAfter(idx)));
}
document.body.appendChild(menu);
contextMenu = menu;
@@ -346,7 +459,10 @@ export function createEditingEngine(options: {
const deleteVertex = (idx: number) => {
const editing = editingRef.current;
if (!editing || editing.isCircle || editing.ring.length <= 3) return;
if (!editing || editing.geometryType === "Point" || editing.isCircle) return;
const isLine = editing.geometryType === "LineString";
const minLength = isLine ? 2 : 3;
if (editing.ring.length <= minLength) return;
if (idx < 0 || idx >= editing.ring.length) return;
editing.ring.splice(idx, 1);
updateEditSources();
@@ -354,8 +470,11 @@ export function createEditingEngine(options: {
const insertVertexAfter = (idx: number) => {
const editing = editingRef.current;
if (!editing || editing.isCircle || editing.ring.length < 2) return;
if (!editing || editing.geometryType === "Point" || editing.isCircle || editing.ring.length < 2) return;
if (idx < 0 || idx >= editing.ring.length) return;
const isLine = editing.geometryType === "LineString";
if (isLine && idx === editing.ring.length - 1) return;
const current = editing.ring[idx];
const next = editing.ring[(idx + 1) % editing.ring.length];
const midpoint: [number, number] = [
+5 -2
View File
@@ -367,10 +367,13 @@ export function initSelect(
const canEditGeometry = allowGeometryEditing ? allowGeometryEditing() : true;
if (isLocalTarget && !isClickOutsideSelection && canEditGeometry) {
const isEditableType =
(clickedFeature.source === "countries" && (clickedFeature.geometry?.type === "Polygon" || clickedFeature.geometry?.type === "LineString")) ||
(clickedFeature.source === "places" && clickedFeature.geometry?.type === "Point");
if (
effectiveCount === 1 &&
clickedFeature.source === "countries" &&
clickedFeature.geometry?.type === "Polygon" &&
isEditableType &&
onEdit
) {
const single = clickedFeature;
+2
View File
@@ -7,6 +7,8 @@
{ "type_key": "country", "geo_type_code": 9, "fixed": true },
{ "type_key": "state", "geo_type_code": 10 },
{ "type_key": "region", "geo_type_code": 11 },
{ "type_key": "location", "geo_type_code": 12 },
{ "type_key": "faction", "geo_type_code": 28 },
{ "type_key": "battle", "geo_type_code": 14 },
@@ -75,6 +75,8 @@ const RAW_GEOMETRY_TYPE_OPTIONS: Array<{
{ value: "rebellion_zone", label: "Rebellion Zone", groupId: "circle", geometryPreset: "circle-area" },
{ value: "person_event", label: "Person Event", groupId: "point", geometryPreset: "point" },
{ value: "region", label: "Region (Vùng nhãn tên)", groupId: "point", geometryPreset: "point" },
{ value: "location", label: "Location (Địa điểm chấm)", groupId: "point", geometryPreset: "point" },
{ value: "temple", label: "Temple", groupId: "point", geometryPreset: "point" },
{ value: "capital", label: "Capital", groupId: "point", geometryPreset: "point" },
{ value: "city", label: "City", groupId: "point", geometryPreset: "point" },
+5 -1
View File
@@ -19,6 +19,8 @@ import { getCityLayers } from "./geotypes/city";
import { getFortificationLayers } from "./geotypes/fortification";
import { getRuinLayers } from "./geotypes/ruin";
import { getPortLayers } from "./geotypes/port";
import { getRegionLayers } from "./geotypes/region";
import { getLocationLayers } from "./geotypes/location";
import { getLineLabelLayers } from "./shared/lineLabels";
import { getPolygonLabelLayers } from "./shared/polygonLabels";
@@ -42,7 +44,9 @@ export function getAllGeotypeLayers(sourceId: string, pathArrowSourceId?: string
...getCityLayers(sourceId, pathArrowSourceId, pointSourceId),
...getFortificationLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRuinLayers(sourceId, pathArrowSourceId, pointSourceId),
...getPortLayers(sourceId, pathArrowSourceId, pointSourceId)
...getPortLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRegionLayers(sourceId, pathArrowSourceId, pointSourceId),
...getLocationLayers(sourceId, pathArrowSourceId, pointSourceId)
];
}
@@ -0,0 +1,96 @@
import { LayerSpecification } from "maplibre-gl";
import { MAP_EMPHASIS_TEXT_FONT_STACK } from "../shared/textFonts";
const TYPE_MATCH_EXPR: any = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""];
const POINT_GEOMETRY_FILTER: any = [
"any",
["==", ["geometry-type"], "Point"],
["==", ["geometry-type"], "MultiPoint"],
];
const SELECTED_EXPR: any = ["boolean", ["feature-state", "selected"], false];
const SELECTED_COLOR = "#22c55e";
export function getLocationLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
const typeId = "location";
const filter: any = ["all", POINT_GEOMETRY_FILTER, ["==", TYPE_MATCH_EXPR, typeId]];
return [
{
id: `${typeId}-selected-halo`,
type: "circle",
source: pointSourceId!,
filter: filter,
paint: {
"circle-color": "#e2e8f0",
"circle-radius": ["case", SELECTED_EXPR, 18, 0],
"circle-opacity": ["case", SELECTED_EXPR, 0.24, 0],
"circle-blur": ["case", SELECTED_EXPR, 0.8, 0],
"circle-stroke-color": "#64748b",
"circle-stroke-width": ["case", SELECTED_EXPR, 1.6, 0],
"circle-stroke-opacity": ["case", SELECTED_EXPR, 0.48, 0],
},
},
{
id: `${typeId}-circle`,
type: "circle",
source: pointSourceId!,
filter: filter,
paint: {
"circle-color": [
"case",
SELECTED_EXPR,
SELECTED_COLOR,
["coalesce", ["get", "entity_color"], "#e2e8f0"],
],
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
1, 2.5,
4, 3.5,
6, 4.5,
],
"circle-stroke-color": [
"case",
SELECTED_EXPR,
SELECTED_COLOR,
["coalesce", ["get", "entity_color"], "#475569"],
],
"circle-stroke-width": 1.5,
},
},
{
id: `${typeId}-label`,
type: "symbol",
source: pointSourceId!,
filter: filter,
layout: {
"text-font": [...MAP_EMPHASIS_TEXT_FONT_STACK],
"text-field": ["coalesce", ["get", "point_label"], ""],
"text-size": [
"interpolate",
["linear"],
["zoom"],
1, 10.5,
4, 12,
6, 14,
],
"text-anchor": "top",
"text-offset": [0, 0.6],
"text-allow-overlap": true,
"text-ignore-placement": true,
"text-optional": true,
"text-max-width": 12,
},
paint: {
"text-color": "#f8fafc",
"text-halo-color": "#0f172a",
"text-halo-width": 1.4,
"text-halo-blur": 0.3,
},
}
];
}
+56
View File
@@ -0,0 +1,56 @@
import { LayerSpecification } from "maplibre-gl";
import { MAP_EMPHASIS_TEXT_FONT_STACK } from "../shared/textFonts";
const TYPE_MATCH_EXPR: any = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""];
const POINT_GEOMETRY_FILTER: any = [
"any",
["==", ["geometry-type"], "Point"],
["==", ["geometry-type"], "MultiPoint"],
];
const SELECTED_EXPR: any = ["boolean", ["feature-state", "selected"], false];
const SELECTED_COLOR = "#22c55e";
export function getRegionLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
const typeId = "region";
const filter: any = ["all", POINT_GEOMETRY_FILTER, ["==", TYPE_MATCH_EXPR, typeId]];
return [
{
id: `${typeId}-label`,
type: "symbol",
source: pointSourceId!,
filter: filter,
layout: {
"text-font": [...MAP_EMPHASIS_TEXT_FONT_STACK],
"text-field": ["coalesce", ["get", "point_label"], ""],
"text-size": [
"interpolate",
["linear"],
["zoom"],
1, 11,
4, 13,
6, 16,
],
"text-anchor": "center",
"text-allow-overlap": true,
"text-ignore-placement": true,
"text-max-width": 12,
},
paint: {
"text-color": [
"case",
SELECTED_EXPR,
SELECTED_COLOR,
"#f8fafc",
],
"text-halo-color": "#0f172a",
"text-halo-width": 1.6,
"text-halo-blur": 0.5,
},
}
];
}
@@ -7,7 +7,6 @@ export function getRetreatRouteLayers(sourceId: string, pathArrowSourceId?: stri
typeId: "retreat_route",
color: "#94a3b8",
strokeColor: "#475569",
dasharray: [6, 3],
opacity: 0.82,
arrowOpacity: 0.68,
showLine: false,