feat: implement updating circle
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export default function NewBadge({ title = "Created in this session and not committed yet" }: Props) {
|
||||
return (
|
||||
<span style={badgeStyle} title={title}>
|
||||
new
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const badgeStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
height: 17,
|
||||
padding: "0 6px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(45, 212, 191, 0.55)",
|
||||
background: "rgba(20, 184, 166, 0.16)",
|
||||
color: "#5eead4",
|
||||
fontSize: 10,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0,
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
|
||||
import { buildCircleRing, distanceMeters } from "@/uhm/lib/map/geo/geoMath";
|
||||
|
||||
const EARTH_RADIUS_METERS = 6371008.8;
|
||||
const CIRCLE_SEGMENTS = 72;
|
||||
const MIN_RADIUS_METERS = 1;
|
||||
const EMPTY_PREVIEW: GeoJSON.FeatureCollection = {
|
||||
@@ -123,6 +123,8 @@ export function initCircle(
|
||||
onComplete({
|
||||
type: "Polygon",
|
||||
coordinates: [ring],
|
||||
circle_center: center,
|
||||
circle_radius: radiusMeters,
|
||||
});
|
||||
resetDrawingState();
|
||||
};
|
||||
@@ -163,85 +165,3 @@ export function initCircle(
|
||||
cancel: resetDrawingState,
|
||||
};
|
||||
}
|
||||
|
||||
// Tạo vòng polygon xấp xỉ hình tròn từ tâm, bán kính và số phân đoạn.
|
||||
function buildCircleRing(
|
||||
center: [number, number],
|
||||
radiusMeters: number,
|
||||
segments: number
|
||||
): [number, number][] {
|
||||
const ring: [number, number][] = [];
|
||||
for (let i = 0; i <= segments; i += 1) {
|
||||
const bearingDeg = (i / segments) * 360; // Chia đều 360 do quanh tâm để tạo các điểm trên vòng tròn.
|
||||
ring.push(destinationPoint(center, radiusMeters, bearingDeg));
|
||||
}
|
||||
return ring;
|
||||
}
|
||||
|
||||
// Tính khoảng cách hai điểm theo công thức Haversine (đơn vị mét).
|
||||
function distanceMeters(a: [number, number], b: [number, number]): number {
|
||||
const lat1 = toRad(a[1]);
|
||||
const lat2 = toRad(b[1]);
|
||||
const dLat = lat2 - lat1; // Delta vĩ độ (radian).
|
||||
const dLng = toRad(b[0] - a[0]); // Delta kinh độ (radian).
|
||||
|
||||
const sinLat = Math.sin(dLat / 2); // Thành phần sin(dLat/2) của công thức Haversine.
|
||||
const sinLng = Math.sin(dLng / 2); // Thành phần sin(dLng/2) của công thức Haversine.
|
||||
const h =
|
||||
sinLat * sinLat +
|
||||
Math.cos(lat1) * Math.cos(lat2) * sinLng * sinLng; // h = haversine(d/R), độ lớn cung tròn chuẩn hóa.
|
||||
const c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); // Góc tâm (radian) giữa hai điểm trên mặt cầu.
|
||||
return EARTH_RADIUS_METERS * c; // Khoảng cách cung tròn: d = R * c.
|
||||
}
|
||||
|
||||
// Tính tọa độ điểm đích từ tâm, khoảng cách và góc phương vị.
|
||||
function destinationPoint(
|
||||
center: [number, number],
|
||||
distance: number,
|
||||
bearingDeg: number
|
||||
): [number, number] {
|
||||
const lat1 = toRad(center[1]);
|
||||
const lng1 = toRad(center[0]);
|
||||
const bearing = toRad(bearingDeg);
|
||||
const angularDistance = distance / EARTH_RADIUS_METERS; // d/R: khoảng cách góc trên mặt cầu.
|
||||
|
||||
const sinLat1 = Math.sin(lat1);
|
||||
const cosLat1 = Math.cos(lat1);
|
||||
const sinAngular = Math.sin(angularDistance);
|
||||
const cosAngular = Math.cos(angularDistance);
|
||||
|
||||
const sinLat2 =
|
||||
sinLat1 * cosAngular +
|
||||
cosLat1 * sinAngular * Math.cos(bearing); // Công thức vĩ độ điểm đích theo great-circle.
|
||||
const lat2 = Math.asin(clamp(sinLat2, -1, 1)); // Kẹp [-1,1] để tránh sai số số học trước khi asin.
|
||||
|
||||
const y = Math.sin(bearing) * sinAngular * cosLat1; // Tử số atan2 cho biến thiên kinh độ.
|
||||
const x = cosAngular - sinLat1 * Math.sin(lat2); // Mẫu số atan2 cho biến thiên kinh độ.
|
||||
const lng2 = lng1 + Math.atan2(y, x); // Kinh độ đích = kinh độ gốc + delta kinh độ.
|
||||
|
||||
return [normalizeLng(toDeg(lng2)), toDeg(lat2)];
|
||||
}
|
||||
|
||||
// Chuẩn hóa kinh độ về miền [-180, 180].
|
||||
function normalizeLng(lng: number): number {
|
||||
let normalized = ((lng + 540) % 360) - 180; // Wrap về khoảng [-180, 180).
|
||||
if (normalized === -180) normalized = 180;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Kẹp giá trị trong đoạn [min, max].
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
// Đổi đơn vị góc từ độ sang radian.
|
||||
function toRad(value: number): number {
|
||||
return (value * Math.PI) / 180; // Đổi độ sang radian.
|
||||
}
|
||||
|
||||
// Đổi đơn vị góc từ radian sang độ.
|
||||
function toDeg(value: number): number {
|
||||
return (value * 180) / Math.PI; // Đổi radian sang độ.
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||
import { buildCircleRing, destinationPoint, distanceMeters } from "@/uhm/lib/map/geo/geoMath";
|
||||
|
||||
export type EditingHandle = {
|
||||
id: string | number;
|
||||
ring: [number, number][];
|
||||
original: Geometry;
|
||||
isCircle?: boolean;
|
||||
circleCenter?: [number, number];
|
||||
circleRadius?: number;
|
||||
};
|
||||
|
||||
export type EditingAPI = {
|
||||
@@ -40,8 +44,13 @@ export function createEditingEngine(options: {
|
||||
const map = mapRef.current;
|
||||
if (!editing || !map) return;
|
||||
|
||||
const closedRing = [...editing.ring, editing.ring[0]];
|
||||
const shape: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
|
||||
let shape: GeoJSON.FeatureCollection<GeoJSON.Polygon>;
|
||||
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: [
|
||||
{
|
||||
@@ -52,7 +61,37 @@ export function createEditingEngine(options: {
|
||||
],
|
||||
};
|
||||
|
||||
const handles: GeoJSON.FeatureCollection<GeoJSON.Point> = {
|
||||
// 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",
|
||||
@@ -60,6 +99,7 @@ export function createEditingEngine(options: {
|
||||
properties: { idx },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
(map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(shape);
|
||||
(map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(handles);
|
||||
@@ -69,10 +109,23 @@ export function createEditingEngine(options: {
|
||||
const finishEditing = () => {
|
||||
const editing = editingRef.current;
|
||||
if (!editing) return;
|
||||
const geometry: Geometry = {
|
||||
|
||||
let geometry: Geometry;
|
||||
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]]],
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate(editing.id, geometry);
|
||||
clearEditing();
|
||||
};
|
||||
@@ -85,15 +138,21 @@ export function createEditingEngine(options: {
|
||||
// Bắt đầu chỉnh sửa từ feature polygon được chọn.
|
||||
const beginEditing = (feature: maplibregl.MapGeoJSONFeature) => {
|
||||
if (feature.geometry.type !== "Polygon") return;
|
||||
const coords = (feature.geometry.coordinates?.[0] ?? []) as [number, number][];
|
||||
const geom = feature.geometry as Geometry;
|
||||
const coords = (geom.coordinates?.[0] ?? []) as [number, number][];
|
||||
if (coords.length < 4) 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]);
|
||||
editingRef.current = {
|
||||
id: feature.id ?? feature.properties?.id,
|
||||
ring,
|
||||
original: feature.geometry as Geometry,
|
||||
original: geom,
|
||||
isCircle,
|
||||
circleCenter: geom.circle_center,
|
||||
circleRadius: geom.circle_radius,
|
||||
};
|
||||
updateEditSources();
|
||||
};
|
||||
@@ -129,7 +188,20 @@ export function createEditingEngine(options: {
|
||||
const editing = editingRef.current;
|
||||
if (!drag || !editing) return;
|
||||
|
||||
if (editing.isCircle && editing.circleCenter && editing.circleRadius !== undefined) {
|
||||
if (drag.idx === 0) {
|
||||
// Move center
|
||||
editing.circleCenter = [e.lngLat.lng, e.lngLat.lat];
|
||||
} else if (drag.idx === 1) {
|
||||
// Change radius
|
||||
editing.circleRadius = distanceMeters(editing.circleCenter, [
|
||||
e.lngLat.lng,
|
||||
e.lngLat.lat,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
editing.ring[drag.idx] = [e.lngLat.lng, e.lngLat.lat];
|
||||
}
|
||||
updateEditSources();
|
||||
};
|
||||
|
||||
@@ -166,7 +238,7 @@ export function createEditingEngine(options: {
|
||||
|
||||
// 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) return;
|
||||
if (!editingRef.current || editingRef.current.isCircle) return;
|
||||
if (!isModifierPressed(e)) return;
|
||||
e.preventDefault();
|
||||
const editing = editingRef.current;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
const EARTH_RADIUS_METERS = 6371008.8;
|
||||
|
||||
// Đổi đơn vị góc từ độ sang radian.
|
||||
export function toRad(value: number): number {
|
||||
return (value * Math.PI) / 180;
|
||||
}
|
||||
|
||||
// Đổi đơn vị góc từ radian sang độ.
|
||||
export function toDeg(value: number): number {
|
||||
return (value * 180) / Math.PI;
|
||||
}
|
||||
|
||||
// Kẹp giá trị trong đoạn [min, max].
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
// Chuẩn hóa kinh độ về miền [-180, 180].
|
||||
export function normalizeLng(lng: number): number {
|
||||
let normalized = ((lng + 540) % 360) - 180;
|
||||
if (normalized === -180) normalized = 180;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Tính khoảng cách hai điểm theo công thức Haversine (đơn vị mét).
|
||||
export function distanceMeters(a: [number, number], b: [number, number]): number {
|
||||
const lat1 = toRad(a[1]);
|
||||
const lat2 = toRad(b[1]);
|
||||
const dLat = lat2 - lat1;
|
||||
const dLng = toRad(b[0] - a[0]);
|
||||
|
||||
const sinLat = Math.sin(dLat / 2);
|
||||
const sinLng = Math.sin(dLng / 2);
|
||||
const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLng * sinLng;
|
||||
const c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
||||
return EARTH_RADIUS_METERS * c;
|
||||
}
|
||||
|
||||
// Tính tọa độ điểm đích từ tâm, khoảng cách và góc phương vị.
|
||||
export function destinationPoint(
|
||||
center: [number, number],
|
||||
distance: number,
|
||||
bearingDeg: number
|
||||
): [number, number] {
|
||||
const lat1 = toRad(center[1]);
|
||||
const lng1 = toRad(center[0]);
|
||||
const bearing = toRad(bearingDeg);
|
||||
const angularDistance = distance / EARTH_RADIUS_METERS;
|
||||
|
||||
const sinLat1 = Math.sin(lat1);
|
||||
const cosLat1 = Math.cos(lat1);
|
||||
const sinAngular = Math.sin(angularDistance);
|
||||
const cosAngular = Math.cos(angularDistance);
|
||||
|
||||
const sinLat2 = sinLat1 * cosAngular + cosLat1 * sinAngular * Math.cos(bearing);
|
||||
const lat2 = Math.asin(clamp(sinLat2, -1, 1));
|
||||
|
||||
const y = Math.sin(bearing) * sinAngular * cosLat1;
|
||||
const x = cosAngular - sinLat1 * Math.sin(lat2);
|
||||
const lng2 = lng1 + Math.atan2(y, x);
|
||||
|
||||
return [normalizeLng(toDeg(lng2)), toDeg(lat2)];
|
||||
}
|
||||
|
||||
// Tạo vòng polygon xấp xỉ hình tròn từ tâm, bán kính và số phân đoạn.
|
||||
export function buildCircleRing(
|
||||
center: [number, number],
|
||||
radiusMeters: number,
|
||||
segments: number = 72
|
||||
): [number, number][] {
|
||||
const ring: [number, number][] = [];
|
||||
for (let i = 0; i <= segments; i += 1) {
|
||||
const bearingDeg = (i / segments) * 360;
|
||||
ring.push(destinationPoint(center, radiusMeters, bearingDeg));
|
||||
}
|
||||
return ring;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
||||
|
||||
const LINE_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||
"any",
|
||||
["==", ["geometry-type"], "LineString"],
|
||||
["==", ["geometry-type"], "MultiLineString"],
|
||||
];
|
||||
|
||||
export function getLineLabelLayers(sourceId: string): LayerSpecification[] {
|
||||
return [
|
||||
{
|
||||
id: "line-labels-text",
|
||||
type: "symbol",
|
||||
source: sourceId,
|
||||
filter: ["all", LINE_GEOMETRY_FILTER, ["!=", ["coalesce", ["get", "line_label"], ""], ""]],
|
||||
layout: {
|
||||
"symbol-placement": "line",
|
||||
"symbol-spacing": 280,
|
||||
"text-field": ["coalesce", ["get", "line_label"], ""],
|
||||
"text-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
1, 11,
|
||||
4, 13,
|
||||
6, 15,
|
||||
],
|
||||
"text-keep-upright": true,
|
||||
"text-max-angle": 35,
|
||||
"text-max-width": 12,
|
||||
"text-padding": 2,
|
||||
"text-allow-overlap": false,
|
||||
"text-ignore-placement": false,
|
||||
"text-optional": true,
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#f8fafc",
|
||||
"text-halo-color": "#0f172a",
|
||||
"text-halo-width": 1.4,
|
||||
"text-halo-blur": 0.25,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
||||
|
||||
export const POINT_GEOTYPE_IDS = [
|
||||
"person_birthplace",
|
||||
"person_deathplace",
|
||||
"person_activity",
|
||||
"temple",
|
||||
"capital",
|
||||
"city",
|
||||
"fortress",
|
||||
"castle",
|
||||
"ruin",
|
||||
"port",
|
||||
"bridge",
|
||||
] as const;
|
||||
|
||||
export type PointGeotypeId = (typeof POINT_GEOTYPE_IDS)[number];
|
||||
|
||||
type PointIconVariant = "default" | "draft";
|
||||
|
||||
type PointLayerOptions = {
|
||||
iconScale?: number;
|
||||
haloRadius?: number;
|
||||
};
|
||||
|
||||
type PointStyleConfig = {
|
||||
fill: string;
|
||||
rim: string;
|
||||
iconScale: number;
|
||||
haloRadius: number;
|
||||
drawGlyph: (ctx: CanvasRenderingContext2D) => void;
|
||||
};
|
||||
|
||||
const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""];
|
||||
const DRAFT_ENTITY_EXPR: maplibregl.ExpressionSpecification = ["==", ["coalesce", ["get", "entity_id"], ""], ""];
|
||||
const SELECTED_EXPR: maplibregl.ExpressionSpecification = ["boolean", ["feature-state", "selected"], false];
|
||||
|
||||
const ICON_CANVAS_SIZE = 64;
|
||||
const DRAFT_FILL = "#ef4444";
|
||||
const DRAFT_RIM = "#7f1d1d";
|
||||
const POINT_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||
"any",
|
||||
["==", ["geometry-type"], "Point"],
|
||||
["==", ["geometry-type"], "MultiPoint"],
|
||||
];
|
||||
|
||||
const POINT_STYLE_CONFIG: Record<PointGeotypeId, PointStyleConfig> = {
|
||||
person_birthplace: {
|
||||
fill: "#22c55e",
|
||||
rim: "#166534",
|
||||
iconScale: 1,
|
||||
haloRadius: 15,
|
||||
drawGlyph: drawHouseGlyph,
|
||||
},
|
||||
person_deathplace: {
|
||||
fill: "#b91c1c",
|
||||
rim: "#450a0a",
|
||||
iconScale: 1,
|
||||
haloRadius: 15,
|
||||
drawGlyph: drawMemorialGlyph,
|
||||
},
|
||||
person_activity: {
|
||||
fill: "#f97316",
|
||||
rim: "#9a3412",
|
||||
iconScale: 0.98,
|
||||
haloRadius: 14,
|
||||
drawGlyph: drawFlagGlyph,
|
||||
},
|
||||
temple: {
|
||||
fill: "#d97706",
|
||||
rim: "#78350f",
|
||||
iconScale: 1.02,
|
||||
haloRadius: 15,
|
||||
drawGlyph: drawTempleGlyph,
|
||||
},
|
||||
capital: {
|
||||
fill: "#eab308",
|
||||
rim: "#854d0e",
|
||||
iconScale: 1.08,
|
||||
haloRadius: 17,
|
||||
drawGlyph: drawCrownGlyph,
|
||||
},
|
||||
city: {
|
||||
fill: "#2563eb",
|
||||
rim: "#1e3a8a",
|
||||
iconScale: 1.02,
|
||||
haloRadius: 15,
|
||||
drawGlyph: drawCityGlyph,
|
||||
},
|
||||
fortress: {
|
||||
fill: "#64748b",
|
||||
rim: "#334155",
|
||||
iconScale: 1.04,
|
||||
haloRadius: 16,
|
||||
drawGlyph: drawShieldGlyph,
|
||||
},
|
||||
castle: {
|
||||
fill: "#7c3aed",
|
||||
rim: "#4c1d95",
|
||||
iconScale: 1.04,
|
||||
haloRadius: 16,
|
||||
drawGlyph: drawCastleGlyph,
|
||||
},
|
||||
ruin: {
|
||||
fill: "#78716c",
|
||||
rim: "#44403c",
|
||||
iconScale: 0.98,
|
||||
haloRadius: 14,
|
||||
drawGlyph: drawRuinGlyph,
|
||||
},
|
||||
port: {
|
||||
fill: "#0284c7",
|
||||
rim: "#075985",
|
||||
iconScale: 1.02,
|
||||
haloRadius: 15,
|
||||
drawGlyph: drawAnchorGlyph,
|
||||
},
|
||||
bridge: {
|
||||
fill: "#b45309",
|
||||
rim: "#7c2d12",
|
||||
iconScale: 1,
|
||||
haloRadius: 14,
|
||||
drawGlyph: drawBridgeGlyph,
|
||||
},
|
||||
};
|
||||
|
||||
export function buildPointGeotypeLayers(
|
||||
typeId: PointGeotypeId,
|
||||
pointSourceId: string,
|
||||
options: PointLayerOptions = {}
|
||||
): LayerSpecification[] {
|
||||
const config = POINT_STYLE_CONFIG[typeId];
|
||||
const haloRadius = (options.haloRadius ?? config.haloRadius) * 2;
|
||||
const iconScale = options.iconScale ?? config.iconScale;
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${typeId}-selected-halo`,
|
||||
type: "circle",
|
||||
source: pointSourceId,
|
||||
filter: pointFilter(typeId),
|
||||
paint: {
|
||||
"circle-color": "#22c55e",
|
||||
"circle-radius": ["case", SELECTED_EXPR, haloRadius, 0],
|
||||
"circle-opacity": ["case", SELECTED_EXPR, 0.24, 0],
|
||||
"circle-blur": ["case", SELECTED_EXPR, 0.8, 0],
|
||||
"circle-stroke-color": "#14532d",
|
||||
"circle-stroke-width": ["case", SELECTED_EXPR, 1.6, 0],
|
||||
"circle-stroke-opacity": ["case", SELECTED_EXPR, 0.48, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `${typeId}-circle`,
|
||||
type: "symbol",
|
||||
source: pointSourceId,
|
||||
filter: pointFilter(typeId),
|
||||
layout: {
|
||||
"icon-image": pointIconExpression(typeId),
|
||||
"icon-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
1, 0.96 * iconScale,
|
||||
4, 1.24 * iconScale,
|
||||
6, 1.52 * iconScale,
|
||||
],
|
||||
"icon-anchor": "center",
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
"symbol-placement": "point",
|
||||
"text-field": ["coalesce", ["get", "point_label"], ""],
|
||||
"text-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
1, 11,
|
||||
4, 13,
|
||||
6, 15,
|
||||
],
|
||||
"text-anchor": "bottom",
|
||||
"text-offset": [0, -1.25],
|
||||
"text-allow-overlap": true,
|
||||
"text-ignore-placement": true,
|
||||
"text-optional": true,
|
||||
"text-max-width": 12,
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": 0.98,
|
||||
"text-color": "#f8fafc",
|
||||
"text-halo-color": "#0f172a",
|
||||
"text-halo-width": 1.4,
|
||||
"text-halo-blur": 0.3,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function ensurePointGeotypeIcons(map: maplibregl.Map): boolean {
|
||||
if (typeof document === "undefined") return false;
|
||||
|
||||
for (const typeId of POINT_GEOTYPE_IDS) {
|
||||
for (const variant of ["default", "draft"] as const) {
|
||||
const iconId = getPointIconId(typeId, variant);
|
||||
if (map.hasImage(iconId)) continue;
|
||||
const imageData = createPointIconImageData(typeId, variant);
|
||||
if (!imageData) return false;
|
||||
map.addImage(iconId, imageData, { pixelRatio: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function pointFilter(typeId: PointGeotypeId): maplibregl.ExpressionSpecification {
|
||||
return ["all", POINT_GEOMETRY_FILTER, ["==", TYPE_MATCH_EXPR, typeId]];
|
||||
}
|
||||
|
||||
function pointIconExpression(typeId: PointGeotypeId): maplibregl.ExpressionSpecification {
|
||||
return ["case", DRAFT_ENTITY_EXPR, getPointIconId(typeId, "draft"), getPointIconId(typeId, "default")];
|
||||
}
|
||||
|
||||
function getPointIconId(typeId: PointGeotypeId, variant: PointIconVariant): string {
|
||||
return `point-${typeId}-${variant}`;
|
||||
}
|
||||
|
||||
function createPointIconImageData(typeId: PointGeotypeId, variant: PointIconVariant): ImageData | null {
|
||||
const config = POINT_STYLE_CONFIG[typeId];
|
||||
const palette = variant === "draft"
|
||||
? { fill: DRAFT_FILL, rim: DRAFT_RIM }
|
||||
: { fill: config.fill, rim: config.rim };
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = ICON_CANVAS_SIZE;
|
||||
canvas.height = ICON_CANVAS_SIZE;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
|
||||
ctx.clearRect(0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||
drawGlyphWithOutline(ctx, palette.fill, palette.rim, () => config.drawGlyph(ctx));
|
||||
|
||||
return ctx.getImageData(0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||
}
|
||||
|
||||
function drawGlyphWithOutline(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
fill: string,
|
||||
rim: string,
|
||||
draw: () => void
|
||||
) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(15, 23, 42, 0.35)";
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.shadowOffsetY = 2;
|
||||
ctx.strokeStyle = rim;
|
||||
ctx.fillStyle = rim;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
draw();
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = fill;
|
||||
ctx.fillStyle = fill;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
draw();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawHouseGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(22, 34);
|
||||
ctx.lineTo(32, 24);
|
||||
ctx.lineTo(42, 34);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(25.5, 34, 13, 9);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 43);
|
||||
ctx.lineTo(32, 36.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawMemorialGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 22);
|
||||
ctx.lineTo(32, 43);
|
||||
ctx.moveTo(25, 28.5);
|
||||
ctx.lineTo(39, 28.5);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(24, 45);
|
||||
ctx.lineTo(40, 45);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawFlagGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(26, 22);
|
||||
ctx.lineTo(26, 43);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(28, 23);
|
||||
ctx.lineTo(40, 27);
|
||||
ctx.lineTo(28, 31);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(22.5, 44.5);
|
||||
ctx.lineTo(31, 44.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawTempleGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(22, 30);
|
||||
ctx.lineTo(32, 22);
|
||||
ctx.lineTo(42, 30);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(21, 31);
|
||||
ctx.lineTo(43, 31);
|
||||
ctx.moveTo(23, 42);
|
||||
ctx.lineTo(41, 42);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2.8;
|
||||
for (const x of [26, 32, 38]) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 31);
|
||||
ctx.lineTo(x, 42);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCrownGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(22, 41);
|
||||
ctx.lineTo(24.5, 28);
|
||||
ctx.lineTo(30, 34);
|
||||
ctx.lineTo(32, 23);
|
||||
ctx.lineTo(34, 34);
|
||||
ctx.lineTo(39.5, 28);
|
||||
ctx.lineTo(42, 41);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(23.5, 41.5);
|
||||
ctx.lineTo(40.5, 41.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCityGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.fillRect(23, 33, 7, 10);
|
||||
ctx.fillRect(30, 27, 6, 16);
|
||||
ctx.fillRect(36, 30, 6, 13);
|
||||
|
||||
ctx.clearRect(25, 36, 1.5, 1.5);
|
||||
ctx.clearRect(25, 39, 1.5, 1.5);
|
||||
ctx.clearRect(32, 31, 1.5, 1.5);
|
||||
ctx.clearRect(32, 35, 1.5, 1.5);
|
||||
ctx.clearRect(38, 33, 1.5, 1.5);
|
||||
ctx.clearRect(38, 37, 1.5, 1.5);
|
||||
}
|
||||
|
||||
function drawShieldGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 22.5);
|
||||
ctx.lineTo(41, 26.5);
|
||||
ctx.lineTo(39, 37.5);
|
||||
ctx.lineTo(32, 43);
|
||||
ctx.lineTo(25, 37.5);
|
||||
ctx.lineTo(23, 26.5);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 25);
|
||||
ctx.lineTo(32, 39);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCastleGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.rect(24, 31, 16, 11);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(24, 31);
|
||||
ctx.lineTo(24, 26);
|
||||
ctx.lineTo(28, 26);
|
||||
ctx.lineTo(28, 29);
|
||||
ctx.lineTo(32, 29);
|
||||
ctx.lineTo(32, 24);
|
||||
ctx.lineTo(36, 24);
|
||||
ctx.lineTo(36, 29);
|
||||
ctx.lineTo(40, 29);
|
||||
ctx.lineTo(40, 26);
|
||||
ctx.lineTo(44, 26);
|
||||
ctx.lineTo(44, 31);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 42);
|
||||
ctx.lineTo(32, 34);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawRuinGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.rect(26, 24, 12, 18);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(24, 24);
|
||||
ctx.lineTo(40, 24);
|
||||
ctx.moveTo(24, 42);
|
||||
ctx.lineTo(40, 42);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(34, 24);
|
||||
ctx.lineTo(31, 29);
|
||||
ctx.lineTo(35, 33);
|
||||
ctx.lineTo(30, 39);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawAnchorGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 22.5, 3.5, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 26.5);
|
||||
ctx.lineTo(32, 41);
|
||||
ctx.moveTo(24, 31.5);
|
||||
ctx.lineTo(40, 31.5);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 35.5, 9, 0.2 * Math.PI, 0.8 * Math.PI);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(24.5, 38);
|
||||
ctx.lineTo(21.5, 34);
|
||||
ctx.moveTo(39.5, 38);
|
||||
ctx.lineTo(42.5, 34);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawBridgeGlyph(ctx: CanvasRenderingContext2D) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(21, 31);
|
||||
ctx.lineTo(43, 31);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(22, 40);
|
||||
ctx.quadraticCurveTo(32, 26, 42, 40);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(26, 37);
|
||||
ctx.lineTo(26, 31);
|
||||
ctx.moveTo(32, 33.5);
|
||||
ctx.lineTo(32, 31);
|
||||
ctx.moveTo(38, 37);
|
||||
ctx.lineTo(38, 31);
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LayerSpecification } from "maplibre-gl";
|
||||
|
||||
export function getPolygonLabelLayers(sourceId: string): LayerSpecification[] {
|
||||
return [
|
||||
{
|
||||
id: "polygon-labels-text",
|
||||
type: "symbol",
|
||||
source: sourceId,
|
||||
layout: {
|
||||
"text-field": ["coalesce", ["get", "polygon_label"], ""],
|
||||
"text-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
1, 12,
|
||||
4, 15,
|
||||
6, 18,
|
||||
],
|
||||
"text-anchor": "center",
|
||||
"text-allow-overlap": true,
|
||||
"text-ignore-placement": true,
|
||||
"text-max-width": 14,
|
||||
"symbol-placement": "point",
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#f8fafc",
|
||||
"text-halo-color": "#0f172a",
|
||||
"text-halo-width": 1.6,
|
||||
"text-halo-blur": 0.35,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
||||
|
||||
const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""];
|
||||
const DRAFT_ENTITY_EXPR: maplibregl.ExpressionSpecification = ["==", ["coalesce", ["get", "entity_id"], ""], ""];
|
||||
const SELECTED_EXPR: maplibregl.ExpressionSpecification = ["boolean", ["feature-state", "selected"], false];
|
||||
|
||||
const SELECTED_COLOR = "#22c55e";
|
||||
const SELECTED_STROKE = "#14532d";
|
||||
const DRAFT_COLOR = "#ef4444";
|
||||
const DRAFT_STROKE = "#7f1d1d";
|
||||
|
||||
type ZoomStops = {
|
||||
z1: number;
|
||||
z4: number;
|
||||
z6: number;
|
||||
};
|
||||
|
||||
type LineGeotypeStyle = {
|
||||
typeId: string;
|
||||
color: string;
|
||||
strokeColor?: string;
|
||||
opacity?: number;
|
||||
width?: ZoomStops;
|
||||
dasharray?: number[];
|
||||
arrow?: boolean;
|
||||
arrowOpacity?: number;
|
||||
arrowOutlineColor?: string;
|
||||
arrowOutlineWidth?: ZoomStops;
|
||||
};
|
||||
|
||||
type PolygonGeotypeStyle = {
|
||||
typeId: string;
|
||||
fillColor: string;
|
||||
strokeColor: string;
|
||||
fillOpacity: number;
|
||||
strokeWidth?: ZoomStops;
|
||||
dasharray?: number[];
|
||||
};
|
||||
|
||||
const DEFAULT_LINE_WIDTH: ZoomStops = { z1: 2.2, z4: 3.2, z6: 4.2 };
|
||||
const DEFAULT_ARROW_OUTLINE_WIDTH: ZoomStops = { z1: 0.45, z4: 0.8, z6: 1.2 };
|
||||
const DEFAULT_POLYGON_STROKE_WIDTH: ZoomStops = { z1: 1.4, z4: 2, z6: 2.8 };
|
||||
const LINE_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||
"any",
|
||||
["==", ["geometry-type"], "LineString"],
|
||||
["==", ["geometry-type"], "MultiLineString"],
|
||||
];
|
||||
const POLYGON_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||
"any",
|
||||
["==", ["geometry-type"], "Polygon"],
|
||||
["==", ["geometry-type"], "MultiPolygon"],
|
||||
];
|
||||
|
||||
export function buildLineGeotypeLayers(
|
||||
sourceId: string,
|
||||
pathArrowSourceId: string | undefined,
|
||||
style: LineGeotypeStyle
|
||||
): LayerSpecification[] {
|
||||
const lineLayer: LayerSpecification = {
|
||||
id: `${style.typeId}-line`,
|
||||
type: "line",
|
||||
source: sourceId,
|
||||
filter: lineFilter(style.typeId),
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": statusColor(style.color),
|
||||
"line-width": widthStops(style.width ?? DEFAULT_LINE_WIDTH),
|
||||
"line-opacity": style.opacity ?? 0.9,
|
||||
...(style.dasharray ? { "line-dasharray": style.dasharray } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const hitLayer: LayerSpecification = {
|
||||
id: `${style.typeId}-hit`,
|
||||
type: "line",
|
||||
source: sourceId,
|
||||
filter: lineFilter(style.typeId),
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": widthStops({ z1: 12, z4: 18, z6: 24 }),
|
||||
"line-opacity": 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (style.arrow === false || !pathArrowSourceId) {
|
||||
return [lineLayer, hitLayer];
|
||||
}
|
||||
|
||||
return [
|
||||
lineLayer,
|
||||
hitLayer,
|
||||
{
|
||||
id: `${style.typeId}-path-arrow-fill`,
|
||||
type: "fill",
|
||||
source: pathArrowSourceId,
|
||||
filter: ["==", TYPE_MATCH_EXPR, style.typeId],
|
||||
paint: {
|
||||
"fill-color": statusColor(style.color),
|
||||
"fill-opacity": [
|
||||
"case",
|
||||
SELECTED_EXPR,
|
||||
0.92,
|
||||
style.arrowOpacity ?? 0.82,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `${style.typeId}-path-arrow-line`,
|
||||
type: "line",
|
||||
source: pathArrowSourceId,
|
||||
filter: ["==", TYPE_MATCH_EXPR, style.typeId],
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": statusStroke(style.arrowOutlineColor ?? style.strokeColor ?? "#0f172a"),
|
||||
"line-width": widthStops(style.arrowOutlineWidth ?? DEFAULT_ARROW_OUTLINE_WIDTH),
|
||||
"line-opacity": 0.9,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildPolygonGeotypeLayers(
|
||||
sourceId: string,
|
||||
style: PolygonGeotypeStyle
|
||||
): LayerSpecification[] {
|
||||
return [
|
||||
{
|
||||
id: `${style.typeId}-fill`,
|
||||
type: "fill",
|
||||
source: sourceId,
|
||||
filter: polygonFilter(style.typeId),
|
||||
paint: {
|
||||
"fill-color": statusColor(style.fillColor),
|
||||
"fill-opacity": [
|
||||
"case",
|
||||
SELECTED_EXPR,
|
||||
0.58,
|
||||
style.fillOpacity,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `${style.typeId}-line`,
|
||||
type: "line",
|
||||
source: sourceId,
|
||||
filter: polygonFilter(style.typeId),
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": statusStroke(style.strokeColor),
|
||||
"line-width": widthStops(style.strokeWidth ?? DEFAULT_POLYGON_STROKE_WIDTH),
|
||||
"line-opacity": 0.95,
|
||||
...(style.dasharray ? { "line-dasharray": style.dasharray } : {}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function statusColor(normalColor: string): maplibregl.ExpressionSpecification {
|
||||
return [
|
||||
"case",
|
||||
SELECTED_EXPR,
|
||||
SELECTED_COLOR,
|
||||
DRAFT_ENTITY_EXPR,
|
||||
DRAFT_COLOR,
|
||||
normalColor,
|
||||
];
|
||||
}
|
||||
|
||||
function statusStroke(normalColor: string): maplibregl.ExpressionSpecification {
|
||||
return [
|
||||
"case",
|
||||
SELECTED_EXPR,
|
||||
SELECTED_STROKE,
|
||||
DRAFT_ENTITY_EXPR,
|
||||
DRAFT_STROKE,
|
||||
normalColor,
|
||||
];
|
||||
}
|
||||
|
||||
function lineFilter(typeId: string): maplibregl.ExpressionSpecification {
|
||||
return ["all", LINE_GEOMETRY_FILTER, ["==", TYPE_MATCH_EXPR, typeId]];
|
||||
}
|
||||
|
||||
function polygonFilter(typeId: string): maplibregl.ExpressionSpecification {
|
||||
return ["all", POLYGON_GEOMETRY_FILTER, ["==", TYPE_MATCH_EXPR, typeId]];
|
||||
}
|
||||
|
||||
function widthStops(stops: ZoomStops): maplibregl.ExpressionSpecification {
|
||||
return ["interpolate", ["linear"], ["zoom"], 1, stops.z1, 4, stops.z4, 6, stops.z6];
|
||||
}
|
||||
@@ -20,12 +20,17 @@ export type CommitSnapshot = {
|
||||
// ---- GeoJSON / FeatureCollection ----
|
||||
|
||||
export type Geometry =
|
||||
| { type: "Point"; coordinates: [number, number] }
|
||||
| { type: "MultiPoint"; coordinates: [number, number][] }
|
||||
| { type: "LineString"; coordinates: [number, number][] }
|
||||
| { type: "MultiLineString"; coordinates: [number, number][][] }
|
||||
| { type: "Polygon"; coordinates: [number, number][][] }
|
||||
| { type: "MultiPolygon"; coordinates: [number, number][][][] };
|
||||
| ({ type: "Point"; coordinates: [number, number] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPoint"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "LineString"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiLineString"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "Polygon"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPolygon"; coordinates: [number, number][][][] } & CircleGeometryMetadata);
|
||||
|
||||
export type CircleGeometryMetadata = {
|
||||
circle_center?: [number, number];
|
||||
circle_radius?: number;
|
||||
};
|
||||
|
||||
export type FeatureId = string | number;
|
||||
|
||||
|
||||
+11
-6
@@ -1,12 +1,17 @@
|
||||
import type { GeometryPreset } from "@/uhm/lib/map/geo/geometryTypeOptions";
|
||||
|
||||
export type Geometry =
|
||||
| { type: "Point"; coordinates: [number, number] }
|
||||
| { type: "MultiPoint"; coordinates: [number, number][] }
|
||||
| { type: "LineString"; coordinates: [number, number][] }
|
||||
| { type: "MultiLineString"; coordinates: [number, number][][] }
|
||||
| { type: "Polygon"; coordinates: [number, number][][] }
|
||||
| { type: "MultiPolygon"; coordinates: [number, number][][][] };
|
||||
| ({ type: "Point"; coordinates: [number, number] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPoint"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "LineString"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiLineString"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "Polygon"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPolygon"; coordinates: [number, number][][][] } & CircleGeometryMetadata);
|
||||
|
||||
export type CircleGeometryMetadata = {
|
||||
circle_center?: [number, number];
|
||||
circle_radius?: number;
|
||||
};
|
||||
|
||||
export type FeatureId = string | number;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user