feat: enable feature selection for preview modes and add click interaction support to the selecting engine
This commit is contained in:
+886
-36
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import Map, { type MapHoverPayload } from "@/uhm/components/Map";
|
import Map, { type MapFeaturePayload } from "@/uhm/components/Map";
|
||||||
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
||||||
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
||||||
import mapLayersStyles from "@/styles/MapLayers.module.css";
|
import mapLayersStyles from "@/styles/MapLayers.module.css";
|
||||||
@@ -82,7 +82,7 @@ export default function Page() {
|
|||||||
completed: 0,
|
completed: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const [hoverAnchor, setHoverAnchor] = useState<MapHoverPayload | null>(null);
|
const [hoverAnchor, setHoverAnchor] = useState<MapFeaturePayload | null>(null);
|
||||||
const [isMapLayersCollapsed, setIsMapLayersCollapsed] = useState(false);
|
const [isMapLayersCollapsed, setIsMapLayersCollapsed] = useState(false);
|
||||||
const [activeEntityId, setActiveEntityId] = useState<string | null>(null);
|
const [activeEntityId, setActiveEntityId] = useState<string | null>(null);
|
||||||
const [activeWikiSlug, setActiveWikiSlug] = useState<string | null>(null);
|
const [activeWikiSlug, setActiveWikiSlug] = useState<string | null>(null);
|
||||||
@@ -369,7 +369,7 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
}, [activeEntityId, relations.geometryEntityIds, selectEntity, selectedFeatureIds]);
|
}, [activeEntityId, relations.geometryEntityIds, selectEntity, selectedFeatureIds]);
|
||||||
|
|
||||||
const handleMapHoverChange = useCallback((payload: MapHoverPayload | null) => {
|
const handleMapHoverChange = useCallback((payload: MapFeaturePayload | null) => {
|
||||||
clearHoverHideTimer();
|
clearHoverHideTimer();
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
@@ -529,8 +529,8 @@ export default function Page() {
|
|||||||
geometryVisibility={geometryVisibility}
|
geometryVisibility={geometryVisibility}
|
||||||
allowGeometryEditing={false}
|
allowGeometryEditing={false}
|
||||||
applyGeometryBindingFilter={true}
|
applyGeometryBindingFilter={true}
|
||||||
onHoverFeatureChange={handleMapHoverChange}
|
onFeatureClick={handleMapHoverChange}
|
||||||
highlightFeatures={activeEntityGeometries}
|
|
||||||
focusFeatureCollection={activeEntityGeometries}
|
focusFeatureCollection={activeEntityGeometries}
|
||||||
focusRequestKey={entityFocusToken}
|
focusRequestKey={entityFocusToken}
|
||||||
focusPadding={activeEntityId && isLargeScreen ? { top: 84, right: sidebarWidth + 80, bottom: 116, left: 84 } : { top: 84, right: 84, bottom: 116, left: 84 }}
|
focusPadding={activeEntityId && isLargeScreen ? { top: 84, right: sidebarWidth + 80, bottom: 116, left: 84 } : { top: 84, right: 84, bottom: 116, left: 84 }}
|
||||||
|
|||||||
+74
-13
@@ -13,7 +13,7 @@ import { useMapInteraction } from "./map/useMapInteraction";
|
|||||||
import { useMapSync } from "./map/useMapSync";
|
import { useMapSync } from "./map/useMapSync";
|
||||||
import { bindImageOverlayInteractions, type MapImageOverlay } from "./map/imageOverlay";
|
import { bindImageOverlayInteractions, type MapImageOverlay } from "./map/imageOverlay";
|
||||||
|
|
||||||
export type MapHoverPayload = {
|
export type MapFeaturePayload = {
|
||||||
featureId: string | number;
|
featureId: string | number;
|
||||||
feature: Feature | null;
|
feature: Feature | null;
|
||||||
point: { x: number; y: number };
|
point: { x: number; y: number };
|
||||||
@@ -29,6 +29,7 @@ export type MapHandle = {
|
|||||||
projection: string;
|
projection: string;
|
||||||
} | null;
|
} | null;
|
||||||
getMap: () => import("maplibre-gl").Map | null;
|
getMap: () => import("maplibre-gl").Map | null;
|
||||||
|
setGlobeProjection: (isGlobe: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MapProps = {
|
type MapProps = {
|
||||||
@@ -53,8 +54,7 @@ type MapProps = {
|
|||||||
height?: CSSProperties["height"];
|
height?: CSSProperties["height"];
|
||||||
fitToDraftBounds?: boolean;
|
fitToDraftBounds?: boolean;
|
||||||
fitBoundsKey?: string | number | null;
|
fitBoundsKey?: string | number | null;
|
||||||
onHoverFeatureChange?: ((payload: MapHoverPayload | null) => void) | undefined;
|
onFeatureClick?: ((payload: MapFeaturePayload | null) => void) | undefined;
|
||||||
highlightFeatures?: FeatureCollection | null;
|
|
||||||
focusFeatureCollection?: FeatureCollection | null;
|
focusFeatureCollection?: FeatureCollection | null;
|
||||||
focusRequestKey?: string | number | null;
|
focusRequestKey?: string | number | null;
|
||||||
focusPadding?: number | import("maplibre-gl").PaddingOptions;
|
focusPadding?: number | import("maplibre-gl").PaddingOptions;
|
||||||
@@ -62,6 +62,10 @@ type MapProps = {
|
|||||||
onImageOverlayChange?: (overlay: MapImageOverlay) => void;
|
onImageOverlayChange?: (overlay: MapImageOverlay) => void;
|
||||||
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void;
|
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void;
|
||||||
showViewportControls?: boolean;
|
showViewportControls?: boolean;
|
||||||
|
isPreviewMode?: boolean;
|
||||||
|
onEnterPreview?: () => void;
|
||||||
|
onExitPreview?: () => void;
|
||||||
|
onPlayPreviewReplay?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Map = forwardRef<MapHandle, MapProps>(function Map({
|
const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||||
@@ -83,8 +87,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
height = "100vh",
|
height = "100vh",
|
||||||
fitToDraftBounds = false,
|
fitToDraftBounds = false,
|
||||||
fitBoundsKey = null,
|
fitBoundsKey = null,
|
||||||
onHoverFeatureChange,
|
onFeatureClick,
|
||||||
highlightFeatures = null,
|
|
||||||
focusFeatureCollection = null,
|
focusFeatureCollection = null,
|
||||||
focusRequestKey = null,
|
focusRequestKey = null,
|
||||||
focusPadding,
|
focusPadding,
|
||||||
@@ -92,6 +95,10 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onImageOverlayChange,
|
onImageOverlayChange,
|
||||||
onBindGeometries,
|
onBindGeometries,
|
||||||
showViewportControls = true,
|
showViewportControls = true,
|
||||||
|
isPreviewMode = false,
|
||||||
|
onEnterPreview,
|
||||||
|
onExitPreview,
|
||||||
|
onPlayPreviewReplay,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
// Ref giữ mode mới nhất cho MapLibre handlers được register một lần.
|
// Ref giữ mode mới nhất cho MapLibre handlers được register một lần.
|
||||||
const modeRef = useRef<MapProps["mode"]>(mode);
|
const modeRef = useRef<MapProps["mode"]>(mode);
|
||||||
@@ -101,8 +108,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
const onSelectFeatureIdsRef = useRef(onSelectFeatureIds);
|
const onSelectFeatureIdsRef = useRef(onSelectFeatureIds);
|
||||||
// Ref callback đổi mode mới nhất, dùng khi map interaction chuyển sang replay/select.
|
// Ref callback đổi mode mới nhất, dùng khi map interaction chuyển sang replay/select.
|
||||||
const onSetModeRef = useRef(onSetMode);
|
const onSetModeRef = useRef(onSetMode);
|
||||||
// Ref callback hover mới nhất cho tooltip/panel ngoài map.
|
// Ref callback click feature mới nhất cho tooltip/panel ngoài map.
|
||||||
const onHoverFeatureChangeRef = useRef<MapProps["onHoverFeatureChange"]>(onHoverFeatureChange);
|
const onFeatureClickRef = useRef<MapProps["onFeatureClick"]>(onFeatureClick);
|
||||||
// Ref callback create mới nhất khi drawing engine tạo feature.
|
// Ref callback create mới nhất khi drawing engine tạo feature.
|
||||||
const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature);
|
const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature);
|
||||||
// Ref callback delete mới nhất khi editing engine xóa feature.
|
// Ref callback delete mới nhất khi editing engine xóa feature.
|
||||||
@@ -122,7 +129,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
useEffect(() => { renderDraftRef.current = renderDraft; }, [renderDraft]);
|
useEffect(() => { renderDraftRef.current = renderDraft; }, [renderDraft]);
|
||||||
useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]);
|
useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]);
|
||||||
useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]);
|
useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]);
|
||||||
useEffect(() => { onHoverFeatureChangeRef.current = onHoverFeatureChange; }, [onHoverFeatureChange]);
|
useEffect(() => { onFeatureClickRef.current = onFeatureClick; }, [onFeatureClick]);
|
||||||
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
|
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
|
||||||
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
|
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
|
||||||
useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]);
|
useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]);
|
||||||
@@ -153,7 +160,10 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
getViewState,
|
getViewState,
|
||||||
getMap: () => mapRef.current,
|
getMap: () => mapRef.current,
|
||||||
}), [getViewState, mapRef]);
|
setGlobeProjection: (isGlobe: boolean) => {
|
||||||
|
setIsGlobeProjection(isGlobe);
|
||||||
|
},
|
||||||
|
}), [getViewState, mapRef, setIsGlobeProjection]);
|
||||||
|
|
||||||
// Hook gắn/dọn các interaction vẽ, chọn, sửa geometry.
|
// Hook gắn/dọn các interaction vẽ, chọn, sửa geometry.
|
||||||
const {
|
const {
|
||||||
@@ -173,14 +183,13 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onDeleteRef,
|
onDeleteRef,
|
||||||
onHideRef,
|
onHideRef,
|
||||||
onUpdateRef,
|
onUpdateRef,
|
||||||
onHoverFeatureChangeRef,
|
onFeatureClickRef,
|
||||||
onBindGeometriesRef,
|
onBindGeometriesRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hook đồng bộ draft/layer/filter/highlight từ React state xuống MapLibre source/layer.
|
// Hook đồng bộ draft/layer/filter/highlight từ React state xuống MapLibre source/layer.
|
||||||
const {
|
const {
|
||||||
applyRenderDraftToMap,
|
applyRenderDraftToMap,
|
||||||
applyHighlightToMap,
|
|
||||||
applyImageOverlayToMap,
|
applyImageOverlayToMap,
|
||||||
tryCenterToUserLocation,
|
tryCenterToUserLocation,
|
||||||
} = useMapSync({
|
} = useMapSync({
|
||||||
@@ -194,7 +203,6 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
applyGeometryBindingFilter,
|
applyGeometryBindingFilter,
|
||||||
fitToDraftBounds,
|
fitToDraftBounds,
|
||||||
fitBoundsKey,
|
fitBoundsKey,
|
||||||
highlightFeatures,
|
|
||||||
focusFeatureCollection,
|
focusFeatureCollection,
|
||||||
focusRequestKey,
|
focusRequestKey,
|
||||||
focusPadding,
|
focusPadding,
|
||||||
@@ -208,7 +216,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map || !isMapLoaded) return;
|
if (!map || !isMapLoaded) return;
|
||||||
|
|
||||||
setupMapLayers(map, backgroundVisibility, highlightFeatures, applyHighlightToMap);
|
setupMapLayers(map, backgroundVisibility);
|
||||||
applyImageOverlayToMap();
|
applyImageOverlayToMap();
|
||||||
setupMapInteractions(map);
|
setupMapInteractions(map);
|
||||||
applyRenderDraftToMap(renderDraftRef.current);
|
applyRenderDraftToMap(renderDraftRef.current);
|
||||||
@@ -365,6 +373,59 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{onEnterPreview || onExitPreview ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={isPreviewMode ? onExitPreview : onEnterPreview}
|
||||||
|
style={{
|
||||||
|
...zoomButtonStyle,
|
||||||
|
width: "auto",
|
||||||
|
minWidth: "76px",
|
||||||
|
padding: "0 12px",
|
||||||
|
background: isPreviewMode ? "#334155" : "#166534",
|
||||||
|
fontWeight: 800,
|
||||||
|
}}
|
||||||
|
aria-label={isPreviewMode ? "Exit preview" : "Enter preview"}
|
||||||
|
title={isPreviewMode ? "Thoat preview" : "Xem nhu nguoi dung"}
|
||||||
|
>
|
||||||
|
{isPreviewMode ? "Editor" : "Preview"}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{onPlayPreviewReplay ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPlayPreviewReplay}
|
||||||
|
style={{
|
||||||
|
...zoomButtonStyle,
|
||||||
|
width: "auto",
|
||||||
|
minWidth: "64px",
|
||||||
|
padding: "0 12px",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "7px",
|
||||||
|
background: "#2563eb",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 800,
|
||||||
|
}}
|
||||||
|
aria-label="Play selected replay"
|
||||||
|
title="Play replay của geometry đang chọn"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderTop: "5px solid transparent",
|
||||||
|
borderBottom: "5px solid transparent",
|
||||||
|
borderLeft: "8px solid currentColor",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleZoomByStep(-0.8)}
|
onClick={() => handleZoomByStep(-0.8)}
|
||||||
|
|||||||
@@ -194,41 +194,21 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderTooltipStyles = () => (
|
const renderStyles = () => (
|
||||||
<style dangerouslySetInnerHTML={{ __html: `
|
<style dangerouslySetInnerHTML={{ __html: `
|
||||||
.${buttonClassName} {
|
.replay-preview-layer-panel::-webkit-scrollbar {
|
||||||
position: relative;
|
display: none;
|
||||||
}
|
}
|
||||||
.${buttonClassName}::after {
|
.replay-preview-layer-panel {
|
||||||
content: attr(data-tooltip);
|
scrollbar-width: none;
|
||||||
position: absolute;
|
-ms-overflow-style: none;
|
||||||
left: 100%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%) scale(0.9);
|
|
||||||
margin-left: 10px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: rgba(15, 23, 42, 0.95);
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
||||||
color: #f8fafc;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 10px 25px rgba(2, 6, 23, 0.5);
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
.${buttonClassName}:hover::after {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-50%) scale(1);
|
|
||||||
}
|
}
|
||||||
`}} />
|
`}} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="replay-preview-layer-panel"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -237,15 +217,16 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
border: "1px solid rgba(148, 163, 184, 0.22)",
|
border: "1px solid rgba(148, 163, 184, 0.22)",
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
padding: "14px 10px",
|
padding: "14px 10px",
|
||||||
width: 100,
|
width: 58,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.45)",
|
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.45)",
|
||||||
backdropFilter: "blur(12px)",
|
backdropFilter: "blur(12px)",
|
||||||
maxHeight: "calc(100vh - 180px)",
|
maxHeight: "calc(100vh - 180px)",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderTooltipStyles()}
|
{renderStyles()}
|
||||||
|
|
||||||
{/* Background layers */}
|
{/* Background layers */}
|
||||||
<div style={groupHeaderStyle}>Map</div>
|
<div style={groupHeaderStyle}>Map</div>
|
||||||
@@ -256,8 +237,7 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<button
|
<button
|
||||||
key={layer.id}
|
key={layer.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={buttonClassName}
|
title={layer.label}
|
||||||
data-tooltip={layer.label}
|
|
||||||
onClick={() => onToggleBackground(layer.id)}
|
onClick={() => onToggleBackground(layer.id)}
|
||||||
style={getButtonStyles(active, "56, 189, 248")} // sky-400
|
style={getButtonStyles(active, "56, 189, 248")} // sky-400
|
||||||
>
|
>
|
||||||
@@ -279,8 +259,7 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
type="button"
|
type="button"
|
||||||
className={buttonClassName}
|
title={label}
|
||||||
data-tooltip={label}
|
|
||||||
onClick={() => onToggleGeometry(typeKey)}
|
onClick={() => onToggleGeometry(typeKey)}
|
||||||
style={getButtonStyles(active, "249, 115, 22")} // orange-500
|
style={getButtonStyles(active, "249, 115, 22")} // orange-500
|
||||||
>
|
>
|
||||||
@@ -302,8 +281,7 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
type="button"
|
type="button"
|
||||||
className={buttonClassName}
|
title={label}
|
||||||
data-tooltip={label}
|
|
||||||
onClick={() => onToggleGeometry(typeKey)}
|
onClick={() => onToggleGeometry(typeKey)}
|
||||||
style={getButtonStyles(active, "192, 132, 252")} // purple-400
|
style={getButtonStyles(active, "192, 132, 252")} // purple-400
|
||||||
>
|
>
|
||||||
@@ -325,8 +303,7 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
type="button"
|
type="button"
|
||||||
className={buttonClassName}
|
title={label}
|
||||||
data-tooltip={label}
|
|
||||||
onClick={() => onToggleGeometry(typeKey)}
|
onClick={() => onToggleGeometry(typeKey)}
|
||||||
style={getButtonStyles(active, "245, 158, 11")} // amber-500
|
style={getButtonStyles(active, "245, 158, 11")} // amber-500
|
||||||
>
|
>
|
||||||
@@ -352,7 +329,7 @@ const groupHeaderStyle: React.CSSProperties = {
|
|||||||
|
|
||||||
const gridStyle: React.CSSProperties = {
|
const gridStyle: React.CSSProperties = {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
gridTemplateColumns: "1fr",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,18 +150,16 @@ export function filterDraftByBinding(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const childIds = new Set<string>();
|
const childIds = new Set<string>();
|
||||||
const selectedChildren = new Set<string>();
|
const parentIds = new Set<string>();
|
||||||
const selectedParents = new Set<string>();
|
const featureParentMap = new Map<string, string>(); // childId -> parentId
|
||||||
|
|
||||||
for (const feature of fc.features) {
|
for (const feature of fc.features) {
|
||||||
const featureId = String(feature.properties.id);
|
const featureId = String(feature.properties.id);
|
||||||
const parentId = normalizeFeatureBoundWith(feature);
|
const parentId = normalizeFeatureBoundWith(feature);
|
||||||
if (!parentId) continue;
|
if (parentId) {
|
||||||
childIds.add(featureId);
|
childIds.add(featureId);
|
||||||
if (selectedIds.has(parentId)) {
|
parentIds.add(parentId);
|
||||||
selectedChildren.add(featureId);
|
featureParentMap.set(featureId, parentId);
|
||||||
}
|
|
||||||
if (selectedIds.has(featureId)) {
|
|
||||||
selectedParents.add(parentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +167,35 @@ export function filterDraftByBinding(
|
|||||||
return { ...fc, features: fc.features.filter((f) => !childIds.has(String(f.properties.id))) };
|
return { ...fc, features: fc.features.filter((f) => !childIds.has(String(f.properties.id))) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeParents = new Set<string>();
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
if (parentIds.has(id)) {
|
||||||
|
activeParents.add(id);
|
||||||
|
} else {
|
||||||
|
const parentId = featureParentMap.get(id);
|
||||||
|
if (parentId) {
|
||||||
|
activeParents.add(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...fc,
|
...fc,
|
||||||
features: fc.features.filter((feature) => {
|
features: fc.features.filter((feature) => {
|
||||||
const featureId = String(feature.properties.id);
|
const featureId = String(feature.properties.id);
|
||||||
if (selectedIds.has(featureId)) return true;
|
const parentId = featureParentMap.get(featureId);
|
||||||
if (selectedChildren.has(featureId)) return true;
|
|
||||||
if (selectedParents.has(featureId)) return true;
|
// 1. If this feature is a parent and its hierarchy is active, hide it
|
||||||
|
if (activeParents.has(featureId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If this feature is a child of an active parent, show it
|
||||||
|
if (parentId && activeParents.has(parentId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. By default, hide all child geometries that are not part of the active hierarchy
|
||||||
return !childIds.has(featureId);
|
return !childIds.has(featureId);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { initCircle } from "@/uhm/lib/map/engines/circleEngine";
|
|||||||
import { createEditingEngine } from "@/uhm/lib/map/engines/editingEngine";
|
import { createEditingEngine } from "@/uhm/lib/map/engines/editingEngine";
|
||||||
import { FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
import { FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||||
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
import { buildClientFeatureId, getSelectableLayers } from "./mapUtils";
|
import { buildClientFeatureId } from "./mapUtils";
|
||||||
import { MapHoverPayload } from "../Map";
|
import type { MapFeaturePayload } from "../Map";
|
||||||
|
|
||||||
type EngineBinding = {
|
type EngineBinding = {
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
@@ -34,7 +34,7 @@ type UseMapInteractionProps = {
|
|||||||
onDeleteRef: React.MutableRefObject<((id: string | number | (string | number)[]) => void) | undefined>;
|
onDeleteRef: React.MutableRefObject<((id: string | number | (string | number)[]) => void) | undefined>;
|
||||||
onHideRef: React.MutableRefObject<((id: string | number) => void) | undefined>;
|
onHideRef: React.MutableRefObject<((id: string | number) => void) | undefined>;
|
||||||
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
|
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
|
||||||
onHoverFeatureChangeRef: React.MutableRefObject<((payload: MapHoverPayload | null) => void) | undefined>;
|
onFeatureClickRef: React.MutableRefObject<((payload: MapFeaturePayload | null) => void) | undefined>;
|
||||||
onBindGeometriesRef?: React.MutableRefObject<((targetId: string | number, sourceIds: (string | number)[]) => void) | undefined>;
|
onBindGeometriesRef?: React.MutableRefObject<((targetId: string | number, sourceIds: (string | number)[]) => void) | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export function useMapInteraction({
|
|||||||
onDeleteRef,
|
onDeleteRef,
|
||||||
onHideRef,
|
onHideRef,
|
||||||
onUpdateRef,
|
onUpdateRef,
|
||||||
onHoverFeatureChangeRef,
|
onFeatureClickRef,
|
||||||
onBindGeometriesRef,
|
onBindGeometriesRef,
|
||||||
}: UseMapInteractionProps) {
|
}: UseMapInteractionProps) {
|
||||||
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
|
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
|
||||||
@@ -69,7 +69,7 @@ export function useMapInteraction({
|
|||||||
}, [mapRef, onUpdateRef]);
|
}, [mapRef, onUpdateRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allowsSelectionMode = mode === "select" || mode === "replay";
|
const allowsSelectionMode = mode === "select" || mode === "replay" || mode === "preview" || mode === "replay_preview";
|
||||||
if (!allowsSelectionMode || !selectedFeatureIds || selectedFeatureIds.length === 0) {
|
if (!allowsSelectionMode || !selectedFeatureIds || selectedFeatureIds.length === 0) {
|
||||||
editingEngineRef.current?.clearEditing();
|
editingEngineRef.current?.clearEditing();
|
||||||
// Clear the internal selection state of the select engine to stay in sync with React state
|
// Clear the internal selection state of the select engine to stay in sync with React state
|
||||||
@@ -183,7 +183,23 @@ export function useMapInteraction({
|
|||||||
(ids) => onSelectFeatureIdsRef.current?.(ids),
|
(ids) => onSelectFeatureIdsRef.current?.(ids),
|
||||||
(id: string | number) => onSetModeRef.current?.("replay", id),
|
(id: string | number) => onSetModeRef.current?.("replay", id),
|
||||||
() => Boolean(editingEngineRef.current?.editingRef.current),
|
() => Boolean(editingEngineRef.current?.editingRef.current),
|
||||||
(targetId, sourceIds) => onBindGeometriesRef?.current?.(targetId, sourceIds)
|
(targetId, sourceIds) => onBindGeometriesRef?.current?.(targetId, sourceIds),
|
||||||
|
(payload) => {
|
||||||
|
if (!payload) {
|
||||||
|
onFeatureClickRef.current?.(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFeature =
|
||||||
|
renderDraftRef.current.features.find(
|
||||||
|
(item) => String(item.properties.id) === String(payload.featureId)
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
onFeatureClickRef.current?.({
|
||||||
|
...payload,
|
||||||
|
feature: currentFeature,
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const cleanupPoint = initPoint(
|
const cleanupPoint = initPoint(
|
||||||
@@ -277,7 +293,9 @@ export function useMapInteraction({
|
|||||||
engineBindingsRef.current = {
|
engineBindingsRef.current = {
|
||||||
draw: drawingEngine,
|
draw: drawingEngine,
|
||||||
select: selectEngine,
|
select: selectEngine,
|
||||||
|
preview: selectEngine,
|
||||||
replay: selectEngine,
|
replay: selectEngine,
|
||||||
|
replay_preview: selectEngine,
|
||||||
"add-line": lineEngine,
|
"add-line": lineEngine,
|
||||||
"add-path": pathEngine,
|
"add-path": pathEngine,
|
||||||
"add-circle": circleEngine,
|
"add-circle": circleEngine,
|
||||||
@@ -292,52 +310,6 @@ export function useMapInteraction({
|
|||||||
drawingEngine.cleanup
|
drawingEngine.cleanup
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHoverMove = (event: maplibregl.MapMouseEvent) => {
|
|
||||||
const callback = onHoverFeatureChangeRef.current;
|
|
||||||
if (!callback) return;
|
|
||||||
|
|
||||||
const selectableLayers = getSelectableLayers(map);
|
|
||||||
if (!selectableLayers.length) {
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const features = map.queryRenderedFeatures(event.point, {
|
|
||||||
layers: selectableLayers,
|
|
||||||
}) as maplibregl.MapGeoJSONFeature[];
|
|
||||||
|
|
||||||
const feature = features[0];
|
|
||||||
const rawFeatureId = feature?.id ?? feature?.properties?.id;
|
|
||||||
if (rawFeatureId === undefined || rawFeatureId === null) {
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentFeature =
|
|
||||||
renderDraftRef.current.features.find(
|
|
||||||
(item) => String(item.properties.id) === String(rawFeatureId)
|
|
||||||
) || null;
|
|
||||||
|
|
||||||
callback({
|
|
||||||
featureId: rawFeatureId,
|
|
||||||
feature: currentFeature,
|
|
||||||
point: { x: event.point.x, y: event.point.y },
|
|
||||||
lngLat: { lng: event.lngLat.lng, lat: event.lngLat.lat },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCanvasMouseLeave = () => {
|
|
||||||
onHoverFeatureChangeRef.current?.(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
map.on("mousemove", handleHoverMove);
|
|
||||||
mapCleanupFnsRef.current.push(() => map.off("mousemove", handleHoverMove));
|
|
||||||
|
|
||||||
map.getCanvasContainer().addEventListener("mouseleave", handleCanvasMouseLeave);
|
|
||||||
mapCleanupFnsRef.current.push(() => {
|
|
||||||
map.getCanvasContainer().removeEventListener("mouseleave", handleCanvasMouseLeave);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allowGeometryEditing) {
|
if (allowGeometryEditing) {
|
||||||
editingEngineRef.current?.bindEditEvents(map);
|
editingEngineRef.current?.bindEditEvents(map);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export function getBaseMapStyle(): maplibregl.StyleSpecification {
|
|||||||
|
|
||||||
export function setupMapLayers(
|
export function setupMapLayers(
|
||||||
map: maplibregl.Map,
|
map: maplibregl.Map,
|
||||||
backgroundVisibility: BackgroundLayerVisibility,
|
backgroundVisibility: BackgroundLayerVisibility
|
||||||
highlightFeatures: FeatureCollection | null,
|
|
||||||
applyHighlightToMap: (fc: FeatureCollection) => void
|
|
||||||
) {
|
) {
|
||||||
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||||
void replaceBackgroundLayersWithGoong(map, backgroundVisibility).catch((error) => {
|
void replaceBackgroundLayersWithGoong(map, backgroundVisibility).catch((error) => {
|
||||||
@@ -211,62 +209,6 @@ export function setupMapLayers(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addSource("entity-focus", {
|
|
||||||
type: "geojson",
|
|
||||||
data: EMPTY_FEATURE_COLLECTION,
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
|
||||||
id: "entity-focus-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: "entity-focus",
|
|
||||||
filter: [
|
|
||||||
"any",
|
|
||||||
["==", ["geometry-type"], "Polygon"],
|
|
||||||
["==", ["geometry-type"], "MultiPolygon"],
|
|
||||||
],
|
|
||||||
paint: {
|
|
||||||
"fill-color": "#fde047",
|
|
||||||
"fill-opacity": 0.2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
|
||||||
id: "entity-focus-line",
|
|
||||||
type: "line",
|
|
||||||
source: "entity-focus",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#f59e0b",
|
|
||||||
"line-width": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
1, 2.4,
|
|
||||||
4, 4,
|
|
||||||
6, 5.5,
|
|
||||||
],
|
|
||||||
"line-opacity": 0.98,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
|
||||||
id: "entity-focus-points",
|
|
||||||
type: "circle",
|
|
||||||
source: "entity-focus",
|
|
||||||
filter: [
|
|
||||||
"any",
|
|
||||||
["==", ["geometry-type"], "Point"],
|
|
||||||
["==", ["geometry-type"], "MultiPoint"],
|
|
||||||
],
|
|
||||||
paint: {
|
|
||||||
"circle-color": "#f8fafc",
|
|
||||||
"circle-radius": 8,
|
|
||||||
"circle-stroke-color": "#f59e0b",
|
|
||||||
"circle-stroke-width": 3,
|
|
||||||
"circle-opacity": 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
applyHighlightToMap(highlightFeatures || EMPTY_FEATURE_COLLECTION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replaceBackgroundLayersWithGoong(
|
async function replaceBackgroundLayersWithGoong(
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type UseMapSyncProps = {
|
|||||||
applyGeometryBindingFilter: boolean;
|
applyGeometryBindingFilter: boolean;
|
||||||
fitToDraftBounds: boolean;
|
fitToDraftBounds: boolean;
|
||||||
fitBoundsKey?: string | number | null;
|
fitBoundsKey?: string | number | null;
|
||||||
highlightFeatures?: FeatureCollection | null;
|
|
||||||
focusFeatureCollection?: FeatureCollection | null;
|
focusFeatureCollection?: FeatureCollection | null;
|
||||||
focusRequestKey?: string | number | null;
|
focusRequestKey?: string | number | null;
|
||||||
focusPadding?: number | maplibregl.PaddingOptions;
|
focusPadding?: number | maplibregl.PaddingOptions;
|
||||||
@@ -58,7 +57,6 @@ export function useMapSync({
|
|||||||
applyGeometryBindingFilter,
|
applyGeometryBindingFilter,
|
||||||
fitToDraftBounds,
|
fitToDraftBounds,
|
||||||
fitBoundsKey,
|
fitBoundsKey,
|
||||||
highlightFeatures,
|
|
||||||
focusFeatureCollection,
|
focusFeatureCollection,
|
||||||
focusRequestKey,
|
focusRequestKey,
|
||||||
focusPadding,
|
focusPadding,
|
||||||
@@ -75,7 +73,6 @@ export function useMapSync({
|
|||||||
const selectedFeatureIdsRef = useRef<(string | number)[]>(selectedFeatureIds);
|
const selectedFeatureIdsRef = useRef<(string | number)[]>(selectedFeatureIds);
|
||||||
const applyGeometryBindingFilterRef = useRef(applyGeometryBindingFilter);
|
const applyGeometryBindingFilterRef = useRef(applyGeometryBindingFilter);
|
||||||
const fitToDraftBoundsRef = useRef(fitToDraftBounds);
|
const fitToDraftBoundsRef = useRef(fitToDraftBounds);
|
||||||
const highlightFeaturesRef = useRef<FeatureCollection | null>(highlightFeatures || null);
|
|
||||||
const imageOverlayRef = useRef<MapImageOverlay | null>(imageOverlay || null);
|
const imageOverlayRef = useRef<MapImageOverlay | null>(imageOverlay || null);
|
||||||
const focusFeatureCollectionRef = useRef<FeatureCollection | null | undefined>(focusFeatureCollection);
|
const focusFeatureCollectionRef = useRef<FeatureCollection | null | undefined>(focusFeatureCollection);
|
||||||
const focusPaddingRef = useRef<number | maplibregl.PaddingOptions | undefined>(focusPadding);
|
const focusPaddingRef = useRef<number | maplibregl.PaddingOptions | undefined>(focusPadding);
|
||||||
@@ -90,7 +87,6 @@ export function useMapSync({
|
|||||||
useEffect(() => { selectedFeatureIdsRef.current = selectedFeatureIds; }, [selectedFeatureIds]);
|
useEffect(() => { selectedFeatureIdsRef.current = selectedFeatureIds; }, [selectedFeatureIds]);
|
||||||
useEffect(() => { applyGeometryBindingFilterRef.current = applyGeometryBindingFilter; }, [applyGeometryBindingFilter]);
|
useEffect(() => { applyGeometryBindingFilterRef.current = applyGeometryBindingFilter; }, [applyGeometryBindingFilter]);
|
||||||
useEffect(() => { fitToDraftBoundsRef.current = fitToDraftBounds; }, [fitToDraftBounds]);
|
useEffect(() => { fitToDraftBoundsRef.current = fitToDraftBounds; }, [fitToDraftBounds]);
|
||||||
useEffect(() => { highlightFeaturesRef.current = highlightFeatures || null; }, [highlightFeatures]);
|
|
||||||
useEffect(() => { imageOverlayRef.current = imageOverlay || null; }, [imageOverlay]);
|
useEffect(() => { imageOverlayRef.current = imageOverlay || null; }, [imageOverlay]);
|
||||||
useEffect(() => { focusFeatureCollectionRef.current = focusFeatureCollection; }, [focusFeatureCollection]);
|
useEffect(() => { focusFeatureCollectionRef.current = focusFeatureCollection; }, [focusFeatureCollection]);
|
||||||
useEffect(() => { focusPaddingRef.current = focusPadding; }, [focusPadding]);
|
useEffect(() => { focusPaddingRef.current = focusPadding; }, [focusPadding]);
|
||||||
@@ -102,8 +98,7 @@ export function useMapSync({
|
|||||||
const applyRenderDraftToMap = useCallback((
|
const applyRenderDraftToMap = useCallback((
|
||||||
renderFc: FeatureCollection,
|
renderFc: FeatureCollection,
|
||||||
labelContextOverride?: FeatureCollection,
|
labelContextOverride?: FeatureCollection,
|
||||||
selectedIdsOverride?: (string | number)[],
|
selectedIdsOverride?: (string | number)[]
|
||||||
highlightFeaturesOverride?: FeatureCollection | null
|
|
||||||
) => {
|
) => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
@@ -122,12 +117,9 @@ export function useMapSync({
|
|||||||
|
|
||||||
const labelContext = labelContextOverride || labelContextDraftRef.current || renderFc;
|
const labelContext = labelContextOverride || labelContextDraftRef.current || renderFc;
|
||||||
const currentSelectedIds = selectedIdsOverride || selectedFeatureIdsRef.current;
|
const currentSelectedIds = selectedIdsOverride || selectedFeatureIdsRef.current;
|
||||||
const highlightFeaturesVal = highlightFeaturesOverride !== undefined
|
|
||||||
? highlightFeaturesOverride
|
|
||||||
: highlightFeaturesRef.current;
|
|
||||||
|
|
||||||
const bindingFilteredRenderDraft = applyGeometryBindingFilterRef.current
|
const bindingFilteredRenderDraft = applyGeometryBindingFilterRef.current
|
||||||
? filterDraftByBinding(renderFc, currentSelectedIds, highlightFeaturesVal)
|
? filterDraftByBinding(renderFc, currentSelectedIds)
|
||||||
: renderFc;
|
: renderFc;
|
||||||
const visibilityFilteredDraft = filterDraftByGeometryVisibility(bindingFilteredRenderDraft, geometryVisibilityRef.current);
|
const visibilityFilteredDraft = filterDraftByGeometryVisibility(bindingFilteredRenderDraft, geometryVisibilityRef.current);
|
||||||
const mapSourceDraft = decorateFeaturesWithEntityColors(visibilityFilteredDraft);
|
const mapSourceDraft = decorateFeaturesWithEntityColors(visibilityFilteredDraft);
|
||||||
@@ -157,15 +149,7 @@ export function useMapSync({
|
|||||||
}
|
}
|
||||||
}, [mapRef]);
|
}, [mapRef]);
|
||||||
|
|
||||||
const applyHighlightToMap = useCallback((fc: FeatureCollection) => {
|
|
||||||
const map = mapRef.current;
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
const source = map.getSource("entity-focus") as maplibregl.GeoJSONSource | undefined;
|
|
||||||
if (!source) return;
|
|
||||||
source.setData(fc);
|
|
||||||
moveHighlightLayersToTop(map);
|
|
||||||
}, [mapRef]);
|
|
||||||
|
|
||||||
const tryCenterToUserLocation = useCallback(() => {
|
const tryCenterToUserLocation = useCallback(() => {
|
||||||
if (geolocationCenteredRef.current) return;
|
if (geolocationCenteredRef.current) return;
|
||||||
@@ -198,12 +182,7 @@ export function useMapSync({
|
|||||||
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||||
}, [backgroundVisibility, mapRef]);
|
}, [backgroundVisibility, mapRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef.current;
|
|
||||||
if (!map || !map.isStyleLoaded()) return;
|
|
||||||
const source = map.getSource("entity-focus") as maplibregl.GeoJSONSource | undefined;
|
|
||||||
source?.setData(highlightFeatures || EMPTY_FEATURE_COLLECTION);
|
|
||||||
}, [highlightFeatures, mapRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
@@ -212,7 +191,7 @@ export function useMapSync({
|
|||||||
}, [imageOverlay, mapRef]);
|
}, [imageOverlay, mapRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
applyRenderDraftToMap(renderDraft, labelContextDraft, selectedFeatureIds, highlightFeatures);
|
applyRenderDraftToMap(renderDraft, labelContextDraft, selectedFeatureIds);
|
||||||
const editingId = editingEngineRef.current?.editingRef?.current?.id;
|
const editingId = editingEngineRef.current?.editingRef?.current?.id;
|
||||||
if (allowGeometryEditing && editingId !== undefined && editingId !== null) {
|
if (allowGeometryEditing && editingId !== undefined && editingId !== null) {
|
||||||
const stillExists = renderDraft.features.some((f) => f.properties.id === editingId);
|
const stillExists = renderDraft.features.some((f) => f.properties.id === editingId);
|
||||||
@@ -228,7 +207,6 @@ export function useMapSync({
|
|||||||
selectedFeatureIds,
|
selectedFeatureIds,
|
||||||
applyGeometryBindingFilter,
|
applyGeometryBindingFilter,
|
||||||
geometryVisibility,
|
geometryVisibility,
|
||||||
highlightFeatures,
|
|
||||||
applyRenderDraftToMap,
|
applyRenderDraftToMap,
|
||||||
editingEngineRef,
|
editingEngineRef,
|
||||||
]);
|
]);
|
||||||
@@ -266,7 +244,6 @@ export function useMapSync({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
applyRenderDraftToMap,
|
applyRenderDraftToMap,
|
||||||
applyHighlightToMap,
|
|
||||||
tryCenterToUserLocation,
|
tryCenterToUserLocation,
|
||||||
applyImageOverlayToMap: () => {
|
applyImageOverlayToMap: () => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
@@ -276,11 +253,4 @@ export function useMapSync({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveHighlightLayersToTop(map: maplibregl.Map) {
|
|
||||||
const layerIds = ["entity-focus-fill", "entity-focus-line", "entity-focus-points"];
|
|
||||||
for (const layerId of layerIds) {
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
map.moveLayer(layerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import "react-quill-new/dist/quill.snow.css";
|
||||||
|
|
||||||
import type { Entity } from "@/uhm/api/entities";
|
import type { Entity } from "@/uhm/api/entities";
|
||||||
import type { Wiki } from "@/uhm/api/wikis";
|
import type { Wiki } from "@/uhm/api/wikis";
|
||||||
@@ -238,34 +239,101 @@ export default function PublicWikiSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ width: `${width}px` }}
|
style={{
|
||||||
className="relative flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-800 dark:bg-gray-950"
|
width: `${width}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
minHeight: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: 20,
|
||||||
|
border: "1px solid rgba(148, 163, 184, 0.22)",
|
||||||
|
background: "linear-gradient(145deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.85))",
|
||||||
|
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.45)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Drag Handle on the left edge */}
|
{/* Drag Handle on the left edge */}
|
||||||
<div
|
<div
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
className="absolute left-0 top-0 bottom-0 w-[6px] cursor-col-resize z-50 group select-none hover:bg-black/[0.03] dark:hover:bg-white/[0.02]"
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 6,
|
||||||
|
cursor: "col-resize",
|
||||||
|
zIndex: 50,
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
title="Kéo để chỉnh kích thước"
|
title="Kéo để chỉnh kích thước"
|
||||||
>
|
>
|
||||||
{/* Visual drag line overlay */}
|
{/* Visual drag line overlay */}
|
||||||
<div className="absolute left-[2px] top-0 bottom-0 w-[2px] bg-transparent group-hover:bg-brand-500/50 group-active:bg-brand-500 transition-colors" />
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 2,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 2,
|
||||||
|
background: "transparent",
|
||||||
|
transition: "background-color 0.2s",
|
||||||
|
}}
|
||||||
|
className="group-hover:bg-sky-500/50 group-active:bg-sky-500"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-gray-200 px-4 py-4 dark:border-gray-800">
|
<div
|
||||||
<div className="flex items-start justify-between gap-3">
|
style={{
|
||||||
<div className="min-w-0">
|
borderBottom: "1px solid rgba(148, 163, 184, 0.15)",
|
||||||
<div className="text-[11px] uppercase tracking-[0.08em] text-gray-500 dark:text-gray-400">
|
padding: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 12 }}>
|
||||||
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "1.2px",
|
||||||
|
fontWeight: 900,
|
||||||
|
color: "#94a3b8",
|
||||||
|
}}
|
||||||
|
>
|
||||||
Wiki
|
Wiki
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-lg font-semibold leading-tight text-gray-900 dark:text-gray-100">
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 4,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
color: "#f8fafc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{entity?.name?.trim() || wiki?.title?.trim() || "Wiki"}
|
{entity?.name?.trim() || wiki?.title?.trim() || "Wiki"}
|
||||||
</div>
|
</div>
|
||||||
{entity?.description?.trim() ? (
|
{entity?.description?.trim() ? (
|
||||||
<div className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: "#cbd5e1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{entity.description.trim()}
|
{entity.description.trim()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{wiki?.title?.trim() && wiki.title.trim() !== entity?.name?.trim() ? (
|
{wiki?.title?.trim() && wiki.title.trim() !== entity?.name?.trim() ? (
|
||||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 6,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#94a3b8",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{wiki.title.trim()}
|
{wiki.title.trim()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -274,7 +342,22 @@ export default function PublicWikiSidebar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-gray-200 text-sm text-gray-500 transition hover:bg-gray-50 hover:text-gray-800 dark:border-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.04] dark:hover:text-gray-100"
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "1px solid rgba(148, 163, 184, 0.25)",
|
||||||
|
background: "rgba(30, 41, 59, 0.4)",
|
||||||
|
color: "#94a3b8",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: 12,
|
||||||
|
transition: "all 0.2s",
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
className="hover:bg-slate-700/50 hover:text-slate-100"
|
||||||
aria-label="Close wiki sidebar"
|
aria-label="Close wiki sidebar"
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
@@ -283,18 +366,44 @@ export default function PublicWikiSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{toc.length ? (
|
{toc.length ? (
|
||||||
<div className="border-b border-gray-200 px-3 py-2 dark:border-gray-800">
|
<div
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
style={{
|
||||||
|
borderBottom: "1px solid rgba(148, 163, 184, 0.15)",
|
||||||
|
padding: "8px 12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="uhm-public-wiki-toc-list"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
overflowX: "auto",
|
||||||
|
paddingBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{toc.slice(0, 8).map((item) => {
|
{toc.slice(0, 8).map((item) => {
|
||||||
const isActive = effectiveActiveHeadingId === item.id;
|
const isActive = effectiveActiveHeadingId === item.id;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={`#${item.id}`}
|
href={`#${item.id}`}
|
||||||
className={`shrink-0 rounded-full px-3 py-1 text-xs transition ${isActive
|
style={{
|
||||||
? "bg-brand-50 text-brand-700 dark:bg-brand-500/10 dark:text-brand-300"
|
flexShrink: 0,
|
||||||
: "bg-gray-50 text-gray-600 hover:bg-gray-100 dark:bg-white/[0.03] dark:text-gray-300 dark:hover:bg-white/[0.06]"
|
borderRadius: 9999,
|
||||||
}`}
|
padding: "4px 10px",
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 650,
|
||||||
|
textDecoration: "none",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
background: isActive
|
||||||
|
? "rgba(56, 189, 248, 0.15)"
|
||||||
|
: "rgba(30, 41, 59, 0.4)",
|
||||||
|
color: isActive ? "#38bdf8" : "#94a3b8",
|
||||||
|
border: isActive
|
||||||
|
? "1px solid rgba(56, 189, 248, 0.3)"
|
||||||
|
: "1px solid rgba(148, 163, 184, 0.1)",
|
||||||
|
}}
|
||||||
|
className={isActive ? "" : "hover:bg-slate-700/40 hover:text-slate-200"}
|
||||||
>
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</a>
|
</a>
|
||||||
@@ -304,31 +413,74 @@ export default function PublicWikiSidebar({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
<div
|
||||||
|
className="uhm-public-wiki-sidebar-content"
|
||||||
|
style={{
|
||||||
|
minHeight: 0,
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-3 px-4 py-4">
|
<div style={{ display: "flex", flexDirection: "column", gap: 12, padding: 16 }}>
|
||||||
<div className="h-4 w-28 animate-pulse rounded bg-gray-100 dark:bg-white/[0.06]" />
|
<div
|
||||||
<div className="h-4 w-full animate-pulse rounded bg-gray-100 dark:bg-white/[0.06]" />
|
style={{ height: 16, width: 110, borderRadius: 4, background: "rgba(148, 163, 184, 0.15)" }}
|
||||||
<div className="h-4 w-4/5 animate-pulse rounded bg-gray-100 dark:bg-white/[0.06]" />
|
className="animate-pulse"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{ height: 16, width: "100%", borderRadius: 4, background: "rgba(148, 163, 184, 0.15)" }}
|
||||||
|
className="animate-pulse"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{ height: 16, width: "80%", borderRadius: 4, background: "rgba(148, 163, 184, 0.15)" }}
|
||||||
|
className="animate-pulse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="px-4 py-4 text-sm text-red-600 dark:text-red-300">
|
<div style={{ padding: 16, fontSize: 14, color: "#f87171" }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : wiki ? (
|
) : wiki ? (
|
||||||
<div
|
<div
|
||||||
ref={contentRootRef}
|
ref={contentRootRef}
|
||||||
className="uhm-wiki-sidebar-view ql-editor text-sm text-gray-900 dark:text-gray-100"
|
className="uhm-wiki-sidebar-view ql-editor"
|
||||||
|
style={{ fontSize: 14, color: "#cbd5e1" }}
|
||||||
dangerouslySetInnerHTML={{ __html: renderHtml }}
|
dangerouslySetInnerHTML={{ __html: renderHtml }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-4 py-4 text-sm text-gray-600 dark:text-gray-300">
|
<div style={{ padding: 16, fontSize: 14, color: "#94a3b8" }}>
|
||||||
Entity này chưa có wiki liên kết.
|
Entity này chưa có wiki liên kết.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
|
.uhm-public-wiki-sidebar-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-sidebar-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-sidebar-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-sidebar-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-toc-list::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-toc-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-toc-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.uhm-public-wiki-toc-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor {
|
.uhm-wiki-sidebar-view.ql-editor {
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
@@ -338,6 +490,7 @@ export default function PublicWikiSidebar({
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
color: #cbd5e1 !important;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor p {
|
.uhm-wiki-sidebar-view.ql-editor p {
|
||||||
margin: 0 0 0.75em;
|
margin: 0 0 0.75em;
|
||||||
@@ -347,12 +500,14 @@ export default function PublicWikiSidebar({
|
|||||||
font-size: 1.6em;
|
font-size: 1.6em;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
color: #f8fafc !important;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor h2 {
|
.uhm-wiki-sidebar-view.ql-editor h2 {
|
||||||
margin: 1.05em 0 0.55em;
|
margin: 1.05em 0 0.55em;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
color: #f8fafc !important;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor h3,
|
.uhm-wiki-sidebar-view.ql-editor h3,
|
||||||
.uhm-wiki-sidebar-view.ql-editor h4,
|
.uhm-wiki-sidebar-view.ql-editor h4,
|
||||||
@@ -362,6 +517,7 @@ export default function PublicWikiSidebar({
|
|||||||
font-size: 1.05em;
|
font-size: 1.05em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
color: #f8fafc !important;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor ul,
|
.uhm-wiki-sidebar-view.ql-editor ul,
|
||||||
.uhm-wiki-sidebar-view.ql-editor ol {
|
.uhm-wiki-sidebar-view.ql-editor ol {
|
||||||
@@ -371,45 +527,53 @@ export default function PublicWikiSidebar({
|
|||||||
.uhm-wiki-sidebar-view.ql-editor blockquote {
|
.uhm-wiki-sidebar-view.ql-editor blockquote {
|
||||||
margin: 0 0 0.75em;
|
margin: 0 0 0.75em;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
border-left: 3px solid rgba(148, 163, 184, 0.6);
|
border-left: 3px solid rgba(148, 163, 184, 0.4);
|
||||||
color: rgba(71, 85, 105, 1);
|
|
||||||
}
|
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor blockquote {
|
|
||||||
border-left-color: rgba(100, 116, 139, 0.6);
|
|
||||||
color: rgba(203, 213, 225, 0.95);
|
color: rgba(203, 213, 225, 0.95);
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor pre {
|
.uhm-wiki-sidebar-view.ql-editor pre {
|
||||||
margin: 0 0 0.75em;
|
margin: 0 0 0.75em;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border: 1px solid rgba(226, 232, 240, 1);
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(248, 250, 252, 1);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
color: #cbd5e1;
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor pre {
|
|
||||||
border-color: rgba(51, 65, 85, 1);
|
|
||||||
background: rgba(2, 6, 23, 0.4);
|
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor img {
|
.uhm-wiki-sidebar-view.ql-editor img {
|
||||||
display: block !important;
|
max-width: 100%;
|
||||||
max-width: 100% !important;
|
height: auto;
|
||||||
height: auto !important;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
float: none !important;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img[style*="float: left"],
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img.ql-align-left {
|
||||||
|
float: left !important;
|
||||||
|
margin: 4px 14px 14px 0px !important;
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img[style*="float: right"],
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img.ql-align-right {
|
||||||
|
float: right !important;
|
||||||
|
margin: 4px 0px 14px 14px !important;
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img[style*="display: block"],
|
||||||
|
.uhm-wiki-sidebar-view.ql-editor img.ql-align-center {
|
||||||
|
display: block !important;
|
||||||
margin: 1.25em auto !important;
|
margin: 1.25em auto !important;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor a {
|
.uhm-wiki-sidebar-view.ql-editor a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor a[href]:not([href=""]):not([href="__missing__"]) {
|
.uhm-wiki-sidebar-view.ql-editor a[href]:not([href=""]):not([href="__missing__"]) {
|
||||||
color: #2563eb;
|
color: #38bdf8 !important;
|
||||||
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor a[href]:not([href=""]):not([href="__missing__"]) {
|
.uhm-wiki-sidebar-view.ql-editor a[href]:not([href=""]):not([href="__missing__"]):hover {
|
||||||
color: #60a5fa;
|
color: #7dd3fc !important;
|
||||||
}
|
}
|
||||||
.uhm-wiki-sidebar-view.ql-editor a[href="__missing__"] {
|
.uhm-wiki-sidebar-view.ql-editor a[href="__missing__"] {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -418,12 +582,7 @@ export default function PublicWikiSidebar({
|
|||||||
.uhm-wiki-sidebar-view.ql-editor a:not([href]),
|
.uhm-wiki-sidebar-view.ql-editor a:not([href]),
|
||||||
.uhm-wiki-sidebar-view.ql-editor a[href=""],
|
.uhm-wiki-sidebar-view.ql-editor a[href=""],
|
||||||
.uhm-wiki-sidebar-view.ql-editor a[href="__missing__"] {
|
.uhm-wiki-sidebar-view.ql-editor a[href="__missing__"] {
|
||||||
color: #dc2626;
|
color: #f87171 !important;
|
||||||
}
|
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor a:not([href]),
|
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor a[href=""],
|
|
||||||
:is(.dark *) .uhm-wiki-sidebar-view.ql-editor a[href="__missing__"] {
|
|
||||||
color: #f87171;
|
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.uhm-wiki-sidebar-view.ql-editor {
|
.uhm-wiki-sidebar-view.ql-editor {
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ Một số nguyên tắc nên giữ:
|
|||||||
- khi cần undo cho entity/wiki/link, đi qua `editor.setSnapshot*()` để undo stack biết
|
- khi cần undo cho entity/wiki/link, đi qua `editor.setSnapshot*()` để undo stack biết
|
||||||
- khi cần undo cho replay script, đi qua `editor.mutateActiveReplay()` hoặc replay collection helper hiện có
|
- khi cần undo cho replay script, đi qua `editor.mutateActiveReplay()` hoặc replay collection helper hiện có
|
||||||
- hạn chế thêm `JSON.stringify` compare ở chỗ nóng nếu chưa đo hiệu năng
|
- hạn chế thêm `JSON.stringify` compare ở chỗ nóng nếu chưa đo hiệu năng
|
||||||
|
- khi thiết kế các chế độ preview, đảm bảo khôi phục camera view state & projection (Globe/Flat) về trạng thái gốc của editor bằng cách dùng `editorOriginalMapViewStateRef` và calling `restoreEditorOriginalMapState()`.
|
||||||
|
|
||||||
## 12. Chỗ dễ gây hiểu nhầm khi debug
|
## 12. Chỗ dễ gây hiểu nhầm khi debug
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Checklist này dùng sau mỗi lần sửa editor. Không thay thế typecheck/l
|
|||||||
- Undo trong replay mode chỉ undo replay session, không undo main geometry.
|
- Undo trong replay mode chỉ undo replay session, không undo main geometry.
|
||||||
- Play preview:
|
- Play preview:
|
||||||
- Step selection chạy đúng thứ tự.
|
- Step selection chạy đúng thứ tự.
|
||||||
- Stop/reset khôi phục title/dialog/image/hidden geometry/timeline/map camera cơ bản.
|
- Stop/reset khôi phục title/dialog/image/hidden geometry/timeline/map camera cơ bản và projection (Globe/Flat) ban đầu.
|
||||||
- Thoát replay rồi vào lại, detail vẫn còn nếu chưa undo.
|
- Thoát replay rồi vào lại, detail vẫn còn nếu chưa undo.
|
||||||
|
|
||||||
## 8. Import GEO từ search
|
## 8. Import GEO từ search
|
||||||
@@ -121,7 +121,7 @@ Các thao tác sau không được thêm undo action và không làm tăng pendi
|
|||||||
- Resize panel.
|
- Resize panel.
|
||||||
- Search query.
|
- Search query.
|
||||||
- Pick/paste/remove image overlay trace.
|
- Pick/paste/remove image overlay trace.
|
||||||
- Replay preview play/stop/reset.
|
- Replay preview play/stop/reset (khôi phục hoàn toàn camera view state và projection của editor ban đầu).
|
||||||
|
|
||||||
## 12. Final smoke
|
## 12. Final smoke
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Replay state nằm trong `useEditorState()`:
|
|||||||
- `replays` cộng overlay của `activeReplayDraft` nếu session hiện tại đã đổi nhưng chưa flush
|
- `replays` cộng overlay của `activeReplayDraft` nếu session hiện tại đã đổi nhưng chưa flush
|
||||||
|
|
||||||
Undo của replay session dùng stack riêng khi `mode === "replay"`.
|
Undo của replay session dùng stack riêng khi `mode === "replay"`.
|
||||||
`replay_preview` là session preview trong page, dùng `previewSession`/`useReplayPreview()` và không persist.
|
`replay_preview` là session preview trong page, dùng `previewSession`/`useReplayPreview()` và không persist. Khi thoát các chế độ preview, editor sẽ dọn dẹp hoàn toàn các map effects, highlight, và khôi phục camera view state & projection (Globe/Flat) ban đầu trước khi vào preview.
|
||||||
|
|
||||||
### 4.4. Project/session task state
|
### 4.4. Project/session task state
|
||||||
|
|
||||||
@@ -185,6 +185,27 @@ Giá trị thật được load từ `localStorage` key `uhm.backgroundLayerVisi
|
|||||||
|
|
||||||
Đây là single source of truth cho phần wiki trong snapshot commit.
|
Đây là single source of truth cho phần wiki trong snapshot commit.
|
||||||
|
|
||||||
|
### 4.8. Preview session states và refs (Viewer / Replay Preview)
|
||||||
|
|
||||||
|
Các states và refs điều khiển preview được khai báo trực tiếp trong `page.tsx`:
|
||||||
|
|
||||||
|
- `previewSession: ReplayPreviewSession | null`
|
||||||
|
- Đóng băng toàn bộ snapshot collections (replays, draft, entities, wikis, links) cùng timeline, filter và camera view state khi chạy preview.
|
||||||
|
- `previewAutoplayMode: "start" | "selection" | null`
|
||||||
|
- Trạng thái autoplay (bắt đầu từ đầu hay từ step được chọn) của Replay Preview.
|
||||||
|
- `previewWikiCache`, `previewWikiError`, `isPreviewWikiLoading`
|
||||||
|
- Cache và status để hiển thị nội dung Wiki tương tác trong sidebar preview.
|
||||||
|
- `previewFeaturePopupAnchor: MapFeaturePayload | null`
|
||||||
|
- Neo tọa độ/payload của popup hiển thị thông tin geometry khi click trên map ở preview mode.
|
||||||
|
- `previewActiveEntityId`, `isPreviewEntitySidebarOpen`
|
||||||
|
- Sidebar hiển thị chi tiết entity được chọn trong preview.
|
||||||
|
- `previewLinkEntityPopup: PreviewLinkEntityPopupState | null`
|
||||||
|
- Trạng thái popup điều hướng sang entity khác khi click vào link wiki trong preview.
|
||||||
|
- `editorOriginalMapViewStateRef: ReturnType<MapHandle["getViewState"]> | null`
|
||||||
|
- Ref lưu giữ camera view state và projection (Globe/Flat) ban đầu của editor trước khi bắt đầu preview, phục vụ việc khôi phục hoàn toàn bản đồ khi exit.
|
||||||
|
- `replayPreviewReturnRef: { mode: "replay" | "preview"; session: ReplayPreviewSession | null }`
|
||||||
|
- Ref ghi nhận session và mode trước đó khi chuyển tiếp từ Viewer Preview sang Replay Preview, cho phép quay trở lại đúng Viewer Preview khi click thoát Replay Preview.
|
||||||
|
|
||||||
## 5. Snapshot state
|
## 5. Snapshot state
|
||||||
|
|
||||||
Editor đang làm việc với các snapshot collection chính ngoài geometry:
|
Editor đang làm việc với các snapshot collection chính ngoài geometry:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type EditorMode =
|
|||||||
| "add-line"
|
| "add-line"
|
||||||
| "add-path"
|
| "add-path"
|
||||||
| "add-circle"
|
| "add-circle"
|
||||||
|
| "preview"
|
||||||
| "replay"
|
| "replay"
|
||||||
| "replay_preview";
|
| "replay_preview";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
|
import type { ModeGetter } from "@/uhm/lib/map/engines/engineTypes";
|
||||||
|
|
||||||
|
export type SelectFeatureClickPayload = {
|
||||||
|
featureId: string | number;
|
||||||
|
point: { x: number; y: number };
|
||||||
|
lngLat: { lng: number; lat: number };
|
||||||
|
};
|
||||||
|
|
||||||
// Khởi tạo engine chọn feature và context menu edit/delete.
|
// Khởi tạo engine chọn feature và context menu edit/delete.
|
||||||
export function initSelect(
|
export function initSelect(
|
||||||
map: maplibregl.Map,
|
map: maplibregl.Map,
|
||||||
@@ -12,7 +18,8 @@ export function initSelect(
|
|||||||
onSelectIds?: (ids: (string | number)[]) => void,
|
onSelectIds?: (ids: (string | number)[]) => void,
|
||||||
onReplayEdit?: (id: string | number) => void,
|
onReplayEdit?: (id: string | number) => void,
|
||||||
isEditSessionActive?: () => boolean,
|
isEditSessionActive?: () => boolean,
|
||||||
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void
|
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void,
|
||||||
|
onFeatureClick?: (payload: SelectFeatureClickPayload | null) => void
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const FEATURE_STATE_SOURCES = [
|
const FEATURE_STATE_SOURCES = [
|
||||||
@@ -24,6 +31,8 @@ export function initSelect(
|
|||||||
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries);
|
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries);
|
||||||
let contextMenu: HTMLDivElement | null = null;
|
let contextMenu: HTMLDivElement | null = null;
|
||||||
let docClickHandler: ((ev: MouseEvent) => void) | null = null;
|
let docClickHandler: ((ev: MouseEvent) => void) | null = null;
|
||||||
|
let cursorTimer: number | null = null;
|
||||||
|
let pendingCursorPoint: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
// Bỏ highlight feature-state của toàn bộ đối tượng đang chọn.
|
// Bỏ highlight feature-state của toàn bộ đối tượng đang chọn.
|
||||||
function clearSelection(emit = true) {
|
function clearSelection(emit = true) {
|
||||||
@@ -38,7 +47,7 @@ export function initSelect(
|
|||||||
// Chọn hoặc toggle đối tượng; giữ Alt để chọn cộng dồn/tắt chọn.
|
// Chọn hoặc toggle đối tượng; giữ Alt để chọn cộng dồn/tắt chọn.
|
||||||
function selectFeature(feature: maplibregl.MapGeoJSONFeature, additive: boolean) {
|
function selectFeature(feature: maplibregl.MapGeoJSONFeature, additive: boolean) {
|
||||||
const id = feature.id ?? feature.properties?.id;
|
const id = feature.id ?? feature.properties?.id;
|
||||||
if (id === undefined || id === null) return;
|
if (id === undefined || id === null) return false;
|
||||||
|
|
||||||
if (!additive) {
|
if (!additive) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
@@ -52,17 +61,19 @@ export function initSelect(
|
|||||||
setSelectionStateForId(idToRemove, false);
|
setSelectionStateForId(idToRemove, false);
|
||||||
selectedIds.delete(idToRemove);
|
selectedIds.delete(idToRemove);
|
||||||
onSelectIds?.(Array.from(selectedIds));
|
onSelectIds?.(Array.from(selectedIds));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectionStateForId(id, true);
|
setSelectionStateForId(id, true);
|
||||||
selectedIds.add(id);
|
selectedIds.add(id);
|
||||||
onSelectIds?.(Array.from(selectedIds));
|
onSelectIds?.(Array.from(selectedIds));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chọn feature theo click trái, hỗ trợ additive bằng Alt.
|
// Chọn feature theo click trái, hỗ trợ additive bằng Alt.
|
||||||
function onClick(e: maplibregl.MapLayerMouseEvent) {
|
function onClick(e: maplibregl.MapLayerMouseEvent) {
|
||||||
if (getMode() !== "select" && getMode() !== "replay") return;
|
const mode = getMode();
|
||||||
|
if (mode !== "select" && mode !== "replay" && mode !== "preview" && mode !== "replay_preview") return;
|
||||||
if (isEditSessionActive?.()) return;
|
if (isEditSessionActive?.()) return;
|
||||||
const selectableLayers = getSelectableLayers();
|
const selectableLayers = getSelectableLayers();
|
||||||
if (!selectableLayers.length) return;
|
if (!selectableLayers.length) return;
|
||||||
@@ -73,22 +84,37 @@ export function initSelect(
|
|||||||
|
|
||||||
if (!features.length) {
|
if (!features.length) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
onFeatureClick?.(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const additive = !!e.originalEvent?.altKey;
|
const additive = !!e.originalEvent?.altKey;
|
||||||
selectFeature(pickPreferredFeature(features), additive);
|
const feature = pickPreferredFeature(features);
|
||||||
|
const didSelect = selectFeature(feature, additive);
|
||||||
|
if (!didSelect) {
|
||||||
|
onFeatureClick?.(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = feature.id ?? feature.properties?.id;
|
||||||
|
if (id === undefined || id === null) return;
|
||||||
|
onFeatureClick?.({
|
||||||
|
featureId: id,
|
||||||
|
point: { x: e.point.x, y: e.point.y },
|
||||||
|
lngLat: { lng: e.lngLat.lng, lat: e.lngLat.lat },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hiển thị menu ngữ cảnh (sửa/xóa) khi click chuột phải.
|
// Hiển thị menu ngữ cảnh (sửa/xóa) khi click chuột phải.
|
||||||
// Mở menu thao tác khi click phải lên feature.
|
// Mở menu thao tác khi click phải lên feature.
|
||||||
function onRightClick(e: maplibregl.MapLayerMouseEvent) {
|
function onRightClick(e: maplibregl.MapLayerMouseEvent) {
|
||||||
if (getMode() !== "select" && getMode() !== "replay") return;
|
const mode = getMode();
|
||||||
|
if (mode !== "select" && mode !== "replay" && mode !== "preview" && mode !== "replay_preview") return;
|
||||||
const selectableLayers = getSelectableLayers();
|
const selectableLayers = getSelectableLayers();
|
||||||
if (!selectableLayers.length) return;
|
if (!selectableLayers.length) return;
|
||||||
|
|
||||||
e.preventDefault(); // block browser menu
|
e.preventDefault(); // block browser menu
|
||||||
if (getMode() === "replay") return;
|
if (mode === "replay" || mode === "preview" || mode === "replay_preview") return;
|
||||||
if (isEditSessionActive?.()) return;
|
if (isEditSessionActive?.()) return;
|
||||||
|
|
||||||
const features = map.queryRenderedFeatures(e.point, {
|
const features = map.queryRenderedFeatures(e.point, {
|
||||||
@@ -122,21 +148,30 @@ export function initSelect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Đổi cursor pointer khi hover lên đối tượng có thể chọn.
|
// Đổi cursor pointer khi hover lên đối tượng có thể chọn.
|
||||||
function onMove(e: maplibregl.MapLayerMouseEvent) {
|
function updateCursorFromPendingPoint() {
|
||||||
if (getMode() !== "select" && getMode() !== "replay") return;
|
cursorTimer = null;
|
||||||
|
const mode = getMode();
|
||||||
|
if (mode !== "select" && mode !== "replay" && mode !== "preview" && mode !== "replay_preview") return;
|
||||||
const selectableLayers = getSelectableLayers();
|
const selectableLayers = getSelectableLayers();
|
||||||
if (!selectableLayers.length) {
|
if (!selectableLayers.length) {
|
||||||
map.getCanvas().style.cursor = "";
|
map.getCanvas().style.cursor = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!pendingCursorPoint) return;
|
||||||
|
|
||||||
const features = map.queryRenderedFeatures(e.point, {
|
const features = map.queryRenderedFeatures([pendingCursorPoint.x, pendingCursorPoint.y], {
|
||||||
layers: selectableLayers,
|
layers: selectableLayers,
|
||||||
});
|
});
|
||||||
|
|
||||||
map.getCanvas().style.cursor = features.length ? "pointer" : "";
|
map.getCanvas().style.cursor = features.length ? "pointer" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMove(e: maplibregl.MapLayerMouseEvent) {
|
||||||
|
pendingCursorPoint = { x: e.point.x, y: e.point.y };
|
||||||
|
if (cursorTimer !== null) return;
|
||||||
|
cursorTimer = window.setTimeout(updateCursorFromPendingPoint, 40);
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectableLayers(): string[] {
|
function getSelectableLayers(): string[] {
|
||||||
const style = map.getStyle();
|
const style = map.getStyle();
|
||||||
if (!style || !style.layers) return [];
|
if (!style || !style.layers) return [];
|
||||||
@@ -198,6 +233,10 @@ export function initSelect(
|
|||||||
try {
|
try {
|
||||||
map.off("click", onClick);
|
map.off("click", onClick);
|
||||||
map.off("mousemove", onMove);
|
map.off("mousemove", onMove);
|
||||||
|
if (cursorTimer !== null) {
|
||||||
|
window.clearTimeout(cursorTimer);
|
||||||
|
cursorTimer = null;
|
||||||
|
}
|
||||||
if (hasContextActions) {
|
if (hasContextActions) {
|
||||||
map.off("contextmenu", onRightClick);
|
map.off("contextmenu", onRightClick);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export function getMigrationRouteLayers(sourceId: string, pathArrowSourceId?: st
|
|||||||
strokeColor: "#065f46",
|
strokeColor: "#065f46",
|
||||||
dasharray: [4, 3],
|
dasharray: [4, 3],
|
||||||
arrowOpacity: 0.76,
|
arrowOpacity: 0.76,
|
||||||
|
showLine: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export function getMilitaryRouteLayers(sourceId: string, pathArrowSourceId?: str
|
|||||||
strokeColor: "#7f1d1d",
|
strokeColor: "#7f1d1d",
|
||||||
width: { z1: 2.6, z4: 3.8, z6: 5 },
|
width: { z1: 2.6, z4: 3.8, z6: 5 },
|
||||||
arrowOpacity: 0.9,
|
arrowOpacity: 0.9,
|
||||||
|
showLine: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export function getRetreatRouteLayers(sourceId: string, pathArrowSourceId?: stri
|
|||||||
dasharray: [6, 3],
|
dasharray: [6, 3],
|
||||||
opacity: 0.82,
|
opacity: 0.82,
|
||||||
arrowOpacity: 0.68,
|
arrowOpacity: 0.68,
|
||||||
|
showLine: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export function getTradeRouteLayers(sourceId: string, pathArrowSourceId?: string
|
|||||||
strokeColor: "#854d0e",
|
strokeColor: "#854d0e",
|
||||||
dasharray: [5, 3],
|
dasharray: [5, 3],
|
||||||
arrowOpacity: 0.78,
|
arrowOpacity: 0.78,
|
||||||
|
showLine: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type LineGeotypeStyle = {
|
|||||||
arrowOpacity?: number;
|
arrowOpacity?: number;
|
||||||
arrowOutlineColor?: string;
|
arrowOutlineColor?: string;
|
||||||
arrowOutlineWidth?: ZoomStops;
|
arrowOutlineWidth?: ZoomStops;
|
||||||
|
showLine?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PolygonGeotypeStyle = {
|
type PolygonGeotypeStyle = {
|
||||||
@@ -64,7 +65,7 @@ export function buildLineGeotypeLayers(
|
|||||||
paint: {
|
paint: {
|
||||||
"line-color": statusColor(style.color),
|
"line-color": statusColor(style.color),
|
||||||
"line-width": widthStops(style.width ?? DEFAULT_LINE_WIDTH),
|
"line-width": widthStops(style.width ?? DEFAULT_LINE_WIDTH),
|
||||||
"line-opacity": style.opacity ?? 0.9,
|
"line-opacity": style.showLine === false ? 0 : (style.opacity ?? 0.9),
|
||||||
...(style.dasharray ? { "line-dasharray": style.dasharray } : {}),
|
...(style.dasharray ? { "line-dasharray": style.dasharray } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type UseReplayPreviewOptions = {
|
|||||||
selectedStageId: number | null;
|
selectedStageId: number | null;
|
||||||
selectedStepIndex: number | null;
|
selectedStepIndex: number | null;
|
||||||
onSelectStep: (stageId: number | null, stepIndex: number | null) => void;
|
onSelectStep: (stageId: number | null, stepIndex: number | null) => void;
|
||||||
|
setMapProjection?: (type: "globe" | "mercator") => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useReplayPreview({
|
export function useReplayPreview({
|
||||||
@@ -57,6 +58,7 @@ export function useReplayPreview({
|
|||||||
selectedStageId,
|
selectedStageId,
|
||||||
selectedStepIndex,
|
selectedStepIndex,
|
||||||
onSelectStep,
|
onSelectStep,
|
||||||
|
setMapProjection,
|
||||||
}: UseReplayPreviewOptions) {
|
}: UseReplayPreviewOptions) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [dialog, setDialog] = useState<DialogState | null>(null);
|
const [dialog, setDialog] = useState<DialogState | null>(null);
|
||||||
@@ -172,9 +174,13 @@ export function useReplayPreview({
|
|||||||
if (map) {
|
if (map) {
|
||||||
mapActions.restore_label_visibility(map, baseline.labelVisibility);
|
mapActions.restore_label_visibility(map, baseline.labelVisibility);
|
||||||
if (baseline.mapViewState) {
|
if (baseline.mapViewState) {
|
||||||
|
if (setMapProjection) {
|
||||||
|
setMapProjection(baseline.mapViewState.projection === "globe" ? "globe" : "mercator");
|
||||||
|
} else {
|
||||||
map.setProjection({
|
map.setProjection({
|
||||||
type: baseline.mapViewState.projection === "globe" ? "globe" : "mercator",
|
type: baseline.mapViewState.projection === "globe" ? "globe" : "mercator",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
mapActions.set_camera_view(map, {
|
mapActions.set_camera_view(map, {
|
||||||
center: baseline.mapViewState.center,
|
center: baseline.mapViewState.center,
|
||||||
zoom: baseline.mapViewState.zoom,
|
zoom: baseline.mapViewState.zoom,
|
||||||
@@ -184,7 +190,7 @@ export function useReplayPreview({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [getMapInstance, resetPresentation]);
|
}, [getMapInstance, resetPresentation, setMapProjection]);
|
||||||
|
|
||||||
const resetPreview = useCallback(() => {
|
const resetPreview = useCallback(() => {
|
||||||
runIdRef.current += 1;
|
runIdRef.current += 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user