feat: add dynamic entity color generation and support for reassigning geometry IDs

This commit is contained in:
taDuc
2026-05-24 11:57:40 +07:00
parent 23b2c6f534
commit 0ebf8e1c65
10 changed files with 437 additions and 67 deletions
+55
View File
@@ -39,6 +39,7 @@ import {
mergeEntitySearchResults, mergeEntitySearchResults,
} from "@/uhm/lib/editor/entity/entityBinding"; } from "@/uhm/lib/editor/entity/entityBinding";
import { buildFeatureEntityPatch } from "@/uhm/lib/editor/entity/entityBinding"; import { buildFeatureEntityPatch } from "@/uhm/lib/editor/entity/entityBinding";
import { newId } from "@/uhm/lib/utils/id";
import { import {
loadBackgroundLayerVisibilityFromStorage, loadBackgroundLayerVisibilityFromStorage,
} from "@/uhm/lib/editor/background/backgroundVisibilityStorage"; } from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
@@ -1862,6 +1863,58 @@ function EditorPageContent() {
setEntityFormStatus, setEntityFormStatus,
}); });
const handleRerollGeometryId = useCallback((oldId: string | number) => {
const nextId = newId();
editor.changeFeatureId(oldId, nextId);
setSelectedFeatureIds((prev) => prev.map((id) => String(id) === String(oldId) ? nextId : id));
}, [editor, setSelectedFeatureIds]);
const handleRerollEntityId = useCallback((oldId: string, nextId: string) => {
const activeEntity = entities.find(e => e.id === oldId);
if (!activeEntity) return;
// 1. Update snapshotEntityRows
editor.setSnapshotEntityRows((prev) => prev.map((e) => {
if (e && String(e.id) === oldId) {
return { ...e, id: nextId };
}
return e;
}), `Reroll Entity ID #${oldId} -> #${nextId}`);
// 2. Update entityCatalog
setEntityCatalog((prev) => prev.map((e) => {
if (e && String(e.id) === oldId) {
return { ...e, id: nextId };
}
return e;
}));
// 3. Update selectedGeometryEntityIds
setSelectedGeometryEntityIds((prev) => prev.map((id) => id === oldId ? nextId : id));
// 4. Update features bound to this entity ID
const featuresToPatch = editor.draft.features.filter((feature) => {
const entityIds = feature.properties.entity_ids || [];
return feature.properties.entity_id === oldId || entityIds.includes(oldId);
});
if (featuresToPatch.length > 0) {
editor.patchFeaturePropertiesBatch(
featuresToPatch.map((feature) => {
const prevEntityIds = feature.properties.entity_ids || [];
const nextEntityIds = prevEntityIds.map((id) => id === oldId ? nextId : id);
return {
id: feature.properties.id,
patch: buildFeatureEntityPatch(feature, nextEntityIds, [
...entities.filter(e => e.id !== oldId),
{ id: nextId, name: activeEntity.name, time_start: activeEntity.time_start ?? null, time_end: activeEntity.time_end ?? null }
])
};
}),
"Cập nhật entity ID mới cho các GEO"
);
}
}, [editor, entities, setEntityCatalog, setSelectedGeometryEntityIds]);
// Tạo entity inline chỉ trong snapshot local, chưa gọi backend cho tới khi commit. // Tạo entity inline chỉ trong snapshot local, chưa gọi backend cho tới khi commit.
const handleCreateEntityOnly = async () => { const handleCreateEntityOnly = async () => {
const name = entityForm.name.trim(); const name = entityForm.name.trim();
@@ -2311,6 +2364,7 @@ function EditorPageContent() {
hasSelectedGeometry={Boolean(selectedFeature)} hasSelectedGeometry={Boolean(selectedFeature)}
selectedGeometryTime={selectedGeometryTime} selectedGeometryTime={selectedGeometryTime}
onToggleBindEntityForSelectedGeometry={handleToggleBindEntityForSelectedGeometry} onToggleBindEntityForSelectedGeometry={handleToggleBindEntityForSelectedGeometry}
onRerollEntityId={handleRerollEntityId}
/> />
<WikiSidebarPanel <WikiSidebarPanel
@@ -2333,6 +2387,7 @@ function EditorPageContent() {
onDeselectAll={() => setSelectedFeatureIds([])} onDeselectAll={() => setSelectedFeatureIds([])}
changeCount={editor.changeCount} changeCount={editor.changeCount}
onReplayEdit={(id) => setMode("replay", id)} onReplayEdit={(id) => setMode("replay", id)}
onRerollGeometryId={handleRerollGeometryId}
/> />
) : null} ) : null}
</div> </div>
+45
View File
@@ -291,3 +291,48 @@
cursor: not-allowed !important; cursor: not-allowed !important;
pointer-events: none !important; pointer-events: none !important;
} }
.numberWrapper {
display: flex;
align-items: center;
gap: 6px;
}
.adjustGroup {
display: flex;
align-items: center;
gap: 4px;
}
.adjustBtn {
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
cursor: pointer;
font-size: 14px;
font-weight: 600;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
outline: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.adjustBtn:hover {
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.15);
}
.adjustBtn:active {
background: rgba(16, 185, 129, 0.25);
border-color: #10b981;
}
.adjustBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@@ -5,6 +5,7 @@ import type { EntitySnapshot } from "@/uhm/types/entities";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import NewBadge from "@/uhm/components/editor/NewBadge"; import NewBadge from "@/uhm/components/editor/NewBadge";
import { useEditorStore } from "@/uhm/store/editorStore"; import { useEditorStore } from "@/uhm/store/editorStore";
import { newId } from "@/uhm/lib/utils/id";
type Props = { type Props = {
onCreateEntityOnly: () => void; onCreateEntityOnly: () => void;
@@ -12,6 +13,7 @@ type Props = {
hasSelectedGeometry?: boolean; hasSelectedGeometry?: boolean;
selectedGeometryTime?: { time_start: number | null; time_end: number | null } | null; selectedGeometryTime?: { time_start: number | null; time_end: number | null } | null;
onToggleBindEntityForSelectedGeometry?: (entityId: string, nextChecked: boolean) => void; onToggleBindEntityForSelectedGeometry?: (entityId: string, nextChecked: boolean) => void;
onRerollEntityId?: (oldId: string, nextId: string) => void;
}; };
export default function ProjectEntityRefsPanel({ export default function ProjectEntityRefsPanel({
@@ -20,6 +22,7 @@ export default function ProjectEntityRefsPanel({
hasSelectedGeometry, hasSelectedGeometry,
selectedGeometryTime, selectedGeometryTime,
onToggleBindEntityForSelectedGeometry, onToggleBindEntityForSelectedGeometry,
onRerollEntityId,
}: Props) { }: Props) {
const { const {
snapshotEntityRows, snapshotEntityRows,
@@ -282,8 +285,35 @@ export default function ProjectEntityRefsPanel({
</div> </div>
</div> </div>
<div style={{ fontSize: 11, color: "#94a3b8", overflowWrap: "anywhere" }}> <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
{String(activeEntity.id)} <div style={{ fontSize: 11, color: "#94a3b8", overflowWrap: "anywhere", minWidth: 0, flex: 1 }}>
{String(activeEntity.id)}
</div>
{activeEntity.source === "inline" && onRerollEntityId && (
<button
type="button"
onClick={() => {
const nextId = newId();
onRerollEntityId(String(activeEntity.id), nextId);
setActiveEntityId(nextId);
}}
disabled={isEntitySubmitting}
title="Đổi mã ID để sinh ngẫu nhiên màu sắc mới cho thực thể này"
style={{
border: "1px solid #0f766e",
borderRadius: "4px",
padding: "2px 6px",
background: "transparent",
color: "#14b8a6",
fontSize: "11px",
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
whiteSpace: "nowrap",
flex: "0 0 auto",
}}
>
Đi màu (Reroll ID)
</button>
)}
</div> </div>
<input <input
value={editName} value={editName}
@@ -20,6 +20,7 @@ type Props = {
onReplayEdit?: (id: string | number) => void; onReplayEdit?: (id: string | number) => void;
onDeleteFeatures?: (ids: (string | number)[]) => void; onDeleteFeatures?: (ids: (string | number)[]) => void;
onDeselectAll?: () => void; onDeselectAll?: () => void;
onRerollGeometryId?: (oldId: string | number) => void;
}; };
export default function SelectedGeometryPanel({ export default function SelectedGeometryPanel({
@@ -29,6 +30,7 @@ export default function SelectedGeometryPanel({
onReplayEdit, onReplayEdit,
onDeleteFeatures, onDeleteFeatures,
onDeselectAll, onDeselectAll,
onRerollGeometryId,
}: Props) { }: Props) {
const { const {
geometryMetaForm, geometryMetaForm,
@@ -221,8 +223,30 @@ export default function SelectedGeometryPanel({
<div style={{ color: "#e2e8f0", fontWeight: 700, fontSize: "12px" }}> <div style={{ color: "#e2e8f0", fontWeight: 700, fontSize: "12px" }}>
Thuộc tính GEO Thuộc tính GEO
</div> </div>
<div style={{ color: "#94a3b8", fontSize: "11px" }}> <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }}>
Các giá trị này thuộc về GEO đang chọn, không phụ thuộc entity. <div style={{ color: "#94a3b8", fontSize: "11px", overflowWrap: "anywhere", minWidth: 0, flex: 1 }}>
{isBulkMode ? `Đang chọn ${selectedFeatures.length} geometries` : `ID: ${representativeFeature.properties.id}`}
</div>
{!isBulkMode && onRerollGeometryId && (
<button
type="button"
onClick={() => onRerollGeometryId(representativeFeature.properties.id)}
title="Đổi mã ID để sinh ngẫu nhiên màu sắc mới cho hình học này"
style={{
border: "1px solid #0f766e",
borderRadius: "4px",
padding: "2px 6px",
background: "transparent",
color: "#14b8a6",
fontSize: "11px",
cursor: "pointer",
whiteSpace: "nowrap",
flex: "0 0 auto",
}}
>
Đi màu (Reroll ID)
</button>
)}
</div> </div>
{!isMultiEditValid ? ( {!isMultiEditValid ? (
+53
View File
@@ -940,3 +940,56 @@ export function clampNumber(value: number, min: number, max: number): number {
if (value > max) return max; if (value > max) return max;
return value; return value;
} }
export function hashStringToColor(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 50%)`;
}
export function decorateFeaturesWithEntityColors(fc: FeatureCollection): FeatureCollection {
return {
...fc,
features: fc.features.map((feature) => {
const geomType = feature.geometry?.type;
if (geomType === "Point" || geomType === "MultiPoint") {
// Point - giữ nguyên màu của preset/icon
return feature;
}
if (geomType === "LineString" || geomType === "MultiLineString") {
const entityIds = getFeatureEntityIds(feature);
if (entityIds.length > 0) {
const sortedCombined = [...entityIds].sort().join("+");
return {
...feature,
properties: {
...feature.properties,
entity_color: hashStringToColor(sortedCombined),
},
};
}
return feature;
}
if (geomType === "Polygon" || geomType === "MultiPolygon") {
const geoId = String(feature.properties?.id || "");
if (geoId) {
return {
...feature,
properties: {
...feature.properties,
entity_color: hashStringToColor(geoId),
},
};
}
return feature;
}
return feature;
}),
};
}
+3 -1
View File
@@ -15,6 +15,7 @@ import {
fitMapToFeatureCollection, fitMapToFeatureCollection,
setSelectedFeatureState, setSelectedFeatureState,
splitDraftFeatures, splitDraftFeatures,
decorateFeaturesWithEntityColors,
} from "./mapUtils"; } from "./mapUtils";
import { applyImageOverlay, type MapImageOverlay } from "./imageOverlay"; import { applyImageOverlay, type MapImageOverlay } from "./imageOverlay";
@@ -128,7 +129,8 @@ export function useMapSync({
const bindingFilteredRenderDraft = applyGeometryBindingFilterRef.current const bindingFilteredRenderDraft = applyGeometryBindingFilterRef.current
? filterDraftByBinding(renderFc, currentSelectedIds, highlightFeaturesVal) ? filterDraftByBinding(renderFc, currentSelectedIds, highlightFeaturesVal)
: renderFc; : renderFc;
const mapSourceDraft = filterDraftByGeometryVisibility(bindingFilteredRenderDraft, geometryVisibilityRef.current); const visibilityFilteredDraft = filterDraftByGeometryVisibility(bindingFilteredRenderDraft, geometryVisibilityRef.current);
const mapSourceDraft = decorateFeaturesWithEntityColors(visibilityFilteredDraft);
const labelTimelineYear = labelTimelineYearRef.current; const labelTimelineYear = labelTimelineYearRef.current;
const { polygons, points } = splitDraftFeatures(mapSourceDraft); const { polygons, points } = splitDraftFeatures(mapSourceDraft);
const labeledGeometries = decorateLineFeaturesWithLabels(polygons, labelContext, labelTimelineYear); const labeledGeometries = decorateLineFeaturesWithLabels(polygons, labelContext, labelTimelineYear);
+139 -17
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } from "@/uhm/lib/utils/timeline"; import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } from "@/uhm/lib/utils/timeline";
import styles from "@/styles/TimelineBar.module.css"; import styles from "@/styles/TimelineBar.module.css";
@@ -33,14 +34,95 @@ export default function TimelineBar({
const effectiveDisabled = disabled; const effectiveDisabled = disabled;
const safeYear = clampYearValue(year, lower, upper); const safeYear = clampYearValue(year, lower, upper);
const [localYear, setLocalYear] = useState(safeYear);
// Đồng bộ prop year với localYear khi prop year thay đổi từ bên ngoài
useEffect(() => {
setLocalYear(safeYear);
}, [safeYear]);
const localYearRef = useRef(localYear);
localYearRef.current = localYear;
const onYearChangeRef = useRef(onYearChange);
onYearChangeRef.current = onYearChange;
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastTriggeredYearRef = useRef<number | null>(null);
const lastTriggerTimeRef = useRef<number>(0);
const commitYearChange = useCallback((nextVal: number) => {
if (nextVal === lastTriggeredYearRef.current) return;
lastTriggeredYearRef.current = nextVal;
lastTriggerTimeRef.current = Date.now();
onYearChangeRef.current(nextVal);
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
}, []);
const handleLocalYearChange = useCallback((nextVal: number) => {
const clamped = clampYearValue(Math.trunc(nextVal), lower, upper);
setLocalYear(clamped);
const now = Date.now();
if (now - lastTriggerTimeRef.current >= 1000) {
commitYearChange(clamped);
} else {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
commitYearChange(clamped);
}, 1000);
}
}, [lower, upper, commitYearChange]);
const startChangingYear = (direction: number) => {
if (effectiveDisabled) return;
const nextVal = localYearRef.current + direction;
handleLocalYearChange(nextVal);
timeoutRef.current = setTimeout(() => {
intervalRef.current = setInterval(() => {
const currentVal = localYearRef.current;
const targetVal = currentVal + direction;
if (targetVal < lower || targetVal > upper) {
stopChangingYear();
return;
}
handleLocalYearChange(targetVal);
}, 80);
}, 400);
};
const stopChangingYear = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
commitYearChange(localYearRef.current);
};
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
if (intervalRef.current) clearInterval(intervalRef.current);
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
};
}, []);
const helperText = isLoading const helperText = isLoading
? "Đang tải geometry theo mốc thời gian..." ? "Đang tải geometry theo mốc thời gian..."
: statusText || null; : statusText || null;
const handleYearChange = (nextYear: number) => {
onYearChange(clampYearValue(Math.trunc(nextYear), lower, upper));
};
const handleTimeRangeChange = (nextValue: number) => { const handleTimeRangeChange = (nextValue: number) => {
if (!onTimeRangeChange) return; if (!onTimeRangeChange) return;
const safe = Number.isFinite(nextValue) ? Math.trunc(nextValue) : 0; const safe = Number.isFinite(nextValue) ? Math.trunc(nextValue) : 0;
@@ -81,8 +163,10 @@ export default function TimelineBar({
min={lower} min={lower}
max={upper} max={upper}
step={1} step={1}
value={safeYear} value={localYear}
onChange={(event) => handleYearChange(Number(event.target.value))} onChange={(event) => handleLocalYearChange(Number(event.target.value))}
onMouseUp={() => commitYearChange(localYearRef.current)}
onTouchEnd={() => commitYearChange(localYearRef.current)}
disabled={effectiveDisabled} disabled={effectiveDisabled}
className={styles.slider} className={styles.slider}
aria-label="Timeline year" aria-label="Timeline year"
@@ -90,17 +174,55 @@ export default function TimelineBar({
<span className={styles.labelBoundsRight}> <span className={styles.labelBoundsRight}>
{formatYear(upper)} {formatYear(upper)}
</span> </span>
<input <div className={styles.numberWrapper}>
type="number" <input
min={lower} type="number"
max={upper} min={lower}
step={1} max={upper}
value={safeYear} step={1}
onChange={(event) => handleYearChange(Number(event.target.value))} value={localYear}
disabled={effectiveDisabled} onChange={(event) => handleLocalYearChange(Number(event.target.value))}
className={styles.numberInput} onBlur={() => commitYearChange(localYearRef.current)}
aria-label="Timeline exact year" onKeyDown={(e) => {
/> if (e.key === "Enter") {
commitYearChange(localYearRef.current);
}
}}
disabled={effectiveDisabled}
className={styles.numberInput}
aria-label="Timeline exact year"
/>
<div className={styles.adjustGroup}>
<button
type="button"
onMouseDown={() => startChangingYear(-1)}
onMouseUp={stopChangingYear}
onMouseLeave={stopChangingYear}
onTouchStart={() => startChangingYear(-1)}
onTouchEnd={stopChangingYear}
disabled={effectiveDisabled}
className={styles.adjustBtn}
title="Giảm 1 năm"
aria-label="Giảm 1 năm"
>
-
</button>
<button
type="button"
onMouseDown={() => startChangingYear(1)}
onMouseUp={stopChangingYear}
onMouseLeave={stopChangingYear}
onTouchStart={() => startChangingYear(1)}
onTouchEnd={stopChangingYear}
disabled={effectiveDisabled}
className={styles.adjustBtn}
title="Tăng 1 năm"
aria-label="Tăng 1 năm"
>
+
</button>
</div>
</div>
{typeof timeRange === "number" && onTimeRangeChange ? ( {typeof timeRange === "number" && onTimeRangeChange ? (
<label <label
title="time_range (0-30)" title="time_range (0-30)"
@@ -14,6 +14,7 @@ import type { WikiSnapshot } from "@/uhm/types/wiki";
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects"; import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants"; import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes"; import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
import { newId } from "@/uhm/lib/utils/id";
export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo"; export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo";
export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes"; export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
@@ -599,6 +600,55 @@ export function useEditorState(
commitMainDraft({ ...mainDraftRef.current, features: nextFeatures }); commitMainDraft({ ...mainDraftRef.current, features: nextFeatures });
} }
function changeFeatureId(oldId: FeatureProperties["id"], newId: FeatureProperties["id"]) {
if (mode === "replay") {
return;
}
const idx = mainDraftRef.current.features.findIndex((feature) => featureIdEquals(feature.properties.id, oldId));
if (idx === -1) return;
const nextFeatures = [...mainDraftRef.current.features];
const prevFeature = nextFeatures[idx];
const newFeature = {
...prevFeature,
id: newId,
properties: {
...prevFeature.properties,
id: newId,
},
};
nextFeatures[idx] = newFeature;
// Cập nhật replays nếu có
const prevReplays = replaysRef.current || [];
const nextReplays = prevReplays.map((replay) => {
if (replay.geometry_id === String(oldId)) {
return {
...deepClone(replay),
id: String(newId),
geometry_id: String(newId),
};
}
return replay;
});
if (JSON.stringify(prevReplays) !== JSON.stringify(nextReplays)) {
updateReplaysState(nextReplays);
}
pushMainUndo({
type: "group",
label: `Đổi ID GEO #${oldId} -> #${newId}`,
actions: [
{ type: "replays", label: "Phục hồi replay cũ", prevReplays: deepClone(prevReplays) },
{ type: "create", id: newId },
{ type: "delete", feature: deepClone(prevFeature), index: idx },
],
});
commitMainDraft({ ...mainDraftRef.current, features: nextFeatures });
}
function pruneReplaysForDeletedGeometryIds( function pruneReplaysForDeletedGeometryIds(
ids: Array<FeatureProperties["id"]>, ids: Array<FeatureProperties["id"]>,
label: string label: string
@@ -822,6 +872,7 @@ export function useEditorState(
updateFeature, updateFeature,
deleteFeature, deleteFeature,
deleteFeatures, deleteFeatures,
changeFeatureId,
undo, undo,
buildPayload, buildPayload,
clearChanges, clearChanges,
+25 -41
View File
@@ -20,6 +20,7 @@ export const POINT_GEOTYPE_ICON_PATHS: Partial<Record<PointGeotypeId, string>> =
city: "/images/mapIcon/point/city.png", city: "/images/mapIcon/point/city.png",
fortification: "/images/mapIcon/point/castle.png", fortification: "/images/mapIcon/point/castle.png",
ruin: "/images/mapIcon/point/ruin.png", ruin: "/images/mapIcon/point/ruin.png",
port: "/images/mapIcon/point/port.png",
}; };
type PointIconVariant = "default" | "draft"; type PointIconVariant = "default" | "draft";
@@ -442,50 +443,33 @@ function drawRuinGlyph(ctx: CanvasRenderingContext2D) {
} }
function drawAnchorGlyph(ctx: CanvasRenderingContext2D) { function drawAnchorGlyph(ctx: CanvasRenderingContext2D) {
ctx.lineWidth = 3; const img = preloadedImages["port"];
ctx.beginPath(); if (img && loadedImageKeys.has("port")) {
ctx.arc(32, 22.5, 3.5, 0, Math.PI * 2); ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
ctx.stroke(); } else {
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(32, 22.5, 3.5, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(32, 26.5); ctx.moveTo(32, 26.5);
ctx.lineTo(32, 41); ctx.lineTo(32, 41);
ctx.moveTo(24, 31.5); ctx.moveTo(24, 31.5);
ctx.lineTo(40, 31.5); ctx.lineTo(40, 31.5);
ctx.stroke(); ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.arc(32, 35.5, 9, 0.2 * Math.PI, 0.8 * Math.PI); ctx.arc(32, 35.5, 9, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.stroke(); ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(24.5, 38); ctx.moveTo(24.5, 38);
ctx.lineTo(21.5, 34); ctx.lineTo(21.5, 34);
ctx.moveTo(39.5, 38); ctx.moveTo(39.5, 38);
ctx.lineTo(42.5, 34); ctx.lineTo(42.5, 34);
ctx.stroke(); 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();
} }
@@ -169,7 +169,7 @@ function statusColor(normalColor: string): maplibregl.ExpressionSpecification {
"case", "case",
SELECTED_EXPR, SELECTED_EXPR,
SELECTED_COLOR, SELECTED_COLOR,
normalColor, ["coalesce", ["get", "entity_color"], normalColor],
]; ];
} }
@@ -178,12 +178,16 @@ function statusStroke(normalColor: string): maplibregl.ExpressionSpecification {
"case", "case",
SELECTED_EXPR, SELECTED_EXPR,
SELECTED_COLOR, SELECTED_COLOR,
normalColor, ["coalesce", ["get", "entity_color"], normalColor],
]; ];
} }
function statusFillColor(normalColor: string): string { function statusFillColor(normalColor: string): maplibregl.ExpressionSpecification {
return normalColor; return [
"coalesce",
["get", "entity_color"],
normalColor,
];
} }
function lineFilter(typeId: string): maplibregl.ExpressionSpecification { function lineFilter(typeId: string): maplibregl.ExpressionSpecification {