select and addable geometry global
This commit is contained in:
@@ -63,6 +63,7 @@ import {
|
|||||||
import { FIXED_TIMELINE_RANGE, clampYearToFixedRange, normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
import { FIXED_TIMELINE_RANGE, clampYearToFixedRange, normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
||||||
import { useFeatureCommands } from "@/uhm/lib/editor/geometry/useFeatureCommands";
|
import { useFeatureCommands } from "@/uhm/lib/editor/geometry/useFeatureCommands";
|
||||||
import { deleteSubmission } from "@/uhm/api/projects";
|
import { deleteSubmission } from "@/uhm/api/projects";
|
||||||
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
import {
|
import {
|
||||||
@@ -820,6 +821,21 @@ function EditorPageContent() {
|
|||||||
globalGeometries.features,
|
globalGeometries.features,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const localFeatureIds = useMemo(() => {
|
||||||
|
const ids = new Set<string | number>();
|
||||||
|
for (const feature of editor.mainDraft.features) {
|
||||||
|
if (feature.properties?.id !== undefined && feature.properties.id !== null) {
|
||||||
|
ids.add(feature.properties.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const feature of baselineFeatureCollection.features) {
|
||||||
|
if (feature.properties?.id !== undefined && feature.properties.id !== null) {
|
||||||
|
ids.add(feature.properties.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(ids);
|
||||||
|
}, [baselineFeatureCollection.features, editor.mainDraft.features]);
|
||||||
|
|
||||||
// Danh sách feature đang chọn, map từ selectedFeatureIds sang draft hiện tại.
|
// Danh sách feature đang chọn, map từ selectedFeatureIds sang draft hiện tại.
|
||||||
const selectedFeatures = useMemo(() => {
|
const selectedFeatures = useMemo(() => {
|
||||||
if (!selectedFeatureIds || selectedFeatureIds.length === 0) return [];
|
if (!selectedFeatureIds || selectedFeatureIds.length === 0) return [];
|
||||||
@@ -2419,6 +2435,62 @@ function EditorPageContent() {
|
|||||||
setSelectedFeatureIds,
|
setSelectedFeatureIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Add geometry đang xem từ global mode vào draft local, kèm entity refs đã map được.
|
||||||
|
const handleAddGlobalGeometryToProject = useCallback((feature: Feature) => {
|
||||||
|
const geoId = String(feature?.properties?.id || "").trim();
|
||||||
|
if (!geoId) return;
|
||||||
|
|
||||||
|
const existing = editor.mainDraft.features.find((item) => String(item.properties.id) === geoId) || null;
|
||||||
|
if (existing) {
|
||||||
|
setSelectedFeatureIds([existing.properties.id]);
|
||||||
|
flashEntityFormStatus("Geometry này đã nằm trong project.", 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGlobalLoading) {
|
||||||
|
flashEntityFormStatus("Đang tải global geometry và entity mapping, thử lại sau.", 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityRefs = buildEntityRefsForFeature(feature, entities);
|
||||||
|
const entityIds = entityRefs.map((entity) => String(entity.id));
|
||||||
|
const featureClone = deepClone(feature);
|
||||||
|
const nextFeature: Feature = {
|
||||||
|
...featureClone,
|
||||||
|
properties: {
|
||||||
|
...featureClone.properties,
|
||||||
|
id: geoId,
|
||||||
|
source: "ref",
|
||||||
|
...buildFeatureEntityPatch(featureClone, entityIds, entityRefs),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const entitySnapshots = entityRefs.map(toEntityRefSnapshot);
|
||||||
|
|
||||||
|
editor.createFeatureWithSnapshotEntityRows(
|
||||||
|
nextFeature,
|
||||||
|
(prev) => mergeSnapshotEntityRefs(prev, entitySnapshots),
|
||||||
|
`Add global GEO #${geoId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entityRefs.length) {
|
||||||
|
setEntityCatalog((prev) => mergeEntityCatalogById(prev, entityRefs));
|
||||||
|
}
|
||||||
|
setSelectedFeatureIds([nextFeature.properties.id]);
|
||||||
|
flashEntityFormStatus(
|
||||||
|
entityRefs.length
|
||||||
|
? `Đã add geometry global vào project kèm ${entityRefs.length} entity. Commit khi sẵn sàng.`
|
||||||
|
: "Đã add geometry global vào project. Geometry này chưa có entity mapping.",
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
editor,
|
||||||
|
entities,
|
||||||
|
flashEntityFormStatus,
|
||||||
|
isGlobalLoading,
|
||||||
|
setEntityCatalog,
|
||||||
|
setSelectedFeatureIds,
|
||||||
|
]);
|
||||||
|
|
||||||
// Commands thao tác metadata/entity binding cho feature đang chọn.
|
// Commands thao tác metadata/entity binding cho feature đang chọn.
|
||||||
const featureCommands = useFeatureCommands({
|
const featureCommands = useFeatureCommands({
|
||||||
editor,
|
editor,
|
||||||
@@ -2826,6 +2898,7 @@ function EditorPageContent() {
|
|||||||
selectedFeatureIds={selectedFeatureIds}
|
selectedFeatureIds={selectedFeatureIds}
|
||||||
onSelectFeatureIds={setSelectedFeatureIds}
|
onSelectFeatureIds={setSelectedFeatureIds}
|
||||||
onCreateFeature={handleCreateFeature}
|
onCreateFeature={handleCreateFeature}
|
||||||
|
onAddFeatureToProject={handleAddGlobalGeometryToProject}
|
||||||
onDeleteFeature={(id) => {
|
onDeleteFeature={(id) => {
|
||||||
if (Array.isArray(id)) {
|
if (Array.isArray(id)) {
|
||||||
editor.deleteFeatures(id);
|
editor.deleteFeatures(id);
|
||||||
@@ -2870,6 +2943,7 @@ function EditorPageContent() {
|
|||||||
imageOverlay={imageOverlay}
|
imageOverlay={imageOverlay}
|
||||||
onImageOverlayChange={setImageOverlay}
|
onImageOverlayChange={setImageOverlay}
|
||||||
onBindGeometries={handleBindGeometries}
|
onBindGeometries={handleBindGeometries}
|
||||||
|
localFeatureIds={localFeatureIds}
|
||||||
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
|
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
|
||||||
isPreviewMode={isAnyPreviewMode}
|
isPreviewMode={isAnyPreviewMode}
|
||||||
onEnterPreview={!isReplayEditMode && !isAnyPreviewMode ? openViewerPreview : undefined}
|
onEnterPreview={!isReplayEditMode && !isAnyPreviewMode ? openViewerPreview : undefined}
|
||||||
@@ -3586,6 +3660,96 @@ function buildEntityLabelContextDraft(draft: FeatureCollection, entities: Entity
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEntityRefsForFeature(feature: Feature, entities: Entity[]): Entity[] {
|
||||||
|
const entityIds = normalizeFeatureEntityIds(feature);
|
||||||
|
if (!entityIds.length) return [];
|
||||||
|
|
||||||
|
const entityById = new globalThis.Map<string, Entity>();
|
||||||
|
for (const entity of entities || []) {
|
||||||
|
const id = String(entity?.id || "").trim();
|
||||||
|
if (!id) continue;
|
||||||
|
entityById.set(id, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityNames = Array.isArray(feature.properties.entity_names)
|
||||||
|
? feature.properties.entity_names
|
||||||
|
: [];
|
||||||
|
const primaryName = typeof feature.properties.entity_name === "string"
|
||||||
|
? feature.properties.entity_name.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return entityIds.map((id, index) => {
|
||||||
|
const catalogEntity = entityById.get(id);
|
||||||
|
if (catalogEntity) return catalogEntity;
|
||||||
|
|
||||||
|
const name = String(entityNames[index] || (index === 0 ? primaryName : "") || id).trim() || id;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description: null,
|
||||||
|
time_start: null,
|
||||||
|
time_end: null,
|
||||||
|
geometry_count: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntityRefSnapshot(entity: Entity): EntitySnapshot {
|
||||||
|
return {
|
||||||
|
id: String(entity.id),
|
||||||
|
source: "ref",
|
||||||
|
operation: "reference",
|
||||||
|
name: entity.name,
|
||||||
|
description: entity.description ?? null,
|
||||||
|
time_start: normalizeTimelineYearValue(entity.time_start),
|
||||||
|
time_end: normalizeTimelineYearValue(entity.time_end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSnapshotEntityRefs(prev: EntitySnapshot[], refs: EntitySnapshot[]): EntitySnapshot[] {
|
||||||
|
if (!refs.length) return prev;
|
||||||
|
|
||||||
|
const refsById = new globalThis.Map<string, EntitySnapshot>();
|
||||||
|
for (const ref of refs) {
|
||||||
|
const id = String(ref?.id || "").trim();
|
||||||
|
if (!id) continue;
|
||||||
|
refsById.set(id, ref);
|
||||||
|
}
|
||||||
|
if (!refsById.size) return prev;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const next = (prev || []).map((row) => {
|
||||||
|
const id = String(row?.id || "").trim();
|
||||||
|
if (!id || !refsById.has(id)) return row;
|
||||||
|
seen.add(id);
|
||||||
|
if (row.operation !== "delete") return row;
|
||||||
|
changed = true;
|
||||||
|
return refsById.get(id) || row;
|
||||||
|
});
|
||||||
|
|
||||||
|
const missing = Array.from(refsById.values()).filter((ref) => !seen.has(String(ref.id)));
|
||||||
|
if (missing.length) changed = true;
|
||||||
|
return changed ? [...missing, ...next] : prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeEntityCatalogById(prev: Entity[], refs: Entity[]): Entity[] {
|
||||||
|
if (!refs.length) return prev;
|
||||||
|
|
||||||
|
const byId = new globalThis.Map<string, Entity>();
|
||||||
|
for (const entity of prev || []) {
|
||||||
|
const id = String(entity?.id || "").trim();
|
||||||
|
if (!id) continue;
|
||||||
|
byId.set(id, entity);
|
||||||
|
}
|
||||||
|
for (const entity of refs) {
|
||||||
|
const id = String(entity?.id || "").trim();
|
||||||
|
if (!id) continue;
|
||||||
|
byId.set(id, entity);
|
||||||
|
}
|
||||||
|
return Array.from(byId.values());
|
||||||
|
}
|
||||||
|
|
||||||
function parseOptionalEntityYearInput(value: string, fieldName: string): number | undefined {
|
function parseOptionalEntityYearInput(value: string, fieldName: string): number | undefined {
|
||||||
const trimmed = String(value || "").trim();
|
const trimmed = String(value || "").trim();
|
||||||
if (!trimmed.length) return undefined;
|
if (!trimmed.length) return undefined;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type MapProps = {
|
|||||||
labelContextDraft?: FeatureCollection;
|
labelContextDraft?: FeatureCollection;
|
||||||
labelTimelineYear?: number | null;
|
labelTimelineYear?: number | null;
|
||||||
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
|
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
|
||||||
|
onAddFeatureToProject?: (feature: FeatureCollection["features"][number]) => void;
|
||||||
onDeleteFeature?: (id: string | number | (string | number)[]) => void;
|
onDeleteFeature?: (id: string | number | (string | number)[]) => void;
|
||||||
onHideFeature?: (id: string | number) => void;
|
onHideFeature?: (id: string | number) => void;
|
||||||
onUpdateFeature?: (id: string | number, geometry: Geometry) => void;
|
onUpdateFeature?: (id: string | number, geometry: Geometry) => void;
|
||||||
@@ -61,6 +62,7 @@ type MapProps = {
|
|||||||
imageOverlay?: MapImageOverlay | null;
|
imageOverlay?: MapImageOverlay | null;
|
||||||
onImageOverlayChange?: (overlay: MapImageOverlay) => void;
|
onImageOverlayChange?: (overlay: MapImageOverlay) => void;
|
||||||
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void;
|
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void;
|
||||||
|
localFeatureIds?: (string | number)[];
|
||||||
showViewportControls?: boolean;
|
showViewportControls?: boolean;
|
||||||
isPreviewMode?: boolean;
|
isPreviewMode?: boolean;
|
||||||
onEnterPreview?: () => void;
|
onEnterPreview?: () => void;
|
||||||
@@ -81,6 +83,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
labelContextDraft,
|
labelContextDraft,
|
||||||
labelTimelineYear,
|
labelTimelineYear,
|
||||||
onCreateFeature,
|
onCreateFeature,
|
||||||
|
onAddFeatureToProject,
|
||||||
onDeleteFeature,
|
onDeleteFeature,
|
||||||
onHideFeature,
|
onHideFeature,
|
||||||
onUpdateFeature,
|
onUpdateFeature,
|
||||||
@@ -96,6 +99,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
imageOverlay = null,
|
imageOverlay = null,
|
||||||
onImageOverlayChange,
|
onImageOverlayChange,
|
||||||
onBindGeometries,
|
onBindGeometries,
|
||||||
|
localFeatureIds,
|
||||||
showViewportControls = true,
|
showViewportControls = true,
|
||||||
isPreviewMode = false,
|
isPreviewMode = false,
|
||||||
onEnterPreview,
|
onEnterPreview,
|
||||||
@@ -116,6 +120,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
const onFeatureClickRef = useRef<MapProps["onFeatureClick"]>(onFeatureClick);
|
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 add geometry global vào project mới nhất cho context menu select.
|
||||||
|
const onAddFeatureToProjectRef = useRef<MapProps["onAddFeatureToProject"]>(onAddFeatureToProject);
|
||||||
// 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.
|
||||||
const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature);
|
const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature);
|
||||||
// Ref callback hide local mới nhất khi context menu select ẩn feature khỏi map.
|
// Ref callback hide local mới nhất khi context menu select ẩn feature khỏi map.
|
||||||
@@ -128,19 +134,23 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
const onImageOverlayChangeRef = useRef<MapProps["onImageOverlayChange"]>(onImageOverlayChange);
|
const onImageOverlayChangeRef = useRef<MapProps["onImageOverlayChange"]>(onImageOverlayChange);
|
||||||
// Ref callback bind geometry mới nhất để interaction không stale.
|
// Ref callback bind geometry mới nhất để interaction không stale.
|
||||||
const onBindGeometriesRef = useRef<MapProps["onBindGeometries"]>(onBindGeometries);
|
const onBindGeometriesRef = useRef<MapProps["onBindGeometries"]>(onBindGeometries);
|
||||||
|
// Ref danh sách geometry thuộc local project để context menu phân biệt global-only feature.
|
||||||
|
const localFeatureIdsRef = useRef<MapProps["localFeatureIds"]>(localFeatureIds);
|
||||||
|
|
||||||
useEffect(() => { modeRef.current = mode; }, [mode]);
|
useEffect(() => { modeRef.current = mode; }, [mode]);
|
||||||
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(() => { onFeatureClickRef.current = onFeatureClick; }, [onFeatureClick]);
|
useEffect(() => { onFeatureClickRef.current = onFeatureClick; }, [onFeatureClick]);
|
||||||
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
|
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
|
||||||
|
useEffect(() => { onAddFeatureToProjectRef.current = onAddFeatureToProject; }, [onAddFeatureToProject]);
|
||||||
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
|
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
|
||||||
useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]);
|
useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]);
|
||||||
useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]);
|
useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]);
|
||||||
useEffect(() => { imageOverlayRef.current = imageOverlay; }, [imageOverlay]);
|
useEffect(() => { imageOverlayRef.current = imageOverlay; }, [imageOverlay]);
|
||||||
useEffect(() => { onImageOverlayChangeRef.current = onImageOverlayChange; }, [onImageOverlayChange]);
|
useEffect(() => { onImageOverlayChangeRef.current = onImageOverlayChange; }, [onImageOverlayChange]);
|
||||||
useEffect(() => { onBindGeometriesRef.current = onBindGeometries; }, [onBindGeometries]);
|
useEffect(() => { onBindGeometriesRef.current = onBindGeometries; }, [onBindGeometries]);
|
||||||
|
useEffect(() => { localFeatureIdsRef.current = localFeatureIds; }, [localFeatureIds]);
|
||||||
|
|
||||||
// Hook sở hữu lifecycle MapLibre instance và các control camera/projection.
|
// Hook sở hữu lifecycle MapLibre instance và các control camera/projection.
|
||||||
const {
|
const {
|
||||||
@@ -189,6 +199,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onUpdateRef,
|
onUpdateRef,
|
||||||
onFeatureClickRef,
|
onFeatureClickRef,
|
||||||
onBindGeometriesRef,
|
onBindGeometriesRef,
|
||||||
|
localFeatureIdsRef,
|
||||||
|
onAddFeatureToProjectRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type UseMapInteractionProps = {
|
|||||||
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
|
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
|
||||||
onFeatureClickRef: React.MutableRefObject<((payload: MapFeaturePayload | 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>;
|
||||||
|
localFeatureIdsRef?: React.MutableRefObject<(string | number)[] | undefined>;
|
||||||
|
onAddFeatureToProjectRef?: React.MutableRefObject<((feature: FeatureCollection["features"][number]) => void) | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useMapInteraction({
|
export function useMapInteraction({
|
||||||
@@ -53,6 +55,8 @@ export function useMapInteraction({
|
|||||||
onUpdateRef,
|
onUpdateRef,
|
||||||
onFeatureClickRef,
|
onFeatureClickRef,
|
||||||
onBindGeometriesRef,
|
onBindGeometriesRef,
|
||||||
|
localFeatureIdsRef,
|
||||||
|
onAddFeatureToProjectRef,
|
||||||
}: UseMapInteractionProps) {
|
}: UseMapInteractionProps) {
|
||||||
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
|
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
|
||||||
const engineBindingsRef = useRef<Partial<Record<EditorMode, EngineBinding>>>({});
|
const engineBindingsRef = useRef<Partial<Record<EditorMode, EngineBinding>>>({});
|
||||||
@@ -199,7 +203,27 @@ export function useMapInteraction({
|
|||||||
...payload,
|
...payload,
|
||||||
feature: currentFeature,
|
feature: currentFeature,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
onAddFeatureToProjectRef?.current
|
||||||
|
? (feature) => {
|
||||||
|
const rawId = feature.id ?? feature.properties?.id;
|
||||||
|
if (rawId === undefined || rawId === null) return;
|
||||||
|
|
||||||
|
const originalFeature = renderDraftRef.current.features.find(
|
||||||
|
(item) => String(item.properties.id) === String(rawId)
|
||||||
|
);
|
||||||
|
if (!originalFeature) return;
|
||||||
|
onAddFeatureToProjectRef.current?.(originalFeature);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
onAddFeatureToProjectRef?.current
|
||||||
|
? (id) => {
|
||||||
|
if (!onAddFeatureToProjectRef?.current) return true;
|
||||||
|
const localIds = localFeatureIdsRef?.current;
|
||||||
|
if (!Array.isArray(localIds)) return true;
|
||||||
|
return localIds.some((localId) => String(localId) === String(id));
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const cleanupPoint = initPoint(
|
const cleanupPoint = initPoint(
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export function initSelect(
|
|||||||
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
|
onFeatureClick?: (payload: SelectFeatureClickPayload | null) => void,
|
||||||
|
onAddToProject?: (feature: maplibregl.MapGeoJSONFeature) => void,
|
||||||
|
isLocalFeature?: (id: string | number) => boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const FEATURE_STATE_SOURCES = [
|
const FEATURE_STATE_SOURCES = [
|
||||||
@@ -28,7 +30,7 @@ export function initSelect(
|
|||||||
"path-arrow-shapes",
|
"path-arrow-shapes",
|
||||||
] as const;
|
] as const;
|
||||||
const selectedIds = new Set<number | string>();
|
const selectedIds = new Set<number | string>();
|
||||||
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries);
|
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries || onAddToProject);
|
||||||
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 cursorTimer: number | null = null;
|
||||||
@@ -306,31 +308,48 @@ export function initSelect(
|
|||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedCount = selectedIds.size;
|
|
||||||
const effectiveCount = selectedCount || 1;
|
|
||||||
const targetId = clickedFeature.id ?? clickedFeature.properties?.id;
|
const targetId = clickedFeature.id ?? clickedFeature.properties?.id;
|
||||||
|
const hasTargetId = targetId !== undefined && targetId !== null;
|
||||||
|
const isLocalTarget = hasTargetId ? (isLocalFeature?.(targetId) ?? true) : false;
|
||||||
|
const selectedLocalIds = Array.from(selectedIds).filter((id) => isLocalFeature?.(id) ?? true);
|
||||||
|
const localActionIds = selectedLocalIds.length
|
||||||
|
? selectedLocalIds
|
||||||
|
: isLocalTarget && hasTargetId
|
||||||
|
? [targetId]
|
||||||
|
: [];
|
||||||
|
const effectiveCount = localActionIds.length;
|
||||||
const isClickOutsideSelection = !isRightClickedItemAlreadySelected && hasSelection;
|
const isClickOutsideSelection = !isRightClickedItemAlreadySelected && hasSelection;
|
||||||
|
|
||||||
type MenuItem = {
|
type MenuItem = {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
group: "edit" | "bind" | "replay" | "delete";
|
group: "add" | "edit" | "bind" | "replay" | "delete";
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: MenuItem[] = [];
|
const items: MenuItem[] = [];
|
||||||
|
|
||||||
if (isClickOutsideSelection && onBindGeometries && targetId !== undefined && targetId !== null) {
|
if (onAddToProject && hasTargetId && !isLocalTarget) {
|
||||||
const sourceIds = Array.from(selectedIds);
|
|
||||||
items.push({
|
items.push({
|
||||||
group: "bind",
|
group: "add",
|
||||||
label: `Bind ${selectedCount} geo đang chọn vào geo này`,
|
label: "Add",
|
||||||
onClick: () => {
|
onClick: () => onAddToProject(clickedFeature),
|
||||||
onBindGeometries(targetId, sourceIds);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isClickOutsideSelection) {
|
if (isClickOutsideSelection && onBindGeometries && isLocalTarget && hasTargetId) {
|
||||||
|
const sourceIds = selectedLocalIds.filter((id) => String(id) !== String(targetId));
|
||||||
|
if (sourceIds.length) {
|
||||||
|
items.push({
|
||||||
|
group: "bind",
|
||||||
|
label: `Bind ${sourceIds.length} geo đang chọn vào geo này`,
|
||||||
|
onClick: () => {
|
||||||
|
onBindGeometries(targetId, sourceIds);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalTarget && !isClickOutsideSelection) {
|
||||||
if (
|
if (
|
||||||
effectiveCount === 1 &&
|
effectiveCount === 1 &&
|
||||||
clickedFeature.source === "countries" &&
|
clickedFeature.source === "countries" &&
|
||||||
@@ -345,7 +364,7 @@ export function initSelect(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveCount === 1 && onDuplicate && targetId !== undefined && targetId !== null) {
|
if (effectiveCount === 1 && onDuplicate && hasTargetId) {
|
||||||
items.push({
|
items.push({
|
||||||
group: "edit",
|
group: "edit",
|
||||||
label: "Duplicate",
|
label: "Duplicate",
|
||||||
@@ -353,7 +372,7 @@ export function initSelect(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveCount === 1 && onHide && targetId !== undefined && targetId !== null) {
|
if (effectiveCount === 1 && onHide && hasTargetId) {
|
||||||
items.push({
|
items.push({
|
||||||
group: "edit",
|
group: "edit",
|
||||||
label: "Hide",
|
label: "Hide",
|
||||||
@@ -362,10 +381,10 @@ export function initSelect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onReplayEdit) {
|
if (isLocalTarget && onReplayEdit) {
|
||||||
const replayId = targetId;
|
const replayId = targetId;
|
||||||
if (replayId !== undefined && replayId !== null) {
|
if (replayId !== undefined && replayId !== null) {
|
||||||
const totalCount = isClickOutsideSelection ? selectedIds.size + 1 : effectiveCount;
|
const totalCount = isClickOutsideSelection ? selectedLocalIds.length + 1 : effectiveCount;
|
||||||
items.push({
|
items.push({
|
||||||
group: "replay",
|
group: "replay",
|
||||||
label: totalCount > 1 ? `Vào replay (${totalCount} geo)` : "Vào replay",
|
label: totalCount > 1 ? `Vào replay (${totalCount} geo)` : "Vào replay",
|
||||||
@@ -374,18 +393,15 @@ export function initSelect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onDelete) {
|
if (isLocalTarget && onDelete && effectiveCount > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
group: "delete",
|
group: "delete",
|
||||||
label: effectiveCount > 1 ? `Xóa ${effectiveCount} mục` : "Xóa",
|
label: effectiveCount > 1 ? `Xóa ${effectiveCount} mục` : "Xóa",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const ids = selectedIds.size
|
if (localActionIds.length === 1) {
|
||||||
? Array.from(selectedIds)
|
onDelete(localActionIds[0]);
|
||||||
: [targetId];
|
|
||||||
if (ids.length === 1) {
|
|
||||||
onDelete(ids[0]);
|
|
||||||
} else {
|
} else {
|
||||||
onDelete(ids);
|
onDelete(localActionIds);
|
||||||
}
|
}
|
||||||
clearSelection();
|
clearSelection();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user