refactor(important): change binđing geometry to bound_with(up side down tree)
This commit is contained in:
@@ -83,7 +83,7 @@ Nguồn: `const editor = useEditorState(initialData)`
|
|||||||
|
|
||||||
- `editor.draft: FeatureCollection`
|
- `editor.draft: FeatureCollection`
|
||||||
- **Single source of truth** cho geometry đang hiển thị + chỉnh sửa.
|
- **Single source of truth** cho geometry đang hiển thị + chỉnh sửa.
|
||||||
- Map render trực tiếp từ `draft` (hoặc bản “visibleDraft” đã filter theo timeline/binding).
|
- Map render trực tiếp từ `draft` (hoặc bản “visibleDraft” đã filter theo timeline/bound_with).
|
||||||
- Đây chính là **store runtime của GEO** trong session.
|
- Đây chính là **store runtime của GEO** trong session.
|
||||||
|
|
||||||
- `editor.changes: Map<id, Change>`
|
- `editor.changes: Map<id, Change>`
|
||||||
@@ -247,7 +247,7 @@ Khi bấm **Import** một geometry từ kết quả search:
|
|||||||
3. Nếu geometry chưa có trong `editor.draft`:
|
3. Nếu geometry chưa có trong `editor.draft`:
|
||||||
- tạo `Feature` mới với `id = geometry.id`
|
- tạo `Feature` mới với `id = geometry.id`
|
||||||
- set `properties.type` từ `geo_type` (map qua `geoTypeCodeToTypeKey`)
|
- set `properties.type` từ `geo_type` (map qua `geoTypeCodeToTypeKey`)
|
||||||
- set `time_start/time_end/binding`
|
- set `time_start/time_end/bound_with`
|
||||||
- set denormalized `entity_id/entity_ids/entity_name/entity_names` để UI/joins hoạt động
|
- set denormalized `entity_id/entity_ids/entity_name/entity_names` để UI/joins hoạt động
|
||||||
4. `editor.createFeature(feature)` và auto select feature đó.
|
4. `editor.createFeature(feature)` và auto select feature đó.
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ import {
|
|||||||
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
import {
|
import {
|
||||||
getDefaultTypeIdForFeature,
|
getDefaultTypeIdForFeature,
|
||||||
normalizeFeatureBindingIds,
|
|
||||||
normalizeFeatureEntityIds,
|
normalizeFeatureEntityIds,
|
||||||
uniqueEntityIds,
|
uniqueEntityIds,
|
||||||
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||||
|
import {
|
||||||
|
getDirectGeometryChildIds,
|
||||||
|
normalizeFeatureBoundWith,
|
||||||
|
wouldCreateGeometryBoundWithCycle,
|
||||||
|
} from "@/uhm/lib/editor/geometry/geometryBinding";
|
||||||
import {
|
import {
|
||||||
buildClientEntityId,
|
buildClientEntityId,
|
||||||
mergeEntitySearchResults,
|
mergeEntitySearchResults,
|
||||||
@@ -71,7 +75,7 @@ import {
|
|||||||
isFeatureVisibleAtYear,
|
isFeatureVisibleAtYear,
|
||||||
normalizeEntitiesForCompare,
|
normalizeEntitiesForCompare,
|
||||||
normalizeEntityWikiLinksForCompare,
|
normalizeEntityWikiLinksForCompare,
|
||||||
normalizeGeoSearchBindingIds,
|
normalizeGeoSearchBoundWith,
|
||||||
normalizeGeoSearchGeometry,
|
normalizeGeoSearchGeometry,
|
||||||
normalizeReplaysForCompare,
|
normalizeReplaysForCompare,
|
||||||
normalizeWikisForCompare,
|
normalizeWikisForCompare,
|
||||||
@@ -630,11 +634,11 @@ function EditorPageContent() {
|
|||||||
return rows;
|
return rows;
|
||||||
}, [activeTimelineFilterEnabled, editor, mapRenderDraft.features]);
|
}, [activeTimelineFilterEnabled, editor, mapRenderDraft.features]);
|
||||||
|
|
||||||
// Binding ids của geometry đại diện đang chọn.
|
// Child ids bound to the selected geometry via child.properties.bound_with.
|
||||||
const selectedGeometryBindingIds = useMemo(() => {
|
const selectedGeometryChildIds = useMemo(() => {
|
||||||
if (!selectedFeature) return [];
|
if (!selectedFeature) return [];
|
||||||
return normalizeFeatureBindingIds(selectedFeature);
|
return getDirectGeometryChildIds(editor.draft, selectedFeature.properties.id);
|
||||||
}, [selectedFeature]);
|
}, [editor.draft, selectedFeature]);
|
||||||
|
|
||||||
// Choices wiki dùng trong replay actions và binding panel.
|
// Choices wiki dùng trong replay actions và binding panel.
|
||||||
const wikiChoices = useMemo(() => {
|
const wikiChoices = useMemo(() => {
|
||||||
@@ -1317,7 +1321,6 @@ function EditorPageContent() {
|
|||||||
type_key: "",
|
type_key: "",
|
||||||
time_start: "",
|
time_start: "",
|
||||||
time_end: "",
|
time_end: "",
|
||||||
binding: "",
|
|
||||||
});
|
});
|
||||||
setEntityFormStatus(null);
|
setEntityFormStatus(null);
|
||||||
lastSelectedFeatureIdRef.current = null;
|
lastSelectedFeatureIdRef.current = null;
|
||||||
@@ -1336,7 +1339,6 @@ function EditorPageContent() {
|
|||||||
type_key: nextTypeKey,
|
type_key: nextTypeKey,
|
||||||
time_start: timeStart != null ? String(timeStart) : "",
|
time_start: timeStart != null ? String(timeStart) : "",
|
||||||
time_end: timeEnd != null ? String(timeEnd) : "",
|
time_end: timeEnd != null ? String(timeEnd) : "",
|
||||||
binding: normalizeFeatureBindingIds(selectedFeature).join(", "),
|
|
||||||
});
|
});
|
||||||
// Only clear status when switching to a different geometry, not when patching metadata/bindings
|
// Only clear status when switching to a different geometry, not when patching metadata/bindings
|
||||||
// on the same selected geometry (otherwise messages will blink).
|
// on the same selected geometry (otherwise messages will blink).
|
||||||
@@ -1518,58 +1520,55 @@ function EditorPageContent() {
|
|||||||
setSelectedGeometryEntityIds,
|
setSelectedGeometryEntityIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Bind/unbind geometry id vào trường binding của selected geometry.
|
// Bind/unbind geometry con vào selected geometry qua field child.bound_with.
|
||||||
const handleToggleBindGeometryForSelectedGeometry = useCallback((geoId: string, nextChecked: boolean) => {
|
const handleToggleBindGeometryForSelectedGeometry = useCallback((geoId: string, nextChecked: boolean) => {
|
||||||
if (!selectedFeatures || selectedFeatures.length === 0) {
|
if (!selectedFeatures || selectedFeatures.length === 0) {
|
||||||
flashGeoBindingStatus("Chưa chọn geometry để bind.");
|
flashGeoBindingStatus("Chưa chọn geometry để bind.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedFeatures.length !== 1 || !selectedFeature) {
|
||||||
|
flashGeoBindingStatus("Chỉ bind geometry-geometry khi chọn đúng một geometry cha.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isMultiEditValid) {
|
if (!isMultiEditValid) {
|
||||||
flashGeoBindingStatus("Không thể bind geometry cho nhiều geometry khác loại.");
|
flashGeoBindingStatus("Không thể bind geometry cho nhiều geometry khác loại.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = String(geoId || "").trim();
|
const id = String(geoId || "").trim();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (selectedFeatures.some(f => String(f.properties.id) === id)) return;
|
const parentId = String(selectedFeature.properties.id);
|
||||||
|
if (parentId === id) return;
|
||||||
|
const childFeature = editor.draft.features.find((f) => String(f.properties.id) === id);
|
||||||
|
if (!childFeature) {
|
||||||
|
flashGeoBindingStatus("Không tìm thấy geometry con.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nextChecked && wouldCreateGeometryBoundWithCycle(editor.draft.features, id, parentId)) {
|
||||||
|
flashGeoBindingStatus("Không thể bind vì sẽ tạo vòng lặp bound_with.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsEntitySubmitting(true);
|
setIsEntitySubmitting(true);
|
||||||
flashGeoBindingStatus(null, 0);
|
flashGeoBindingStatus(null, 0);
|
||||||
try {
|
try {
|
||||||
const bindingPatches = selectedFeatures.map((feature) => {
|
const currentParentId = normalizeFeatureBoundWith(childFeature);
|
||||||
const prevBindingIds = normalizeFeatureBindingIds(feature);
|
const nextBoundWith = nextChecked
|
||||||
const has = prevBindingIds.includes(id);
|
? parentId
|
||||||
const nextBindingIds = (() => {
|
: currentParentId === parentId
|
||||||
if (nextChecked) {
|
? null
|
||||||
if (has) return prevBindingIds;
|
: currentParentId;
|
||||||
return [...prevBindingIds, id];
|
|
||||||
}
|
|
||||||
if (!has) return prevBindingIds;
|
|
||||||
return prevBindingIds.filter((x) => x !== id);
|
|
||||||
})();
|
|
||||||
return {
|
|
||||||
id: feature.properties.id,
|
|
||||||
patch: { binding: nextBindingIds },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
editor.patchFeaturePropertiesBatch(
|
editor.patchFeaturePropertiesBatch(
|
||||||
bindingPatches,
|
[{
|
||||||
|
id: childFeature.properties.id,
|
||||||
|
patch: { bound_with: nextBoundWith },
|
||||||
|
}],
|
||||||
nextChecked ? "Bind geometry vào GEO" : "Unbind geometry khỏi GEO"
|
nextChecked ? "Bind geometry vào GEO" : "Unbind geometry khỏi GEO"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assume selectedFeature (the first one) reflects the representative binding in UI
|
|
||||||
const firstFeaturePrevBindings = normalizeFeatureBindingIds(selectedFeatures[0]);
|
|
||||||
const firstFeatureHas = firstFeaturePrevBindings.includes(id);
|
|
||||||
const nextBindingIdsForUI = (() => {
|
|
||||||
if (nextChecked) return firstFeatureHas ? firstFeaturePrevBindings : [...firstFeaturePrevBindings, id];
|
|
||||||
return firstFeatureHas ? firstFeaturePrevBindings.filter(x => x !== id) : firstFeaturePrevBindings;
|
|
||||||
})();
|
|
||||||
setGeometryMetaForm((prev) => ({ ...prev, binding: nextBindingIdsForUI.join(", ") }));
|
|
||||||
flashGeoBindingStatus(
|
flashGeoBindingStatus(
|
||||||
nextChecked
|
nextChecked
|
||||||
? "Đã bind geometry vào binding. Commit khi sẵn sàng."
|
? "Đã set bound_with cho geometry con. Commit khi sẵn sàng."
|
||||||
: "Đã gỡ binding geometry. Commit khi sẵn sàng.",
|
: "Đã gỡ bound_with khỏi geometry con. Commit khi sẵn sàng.",
|
||||||
3000
|
3000
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1578,13 +1577,13 @@ function EditorPageContent() {
|
|||||||
}, [
|
}, [
|
||||||
editor,
|
editor,
|
||||||
flashGeoBindingStatus,
|
flashGeoBindingStatus,
|
||||||
|
selectedFeature,
|
||||||
selectedFeatures,
|
selectedFeatures,
|
||||||
isMultiEditValid,
|
isMultiEditValid,
|
||||||
setGeometryMetaForm,
|
|
||||||
setIsEntitySubmitting,
|
setIsEntitySubmitting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Bind nhiều geometries vào target geometry.
|
// Bind nhiều geometries con vào target geometry.
|
||||||
const handleBindGeometries = useCallback((targetId: string | number, sourceIds: (string | number)[]) => {
|
const handleBindGeometries = useCallback((targetId: string | number, sourceIds: (string | number)[]) => {
|
||||||
const idStr = String(targetId).trim();
|
const idStr = String(targetId).trim();
|
||||||
if (!idStr) return;
|
if (!idStr) return;
|
||||||
@@ -1595,23 +1594,27 @@ function EditorPageContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevBindingIds = normalizeFeatureBindingIds(targetFeature);
|
const sourceFeatures = sourceIds
|
||||||
|
.map((sourceId) => editor.draft.features.find((f) => String(f.properties.id) === String(sourceId)))
|
||||||
|
.filter((feature): feature is Feature => Boolean(feature))
|
||||||
|
.filter((feature) => String(feature.properties.id) !== idStr)
|
||||||
|
.filter((feature) => !wouldCreateGeometryBoundWithCycle(editor.draft.features, feature.properties.id, idStr));
|
||||||
|
|
||||||
// Merge prevBindingIds with sourceIds (which are strings of selected features)
|
if (!sourceFeatures.length) {
|
||||||
// filter out targetId itself (we can't bind a geometry to itself)
|
flashGeoBindingStatus("Không có geometry con hợp lệ để bind.");
|
||||||
const newSources = sourceIds.map(String).filter((x) => x !== idStr);
|
return;
|
||||||
const merged = Array.from(new Set([...prevBindingIds, ...newSources]));
|
}
|
||||||
|
|
||||||
editor.patchFeaturePropertiesBatch(
|
editor.patchFeaturePropertiesBatch(
|
||||||
[{
|
sourceFeatures.map((feature) => ({
|
||||||
id: targetFeature.properties.id,
|
id: feature.properties.id,
|
||||||
patch: { binding: merged },
|
patch: { bound_with: idStr },
|
||||||
}],
|
})),
|
||||||
"Bind các geometry đã chọn vào GEO"
|
"Bind các geometry đã chọn vào GEO"
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedFeatureIds([targetFeature.properties.id]);
|
setSelectedFeatureIds([targetFeature.properties.id]);
|
||||||
flashGeoBindingStatus(`Đã bind ${newSources.length} geometry vào GEO này. Commit khi sẵn sàng.`, 3000);
|
flashGeoBindingStatus(`Đã set bound_with cho ${sourceFeatures.length} geometry con. Commit khi sẵn sàng.`, 3000);
|
||||||
}, [editor, flashGeoBindingStatus, setSelectedFeatureIds]);
|
}, [editor, flashGeoBindingStatus, setSelectedFeatureIds]);
|
||||||
|
|
||||||
// Focus/zoom tới geometry từ binding panel; nếu geo có time_start thì kéo year filter về năm đó.
|
// Focus/zoom tới geometry từ binding panel; nếu geo có time_start thì kéo year filter về năm đó.
|
||||||
@@ -1787,7 +1790,7 @@ function EditorPageContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingIds = normalizeGeoSearchBindingIds(geo.binding);
|
const boundWith = normalizeGeoSearchBoundWith(geo.bound_with);
|
||||||
const typeKey = geo.type || null;
|
const typeKey = geo.type || null;
|
||||||
|
|
||||||
const feature: Feature = {
|
const feature: Feature = {
|
||||||
@@ -1797,7 +1800,7 @@ function EditorPageContent() {
|
|||||||
type: typeKey,
|
type: typeKey,
|
||||||
time_start: normalizeTimelineYearValue(geo.time_start),
|
time_start: normalizeTimelineYearValue(geo.time_start),
|
||||||
time_end: normalizeTimelineYearValue(geo.time_end),
|
time_end: normalizeTimelineYearValue(geo.time_end),
|
||||||
binding: bindingIds.length ? bindingIds : undefined,
|
bound_with: boundWith,
|
||||||
entity_id: entityItem.entity_id,
|
entity_id: entityItem.entity_id,
|
||||||
entity_ids: [entityItem.entity_id],
|
entity_ids: [entityItem.entity_id],
|
||||||
entity_name: (entityItem.name || "").trim() || entityItem.entity_id,
|
entity_name: (entityItem.name || "").trim() || entityItem.entity_id,
|
||||||
@@ -2345,7 +2348,7 @@ function EditorPageContent() {
|
|||||||
<GeometryBindingPanel
|
<GeometryBindingPanel
|
||||||
geometries={geometryChoices}
|
geometries={geometryChoices}
|
||||||
selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null}
|
selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null}
|
||||||
selectedGeometryBindingIds={selectedGeometryBindingIds}
|
selectedGeometryChildIds={selectedGeometryChildIds}
|
||||||
onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry}
|
onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry}
|
||||||
onFocusGeometry={handleFocusGeometryFromBindingPanel}
|
onFocusGeometry={handleFocusGeometryFromBindingPanel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-12
@@ -10,7 +10,7 @@ export type EntityGeometrySearchGeo = {
|
|||||||
id: string;
|
id: string;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
draw_geometry: Geometry;
|
draw_geometry: Geometry;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
|
|
||||||
time_start?: number | null;
|
time_start?: number | null;
|
||||||
time_end?: number | null;
|
time_end?: number | null;
|
||||||
@@ -96,7 +96,7 @@ export async function searchGeometriesByEntityName(
|
|||||||
id: geometry.id,
|
id: geometry.id,
|
||||||
type: geoTypeCodeToTypeKey(geometry.geo_type) || null,
|
type: geoTypeCodeToTypeKey(geometry.geo_type) || null,
|
||||||
draw_geometry: geometry.draw_geometry,
|
draw_geometry: geometry.draw_geometry,
|
||||||
binding: geometry.binding,
|
bound_with: normalizeBoundWith(geometry.bound_with),
|
||||||
time_start: geometry.time_start ?? null,
|
time_start: geometry.time_start ?? null,
|
||||||
time_end: geometry.time_end ?? null,
|
time_end: geometry.time_end ?? null,
|
||||||
})),
|
})),
|
||||||
@@ -108,7 +108,7 @@ type GeometryRow = {
|
|||||||
id: string;
|
id: string;
|
||||||
geo_type: number;
|
geo_type: number;
|
||||||
draw_geometry: Geometry;
|
draw_geometry: Geometry;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
|
|
||||||
time_start?: number;
|
time_start?: number;
|
||||||
time_end?: number;
|
time_end?: number;
|
||||||
@@ -127,7 +127,7 @@ function geometriesToFeatureCollection(rows: GeometryRow[]): FeatureCollection {
|
|||||||
const geometry = normalizeGeometry(row.draw_geometry);
|
const geometry = normalizeGeometry(row.draw_geometry);
|
||||||
if (!geometry) continue;
|
if (!geometry) continue;
|
||||||
|
|
||||||
const binding = normalizeBinding(row.binding);
|
const boundWith = normalizeBoundWith(row.bound_with);
|
||||||
const typeKey = geoTypeCodeToTypeKey(row.geo_type) || null;
|
const typeKey = geoTypeCodeToTypeKey(row.geo_type) || null;
|
||||||
|
|
||||||
const properties: FeatureProperties = {
|
const properties: FeatureProperties = {
|
||||||
@@ -135,7 +135,7 @@ function geometriesToFeatureCollection(rows: GeometryRow[]): FeatureCollection {
|
|||||||
type: typeKey,
|
type: typeKey,
|
||||||
time_start: row.time_start ?? null,
|
time_start: row.time_start ?? null,
|
||||||
time_end: row.time_end ?? null,
|
time_end: row.time_end ?? null,
|
||||||
binding: binding.length ? binding : undefined,
|
bound_with: boundWith,
|
||||||
};
|
};
|
||||||
|
|
||||||
features.push({
|
features.push({
|
||||||
@@ -156,11 +156,9 @@ function normalizeGeometry(value: unknown): Geometry | null {
|
|||||||
return value as Geometry;
|
return value as Geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBinding(value: unknown): string[] {
|
function normalizeBoundWith(value: unknown): string | null {
|
||||||
if (!value) return [];
|
if (value == null) return null;
|
||||||
if (Array.isArray(value)) {
|
if (typeof value !== "string" && typeof value !== "number") return null;
|
||||||
return value.map((v) => String(v)).filter((v) => v.length > 0);
|
const id = String(value).trim();
|
||||||
}
|
return id.length ? id : null;
|
||||||
// Some deployments may return binding as an object; ignore it for FE properties.binding.
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type GeometryRow = Required<Pick<GeometryChoice, "id" | "label" | "isOrphan" | "
|
|||||||
type Props = {
|
type Props = {
|
||||||
geometries: GeometryChoice[];
|
geometries: GeometryChoice[];
|
||||||
selectedGeometryId?: string | null;
|
selectedGeometryId?: string | null;
|
||||||
selectedGeometryBindingIds: string[];
|
selectedGeometryChildIds: string[];
|
||||||
onToggleBindGeometryForSelectedGeometry?: (geometryId: string, nextChecked: boolean) => void;
|
onToggleBindGeometryForSelectedGeometry?: (geometryId: string, nextChecked: boolean) => void;
|
||||||
onFocusGeometry?: (geometryId: string) => void;
|
onFocusGeometry?: (geometryId: string) => void;
|
||||||
};
|
};
|
||||||
@@ -37,7 +37,7 @@ type Props = {
|
|||||||
export default function GeometryBindingPanel({
|
export default function GeometryBindingPanel({
|
||||||
geometries,
|
geometries,
|
||||||
selectedGeometryId,
|
selectedGeometryId,
|
||||||
selectedGeometryBindingIds,
|
selectedGeometryChildIds,
|
||||||
onToggleBindGeometryForSelectedGeometry,
|
onToggleBindGeometryForSelectedGeometry,
|
||||||
onFocusGeometry,
|
onFocusGeometry,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -85,7 +85,7 @@ export default function GeometryBindingPanel({
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
}, [geometries]);
|
}, [geometries]);
|
||||||
|
|
||||||
const bindingSet = useMemo(() => new Set(selectedGeometryBindingIds || []), [selectedGeometryBindingIds]);
|
const childSet = useMemo(() => new Set(selectedGeometryChildIds || []), [selectedGeometryChildIds]);
|
||||||
const selectedGeometry = useMemo(() => {
|
const selectedGeometry = useMemo(() => {
|
||||||
if (!effectiveSelectedGeometryId) return null;
|
if (!effectiveSelectedGeometryId) return null;
|
||||||
return rows.find((g) => g.id === effectiveSelectedGeometryId) || null;
|
return rows.find((g) => g.id === effectiveSelectedGeometryId) || null;
|
||||||
@@ -94,12 +94,12 @@ export default function GeometryBindingPanel({
|
|||||||
return rows
|
return rows
|
||||||
.filter((g) => g.id !== effectiveSelectedGeometryId)
|
.filter((g) => g.id !== effectiveSelectedGeometryId)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aBound = bindingSet.has(a.id);
|
const aBound = childSet.has(a.id);
|
||||||
const bBound = bindingSet.has(b.id);
|
const bBound = childSet.has(b.id);
|
||||||
if (aBound !== bBound) return aBound ? -1 : 1;
|
if (aBound !== bBound) return aBound ? -1 : 1;
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
});
|
});
|
||||||
}, [bindingSet, effectiveSelectedGeometryId, rows]);
|
}, [childSet, effectiveSelectedGeometryId, rows]);
|
||||||
const summary = useMemo(() => {
|
const summary = useMemo(() => {
|
||||||
let orphan = 0;
|
let orphan = 0;
|
||||||
let missingTime = 0;
|
let missingTime = 0;
|
||||||
@@ -247,7 +247,7 @@ export default function GeometryBindingPanel({
|
|||||||
{collapsed ? null : selectedGeometry ? (
|
{collapsed ? null : selectedGeometry ? (
|
||||||
(() => {
|
(() => {
|
||||||
const isHidden = geometryVisibility[selectedGeometry.id] === false;
|
const isHidden = geometryVisibility[selectedGeometry.id] === false;
|
||||||
const isBound = bindingSet.has(selectedGeometry.id);
|
const isBound = childSet.has(selectedGeometry.id);
|
||||||
const title = buildGeometryTitle(selectedGeometry, isHidden, isBound);
|
const title = buildGeometryTitle(selectedGeometry, isHidden, isBound);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -306,7 +306,7 @@ export default function GeometryBindingPanel({
|
|||||||
<div style={{ marginTop: "10px", display: "grid", gap: "6px", maxHeight: 250, overflowY: "auto", paddingRight: 4 }}>
|
<div style={{ marginTop: "10px", display: "grid", gap: "6px", maxHeight: 250, overflowY: "auto", paddingRight: 4 }}>
|
||||||
{visibleRows
|
{visibleRows
|
||||||
.map((g) => {
|
.map((g) => {
|
||||||
const isBound = bindingSet.has(g.id);
|
const isBound = childSet.has(g.id);
|
||||||
const isHidden = geometryVisibility[g.id] === false;
|
const isHidden = geometryVisibility[g.id] === false;
|
||||||
const title = buildGeometryTitle(g, isHidden, isBound);
|
const title = buildGeometryTitle(g, isHidden, isBound);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -58,10 +58,8 @@ export default function SelectedGeometryPanel({
|
|||||||
geometryMetaForm.type_key,
|
geometryMetaForm.type_key,
|
||||||
geometryMetaForm.time_start,
|
geometryMetaForm.time_start,
|
||||||
geometryMetaForm.time_end,
|
geometryMetaForm.time_end,
|
||||||
geometryMetaForm.binding,
|
|
||||||
].join("|");
|
].join("|");
|
||||||
}, [
|
}, [
|
||||||
geometryMetaForm.binding,
|
|
||||||
geometryMetaForm.time_end,
|
geometryMetaForm.time_end,
|
||||||
geometryMetaForm.time_start,
|
geometryMetaForm.time_start,
|
||||||
geometryMetaForm.type_key,
|
geometryMetaForm.type_key,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { PATH_RENDER_BY_TYPE } from "@/uhm/lib/map/styles/style";
|
|||||||
import { getBackgroundRasterSourceSpecification } from "@/uhm/api/tiles";
|
import { getBackgroundRasterSourceSpecification } from "@/uhm/api/tiles";
|
||||||
import { newId } from "@/uhm/lib/utils/id";
|
import { newId } from "@/uhm/lib/utils/id";
|
||||||
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
|
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
|
||||||
|
import { normalizeBoundWithId, normalizeFeatureBoundWith } from "@/uhm/lib/editor/geometry/geometryBinding";
|
||||||
import type { EntityLabelCandidate } from "@/uhm/types/geo";
|
import type { EntityLabelCandidate } from "@/uhm/types/geo";
|
||||||
|
|
||||||
type Coordinate = [number, number];
|
type Coordinate = [number, number];
|
||||||
@@ -149,9 +150,18 @@ export function filterDraftByBinding(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const childIds = new Set<string>();
|
const childIds = new Set<string>();
|
||||||
|
const selectedChildren = new Set<string>();
|
||||||
|
const selectedParents = new Set<string>();
|
||||||
for (const feature of fc.features) {
|
for (const feature of fc.features) {
|
||||||
for (const id of normalizeBindingIds(feature.properties.binding)) {
|
const featureId = String(feature.properties.id);
|
||||||
childIds.add(id);
|
const parentId = normalizeFeatureBoundWith(feature);
|
||||||
|
if (!parentId) continue;
|
||||||
|
childIds.add(featureId);
|
||||||
|
if (selectedIds.has(parentId)) {
|
||||||
|
selectedChildren.add(featureId);
|
||||||
|
}
|
||||||
|
if (selectedIds.has(featureId)) {
|
||||||
|
selectedParents.add(parentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,21 +169,13 @@ 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 selectedChildren = new Set<string>();
|
|
||||||
for (const feature of fc.features) {
|
|
||||||
if (selectedIds.has(String(feature.properties.id))) {
|
|
||||||
for (const id of normalizeBindingIds(feature.properties.binding)) {
|
|
||||||
selectedChildren.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
if (selectedIds.has(featureId)) return true;
|
||||||
if (selectedChildren.has(featureId)) return true;
|
if (selectedChildren.has(featureId)) return true;
|
||||||
|
if (selectedParents.has(featureId)) return true;
|
||||||
return !childIds.has(featureId);
|
return !childIds.has(featureId);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -200,20 +202,6 @@ export function filterDraftByGeometryVisibility(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeBindingIds(rawBinding: unknown): string[] {
|
|
||||||
if (!Array.isArray(rawBinding)) return [];
|
|
||||||
const deduped: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const rawId of rawBinding) {
|
|
||||||
if (typeof rawId !== "string" && typeof rawId !== "number") continue;
|
|
||||||
const id = String(rawId).trim();
|
|
||||||
if (!id || seen.has(id)) continue;
|
|
||||||
seen.add(id);
|
|
||||||
deduped.push(id);
|
|
||||||
}
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function splitDraftFeatures(fc: FeatureCollection) {
|
export function splitDraftFeatures(fc: FeatureCollection) {
|
||||||
const polygons = {
|
const polygons = {
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
@@ -683,21 +671,13 @@ function createFeatureLabelResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const feature of fc.features) {
|
for (const feature of fc.features) {
|
||||||
const parentLabel = directLabelsByFeatureId.get(String(feature.properties.id));
|
|
||||||
const featureId = String(feature.properties.id);
|
const featureId = String(feature.properties.id);
|
||||||
const bindingIds = normalizeBindingIds(feature.properties.binding);
|
const parentId = normalizeBoundWithId(feature.properties.bound_with);
|
||||||
|
if (!parentId) continue;
|
||||||
|
|
||||||
|
const parentLabel = directLabelsByFeatureId.get(parentId);
|
||||||
if (parentLabel) {
|
if (parentLabel) {
|
||||||
for (const childId of bindingIds) {
|
mergeInheritedFeatureLabel(inheritedLabelsByChildId, featureId, parentLabel);
|
||||||
mergeInheritedFeatureLabel(inheritedLabelsByChildId, childId, parentLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const parentId of bindingIds) {
|
|
||||||
const linkedParentLabel = directLabelsByFeatureId.get(parentId);
|
|
||||||
if (linkedParentLabel) {
|
|
||||||
mergeInheritedFeatureLabel(inheritedLabelsByChildId, featureId, linkedParentLabel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export function useMapInteraction({
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_type_id: null,
|
entity_type_id: null,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
});
|
});
|
||||||
@@ -201,7 +201,7 @@ export function useMapInteraction({
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_type_id: null,
|
entity_type_id: null,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
});
|
});
|
||||||
@@ -223,7 +223,7 @@ export function useMapInteraction({
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_type_id: null,
|
entity_type_id: null,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
});
|
});
|
||||||
@@ -245,7 +245,7 @@ export function useMapInteraction({
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_type_id: null,
|
entity_type_id: null,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
});
|
});
|
||||||
@@ -267,7 +267,7 @@ export function useMapInteraction({
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_type_id: null,
|
entity_type_id: null,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
});
|
});
|
||||||
@@ -372,7 +372,7 @@ function buildDuplicatedFeatureShapeOnly(
|
|||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
entity_name: null,
|
entity_name: null,
|
||||||
entity_names: [],
|
entity_names: [],
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
geometry,
|
geometry,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export type FeatureProperties = {
|
|||||||
geometry_preset?: GeometryPreset | null;
|
geometry_preset?: GeometryPreset | null;
|
||||||
time_start?: number | null;
|
time_start?: number | null;
|
||||||
time_end?: number | null;
|
time_end?: number | null;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
|
|
||||||
// UI/editor-only denormalized fields.
|
// UI/editor-only denormalized fields.
|
||||||
entity_id?: string | null;
|
entity_id?: string | null;
|
||||||
@@ -103,7 +103,7 @@ export type GeometrySnapshot = {
|
|||||||
type?: string | null;
|
type?: string | null;
|
||||||
draw_geometry?: Geometry;
|
draw_geometry?: Geometry;
|
||||||
geometry?: Geometry;
|
geometry?: Geometry;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
time_start?: number | null;
|
time_start?: number | null;
|
||||||
time_end?: number | null;
|
time_end?: number | null;
|
||||||
bbox?: {
|
bbox?: {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Checklist an toàn:
|
|||||||
- `type`
|
- `type`
|
||||||
- `geometry_preset`
|
- `geometry_preset`
|
||||||
- `entity_ids`
|
- `entity_ids`
|
||||||
- `binding`
|
- `bound_with`
|
||||||
6. Kiểm tra interaction cleanup khi chuyển mode.
|
6. Kiểm tra interaction cleanup khi chuyển mode.
|
||||||
|
|
||||||
Nếu mode chưa được cleanup đúng, map rất dễ giữ preview cũ hoặc event listener cũ.
|
Nếu mode chưa được cleanup đúng, map rất dễ giữ preview cũ hoặc event listener cũ.
|
||||||
@@ -101,7 +101,7 @@ File quan trọng nhất là `editorSnapshot.ts`.
|
|||||||
|
|
||||||
- `normalizeEditorSnapshot(raw)`
|
- `normalizeEditorSnapshot(raw)`
|
||||||
- đọc payload từ backend
|
- đọc payload từ backend
|
||||||
- rehydrate fields UI như `entity_ids`, `entity_name`, `binding`, `time_start`, `time_end`
|
- rehydrate fields UI như `entity_ids`, `entity_name`, `bound_with`, `time_start`, `time_end`
|
||||||
- `buildEditorSnapshot(options)`
|
- `buildEditorSnapshot(options)`
|
||||||
- strip các field generate-only khỏi `editor_feature_collection`
|
- strip các field generate-only khỏi `editor_feature_collection`
|
||||||
- build `geometry_entity[]` và `entity_wiki[]`
|
- build `geometry_entity[]` và `entity_wiki[]`
|
||||||
@@ -175,7 +175,7 @@ Có thể do:
|
|||||||
|
|
||||||
- timeline filter
|
- timeline filter
|
||||||
- geometry visibility theo type
|
- geometry visibility theo type
|
||||||
- binding filter
|
- bound_with filter
|
||||||
|
|
||||||
Không phải lúc nào cũng là bug render layer.
|
Không phải lúc nào cũng là bug render layer.
|
||||||
|
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ Các link entity-wiki hiện tại của snapshot. Snapshot builder sẽ tự si
|
|||||||
|
|
||||||
Join table persist quan hệ geometry-entity trong snapshot commit. `feature.properties.entity_ids` chỉ là field denormalized cho UI.
|
Join table persist quan hệ geometry-entity trong snapshot commit. `feature.properties.entity_ids` chỉ là field denormalized cho UI.
|
||||||
|
|
||||||
### `binding`
|
### `bound_with`
|
||||||
|
|
||||||
Field geometry-geometry binding trên feature. Binding này không tính là entity binding; geometry không có `entity_ids/entity_id` hợp lệ vẫn là orphan.
|
Field geometry-geometry trên feature con, lưu id geometry cha mà nó nằm trong. `bound_with` không tính là entity binding; geometry không có `entity_ids/entity_id` hợp lệ vẫn là orphan.
|
||||||
|
|
||||||
### `geometryVisibility`
|
### `geometryVisibility`
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ Map local visibility override. Key có thể là geometry id hoặc semantic geo
|
|||||||
|
|
||||||
### `applyGeometryBindingFilter`
|
### `applyGeometryBindingFilter`
|
||||||
|
|
||||||
Filter map theo selection/binding. Chỉ ảnh hưởng render trên map, không đổi draft và không đi snapshot.
|
Filter map theo selection/bound_with. Chỉ ảnh hưởng render trên map, không đổi draft và không đi snapshot.
|
||||||
|
|
||||||
## Guard rails
|
## Guard rails
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Geometry mới mặc định có:
|
|||||||
- `type: "country"`
|
- `type: "country"`
|
||||||
- `geometry_preset: "polygon"`
|
- `geometry_preset: "polygon"`
|
||||||
- `entity_ids: []`
|
- `entity_ids: []`
|
||||||
- `binding: []`
|
- `bound_with: null`
|
||||||
|
|
||||||
### Point (`add-point`)
|
### Point (`add-point`)
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ Khi đang ở `select`, editor có thể sửa polygon/circle qua `editingEngine
|
|||||||
- `time_start`
|
- `time_start`
|
||||||
- `time_end`
|
- `time_end`
|
||||||
|
|
||||||
`binding` đang được hiển thị trong state form nhưng không có input edit trực tiếp trong panel; việc bind/unbind geometry hiện đi qua `GeometryBindingPanel`.
|
`bound_with` không nằm trong form metadata; việc bind/unbind geometry hiện đi qua `GeometryBindingPanel`.
|
||||||
|
|
||||||
Các ràng buộc đang có:
|
Các ràng buộc đang có:
|
||||||
|
|
||||||
@@ -203,12 +203,12 @@ Panel `ProjectEntityRefsPanel` là nơi bind/unbind entity theo geometry đang c
|
|||||||
|
|
||||||
### Geometry ↔ Geometry
|
### Geometry ↔ Geometry
|
||||||
|
|
||||||
`GeometryBindingPanel` thao tác trên `feature.properties.binding`.
|
`GeometryBindingPanel` thao tác trên `feature.properties.bound_with` của geometry con.
|
||||||
|
|
||||||
- Chọn một geometry làm gốc.
|
- Chọn một geometry làm gốc.
|
||||||
- Bind/unbind với geometry khác trong project.
|
- Bind/unbind với geometry khác trong project bằng cách set/clear `bound_with` của geometry con.
|
||||||
- Có nút focus để zoom vào geometry trong list binding.
|
- Có nút focus để zoom vào geometry trong list binding.
|
||||||
- Có toggle `Filter`: map chỉ hiển thị geometry liên quan tới selection nếu filter binding đang bật.
|
- Có toggle `Filter`: map chỉ hiển thị geometry liên quan tới selection nếu filter bound_with đang bật.
|
||||||
- Row geometry hiển thị chip trạng thái trong panel:
|
- Row geometry hiển thị chip trạng thái trong panel:
|
||||||
- `no entity` nếu geometry chưa bind entity.
|
- `no entity` nếu geometry chưa bind entity.
|
||||||
- `no time` nếu thiếu cả `time_start` và `time_end`.
|
- `no time` nếu thiếu cả `time_start` và `time_end`.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Checklist này dùng sau mỗi lần sửa editor. Không thay thế typecheck/l
|
|||||||
- Chọn một geometry, bind geometry khác trong `GeometryBindingPanel`.
|
- Chọn một geometry, bind geometry khác trong `GeometryBindingPanel`.
|
||||||
- Panel hiện chip `bound` cho geometry liên quan.
|
- Panel hiện chip `bound` cho geometry liên quan.
|
||||||
- Toggle Filter: map chỉ hiện selection, selected children và parent/root phù hợp.
|
- Toggle Filter: map chỉ hiện selection, selected children và parent/root phù hợp.
|
||||||
- Undo bind/unbind geometry phải khôi phục `properties.binding`.
|
- Undo bind/unbind geometry phải khôi phục `properties.bound_with`.
|
||||||
- Bind geometry-geometry không làm mất chip `no entity` nếu geometry vẫn chưa bind entity.
|
- Bind geometry-geometry không làm mất chip `no entity` nếu geometry vẫn chưa bind entity.
|
||||||
|
|
||||||
## 6. Wiki và entity-wiki
|
## 6. Wiki và entity-wiki
|
||||||
@@ -69,7 +69,7 @@ Checklist này dùng sau mỗi lần sửa editor. Không thay thế typecheck/l
|
|||||||
## 7. Replay
|
## 7. Replay
|
||||||
|
|
||||||
- Chọn geometry có entity, bấm replay.
|
- Chọn geometry có entity, bấm replay.
|
||||||
- Replay mở với MAIN geo và các target ids liên quan binding.
|
- Replay mở với MAIN geo và các target ids có `bound_with` trỏ tới MAIN.
|
||||||
- Tạo stage, tạo step, đổi duration.
|
- Tạo stage, tạo step, đổi duration.
|
||||||
- Thêm narrative action `set_title` và `set_descriptions`.
|
- Thêm narrative action `set_title` và `set_descriptions`.
|
||||||
- Thêm map action `set_time_filter`, `show_labels`, `hide_labels`.
|
- Thêm map action `set_time_filter`, `show_labels`, `hide_labels`.
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ Một thao tác không cần undo nếu nó chỉ đổi trạng thái xem/đi
|
|||||||
| Ẩn/hiện geometry local | Eye button, map hide callback | `geometryVisibility` | Không | Không | Local UI only, không đi snapshot |
|
| Ẩn/hiện geometry local | Eye button, map hide callback | `geometryVisibility` | Không | Không | Local UI only, không đi snapshot |
|
||||||
| Geometry status panel | `GeometryBindingPanel` | Derived từ draft/timeline/visibility | Không | Không | Hiện `no entity`, `no time`, `partial time`, `timeline`, `out timeline`, `hidden`, `bound`, `new`; ID chỉ nằm trong tooltip |
|
| Geometry status panel | `GeometryBindingPanel` | Derived từ draft/timeline/visibility | Không | Không | Hiện `no entity`, `no time`, `partial time`, `timeline`, `out timeline`, `hidden`, `bound`, `new`; ID chỉ nằm trong tooltip |
|
||||||
|
|
||||||
## 3. Geometry binding
|
## 3. Geometry bound_with
|
||||||
|
|
||||||
| Thao tác | Entry point | State đổi | Undo | Snapshot/commit | Ghi chú |
|
| Thao tác | Entry point | State đổi | Undo | Snapshot/commit | Ghi chú |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| Bind entity vào selected geometry | `ProjectEntityRefsPanel` checkbox | `entity_id`, `entity_ids`, `entity_name`, `entity_names` trên selected features | `properties` hoặc `group` | `geometry_entity[]` | Multi-select chỉ hợp lệ khi cùng shape type |
|
| Bind entity vào selected geometry | `ProjectEntityRefsPanel` checkbox | `entity_id`, `entity_ids`, `entity_name`, `entity_names` trên selected features | `properties` hoặc `group` | `geometry_entity[]` | Multi-select chỉ hợp lệ khi cùng shape type |
|
||||||
| Unbind entity | `ProjectEntityRefsPanel` checkbox | Các field entity trên feature | `properties` hoặc `group` | `geometry_entity[]` delete delta nếu baseline có link | Commit/submit chặn geometry không còn entity |
|
| Unbind entity | `ProjectEntityRefsPanel` checkbox | Các field entity trên feature | `properties` hoặc `group` | `geometry_entity[]` delete delta nếu baseline có link | Commit/submit chặn geometry không còn entity |
|
||||||
| Bind geometry-geometry | `GeometryBindingPanel` lock button | `feature.properties.binding` | `properties` hoặc `group` | `geometries[].binding` | Binding geometry không thay thế entity binding |
|
| Bind geometry-geometry | `GeometryBindingPanel` lock button | `child.properties.bound_with = selectedGeometryId` | `properties` | `geometries[].bound_with` | Geometry con lưu id cha; không thay thế entity binding |
|
||||||
| Unbind geometry-geometry | `GeometryBindingPanel` unlock button | `feature.properties.binding` | `properties` hoặc `group` | `geometries[].binding` | Không ảnh hưởng `geometry_entity[]` |
|
| Unbind geometry-geometry | `GeometryBindingPanel` unlock button | `child.properties.bound_with = null` | `properties` | `geometries[].bound_with` | Không ảnh hưởng `geometry_entity[]` |
|
||||||
| Bind nhiều geometry vào target | Map bind callback | `binding` của target geometry | `properties` | `geometries[].binding` | Tự bỏ target id khỏi source ids |
|
| Bind nhiều geometry vào target | Map bind callback | `bound_with` của từng source geometry | `properties` hoặc `group` | `geometries[].bound_with` | Tự bỏ target id khỏi source ids và chặn cycle |
|
||||||
| Toggle binding filter | `GeometryBindingPanel` filter checkbox | `geometryBindingFilterEnabled` | Không | Không | Chỉ lọc hiển thị map theo selection/binding |
|
| Toggle binding filter | `GeometryBindingPanel` filter checkbox | `geometryBindingFilterEnabled` | Không | Không | Chỉ lọc hiển thị map theo selection/bound_with |
|
||||||
|
|
||||||
## 4. Entity snapshot
|
## 4. Entity snapshot
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ Khi một thao tác cần đi vào commit, kiểm output snapshot:
|
|||||||
- Wiki rows nằm trong `wikis[]`.
|
- Wiki rows nằm trong `wikis[]`.
|
||||||
- Entity-wiki rows nằm trong `entity_wiki[]`.
|
- Entity-wiki rows nằm trong `entity_wiki[]`.
|
||||||
- Replay script nằm trong `replays[]`, không lưu `replayDraft`.
|
- Replay script nằm trong `replays[]`, không lưu `replayDraft`.
|
||||||
- Generate-only fields trên feature như `entity_id`, `entity_ids`, `entity_name`, `entity_names`, `entity_label_candidates`, `time_start`, `time_end`, `binding`, `type` được snapshot builder xử lý/loại bỏ đúng chỗ trước API payload.
|
- Generate-only fields trên feature như `entity_id`, `entity_ids`, `entity_name`, `entity_names`, `entity_label_candidates`, `time_start`, `time_end`, `bound_with`, `type` được snapshot builder xử lý/loại bỏ đúng chỗ trước API payload.
|
||||||
|
|
||||||
## 12. Các thao tác cần audit lại nếu editor đổi lớn
|
## 12. Các thao tác cần audit lại nếu editor đổi lớn
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ Mỗi feature trong `mainDraft.features` sinh một row:
|
|||||||
| `operation` | `"create"`, `"update"` hoặc `"reference"` theo baseline/diff |
|
| `operation` | `"create"`, `"update"` hoặc `"reference"` theo baseline/diff |
|
||||||
| `type` | FE type key trước `toApiEditorSnapshot()`, backend code string sau normalize API |
|
| `type` | FE type key trước `toApiEditorSnapshot()`, backend code string sau normalize API |
|
||||||
| `draw_geometry` | `feature.geometry` |
|
| `draw_geometry` | `feature.geometry` |
|
||||||
| `binding` | `normalizeFeatureBindingIds(feature)` |
|
| `bound_with` | `normalizeFeatureBoundWith(feature)` |
|
||||||
| `time_start` / `time_end` | `feature.properties.time_start/time_end ?? null` |
|
| `time_start` / `time_end` | `feature.properties.time_start/time_end ?? null` |
|
||||||
| `bbox` | BBox tính từ geometry, hoặc `null` |
|
| `bbox` | BBox tính từ geometry, hoặc `null` |
|
||||||
|
|
||||||
|
Snapshot legacy có `binding: string[]` trên geometry cha được FE migrate khi load bằng cách invert sang `bound_with` trên từng geometry con.
|
||||||
|
|
||||||
Geometry đã bị xóa sinh row:
|
Geometry đã bị xóa sinh row:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -93,7 +95,7 @@ Geometry đã bị xóa sinh row:
|
|||||||
- `type`
|
- `type`
|
||||||
- `time_start`
|
- `time_start`
|
||||||
- `time_end`
|
- `time_end`
|
||||||
- `binding`
|
- `bound_with`
|
||||||
- `entity_id`
|
- `entity_id`
|
||||||
- `entity_ids`
|
- `entity_ids`
|
||||||
- `entity_name`
|
- `entity_name`
|
||||||
@@ -103,7 +105,7 @@ Geometry đã bị xóa sinh row:
|
|||||||
|
|
||||||
Các field này được lưu ở collection chuẩn hơn:
|
Các field này được lưu ở collection chuẩn hơn:
|
||||||
|
|
||||||
- `type/time/binding` nằm ở `geometries[]`.
|
- `type/time/bound_with` nằm ở `geometries[]`.
|
||||||
- entity relation nằm ở `geometry_entity[]`.
|
- entity relation nằm ở `geometry_entity[]`.
|
||||||
- entity label/name được hydrate lại từ `entities[]` và join table khi load.
|
- entity label/name được hydrate lại từ `entities[]` và join table khi load.
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ Build rule:
|
|||||||
|
|
||||||
Rows được dedupe/sort theo `geometry_id`, rồi `entity_id`.
|
Rows được dedupe/sort theo `geometry_id`, rồi `entity_id`.
|
||||||
|
|
||||||
Commit/submit hiện chặn nếu có geometry không có entity ids hợp lệ. Geometry-geometry `binding` không được tính là đã bind entity.
|
Commit/submit hiện chặn nếu có geometry không có entity ids hợp lệ. Geometry-geometry `bound_with` không được tính là đã bind entity.
|
||||||
|
|
||||||
## 6. Entity contract
|
## 6. Entity contract
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Replay seed mới có dạng:
|
|||||||
|
|
||||||
- MAIN geo
|
- MAIN geo
|
||||||
- toàn bộ bulk selection hiện tại
|
- toàn bộ bulk selection hiện tại
|
||||||
- toàn bộ `binding` của MAIN geo trong `mainDraft`
|
- toàn bộ geometry con có `bound_with` trỏ tới MAIN geo trong `mainDraft`
|
||||||
|
|
||||||
Rule hiện tại:
|
Rule hiện tại:
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ Hydrate hiện tại:
|
|||||||
|
|
||||||
- lấy feature từ `mainDraft` theo đúng thứ tự `target_geometry_ids`
|
- lấy feature từ `mainDraft` theo đúng thứ tự `target_geometry_ids`
|
||||||
- clone ra `FeatureCollection` mới
|
- clone ra `FeatureCollection` mới
|
||||||
- flatten `binding` thành `[]` để các geo trong replay bình đẳng với nhau
|
- flatten `bound_with` thành `null` để các geo trong replay bình đẳng với nhau
|
||||||
|
|
||||||
## 6. Trong replay mode map đang đọc gì
|
## 6. Trong replay mode map đang đọc gì
|
||||||
|
|
||||||
|
|||||||
@@ -116,9 +116,8 @@ Editor hiện có `undo`, nhưng chưa có redo.
|
|||||||
- `type_key`
|
- `type_key`
|
||||||
- `time_start`
|
- `time_start`
|
||||||
- `time_end`
|
- `time_end`
|
||||||
- `binding`
|
|
||||||
|
|
||||||
`geometryMetaForm.binding` hiện chủ yếu là giá trị hiển thị/đồng bộ UI, còn chỉnh sửa binding thật đi qua `GeometryBindingPanel`.
|
Geometry-geometry bound state không nằm trong `geometryMetaForm`; `GeometryBindingPanel` chỉnh trực tiếp `feature.properties.bound_with` của geometry con.
|
||||||
|
|
||||||
### 4.3. Replay state
|
### 4.3. Replay state
|
||||||
|
|
||||||
@@ -324,5 +323,5 @@ Hiệu ứng:
|
|||||||
- có `undo`, chưa có `redo`
|
- có `undo`, chưa có `redo`
|
||||||
- timeline state có `timelineYear`, nhưng page hiện dùng `timelineDraftYear` cho filtering
|
- timeline state có `timelineYear`, nhưng page hiện dùng `timelineDraftYear` cho filtering
|
||||||
- dirty count của commit không tương ứng một-một với số mutation backend
|
- dirty count của commit không tương ứng một-một với số mutation backend
|
||||||
- map selection, binding filter và timeline filter đều là state client-side
|
- map selection, bound_with filter và timeline filter đều là state client-side
|
||||||
- trạng thái orphan/time/timeline trong `GeometryBindingPanel` là derived từ draft + visibility, không phải field persist riêng
|
- trạng thái orphan/time/timeline trong `GeometryBindingPanel` là derived từ draft + visibility, không phải field persist riêng
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ FE không còn export/persist cả `FeatureCollection` riêng của replay nữa
|
|||||||
|
|
||||||
- geo MAIN
|
- geo MAIN
|
||||||
- các geo được đưa vào replay từ bulk select
|
- các geo được đưa vào replay từ bulk select
|
||||||
- binding của MAIN geo
|
- các geometry con có `bound_with` trỏ tới MAIN geo
|
||||||
|
|
||||||
Khi mở replay, FE sẽ hydrate lại `replayDraft` từ:
|
Khi mở replay, FE sẽ hydrate lại `replayDraft` từ:
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ Source này dùng cho:
|
|||||||
`useMapSync()` chịu trách nhiệm:
|
`useMapSync()` chịu trách nhiệm:
|
||||||
|
|
||||||
1. nhận `renderDraft` đã được page áp timeline/replay/preview filter trước
|
1. nhận `renderDraft` đã được page áp timeline/replay/preview filter trước
|
||||||
2. filter draft theo binding nếu `applyGeometryBindingFilter = true`
|
2. filter draft theo `bound_with` nếu `applyGeometryBindingFilter = true`
|
||||||
3. filter theo geometry visibility
|
3. filter theo geometry visibility
|
||||||
4. split feature thành nhóm polygon/line/point
|
4. split feature thành nhóm polygon/line/point
|
||||||
5. decorate line/polygon/point cho label rendering
|
5. decorate line/polygon/point cho label rendering
|
||||||
@@ -100,7 +100,7 @@ Source này dùng cho:
|
|||||||
- data mà map render không phải raw `mainDraft` nguyên xi
|
- data mà map render không phải raw `mainDraft` nguyên xi
|
||||||
- `renderDraft` là nguồn quyết định geometry nào xuất hiện trên map
|
- `renderDraft` là nguồn quyết định geometry nào xuất hiện trên map
|
||||||
- `labelContextDraft` chỉ dùng để lookup label/entity name, có thể chứa geometry đã bị timeline filter ẩn, và không được dùng để quyết định render
|
- `labelContextDraft` chỉ dùng để lookup label/entity name, có thể chứa geometry đã bị timeline filter ẩn, và không được dùng để quyết định render
|
||||||
- source MapLibre cuối cùng là `renderDraft` sau khi đã qua binding filter, geometry visibility và label decoration
|
- source MapLibre cuối cùng là `renderDraft` sau khi đã qua bound_with filter, geometry visibility và label decoration
|
||||||
|
|
||||||
## 5. Map interaction layer
|
## 5. Map interaction layer
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Có ba lớp filter hiển thị trong runtime:
|
|||||||
|
|
||||||
1. background layer visibility
|
1. background layer visibility
|
||||||
2. geometry visibility theo type key từ panel phải
|
2. geometry visibility theo type key từ panel phải
|
||||||
3. binding filter / replay filter / timeline filter ở phía data trước khi set source
|
3. bound_with filter / replay filter / timeline filter ở phía data trước khi set source
|
||||||
|
|
||||||
Vì vậy khi một geometry "không hiện", có thể nguyên nhân nằm ở data filtering chứ không phải style layer.
|
Vì vậy khi một geometry "không hiện", có thể nguyên nhân nằm ở data filtering chứ không phải style layer.
|
||||||
|
|
||||||
@@ -170,6 +170,6 @@ Implementation hiện tại không làm vậy.
|
|||||||
Thay vào đó:
|
Thay vào đó:
|
||||||
|
|
||||||
- timeline filter đang chạy phía data trong `page.tsx`
|
- timeline filter đang chạy phía data trong `page.tsx`
|
||||||
- binding filter và geometry visibility cũng chủ yếu chạy trước khi set source
|
- bound_with filter và geometry visibility cũng chủ yếu chạy trước khi set source
|
||||||
|
|
||||||
Tức là phần lớn filtering là `prepare data -> set source`, không phải `add layer filter expression per year`.
|
Tức là phần lớn filtering là `prepare data -> set source`, không phải `add layer filter expression per year`.
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ và sinh ra:
|
|||||||
Các điểm quan trọng:
|
Các điểm quan trọng:
|
||||||
|
|
||||||
- geometry many-to-many với entity được persist ở `geometry_entity[]`
|
- geometry many-to-many với entity được persist ở `geometry_entity[]`
|
||||||
- denormalized fields trên feature như `entity_ids`, `entity_name`, `binding`, `time_start` sẽ bị strip khỏi `editor_feature_collection` trước khi gửi API
|
- denormalized fields trên feature như `entity_ids`, `entity_name`, `bound_with`, `time_start` sẽ bị strip khỏi `editor_feature_collection` trước khi gửi API
|
||||||
- wiki/entity/link được chuẩn hóa lại thành `reference`, `binding`, `delete`, `create`, `update` tùy baseline
|
- wiki/entity/link được chuẩn hóa lại thành `reference`, `binding`, `delete`, `create`, `update` tùy baseline
|
||||||
- replay script được persist ở `replays[]`; `replayDraft` không được gửi
|
- replay script được persist ở `replays[]`; `replayDraft` không được gửi
|
||||||
|
|
||||||
|
|||||||
@@ -126,17 +126,10 @@ export function normalizeGeoSearchGeometry(value: unknown): Geometry | null {
|
|||||||
return value as Geometry;
|
return value as Geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chuẩn hóa danh sách binding id từ API search GEO.
|
// Chuẩn hóa parent id từ API search GEO.
|
||||||
export function normalizeGeoSearchBindingIds(value: unknown): string[] {
|
export function normalizeGeoSearchBoundWith(value: unknown): string | null {
|
||||||
if (!Array.isArray(value)) return [];
|
if (value == null) return null;
|
||||||
const deduped: string[] = [];
|
if (typeof value !== "string" && typeof value !== "number") return null;
|
||||||
const seen = new Set<string>();
|
const id = String(value).trim();
|
||||||
for (const rawId of value) {
|
return id.length ? id : null;
|
||||||
if (typeof rawId !== "string" && typeof rawId !== "number") continue;
|
|
||||||
const id = String(rawId).trim();
|
|
||||||
if (!id || seen.has(id)) continue;
|
|
||||||
seen.add(id);
|
|
||||||
deduped.push(id);
|
|
||||||
}
|
|
||||||
return deduped;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Feature, FeatureCollection } from "@/uhm/types/geo";
|
||||||
|
|
||||||
|
export function normalizeBoundWithId(value: unknown): string | null {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value !== "string" && typeof value !== "number") return null;
|
||||||
|
const id = String(value).trim();
|
||||||
|
return id.length ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFeatureBoundWith(feature: Feature): string | null {
|
||||||
|
return normalizeBoundWithId(feature.properties.bound_with);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLegacyGeometryBindingIds(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
const ids: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const rawId of value) {
|
||||||
|
const id = normalizeBoundWithId(rawId);
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDirectGeometryChildIds(
|
||||||
|
fc: FeatureCollection,
|
||||||
|
parentId: string | number | null | undefined
|
||||||
|
): string[] {
|
||||||
|
const normalizedParentId = normalizeBoundWithId(parentId);
|
||||||
|
if (!normalizedParentId) return [];
|
||||||
|
|
||||||
|
return fc.features
|
||||||
|
.filter((feature) => normalizeFeatureBoundWith(feature) === normalizedParentId)
|
||||||
|
.map((feature) => String(feature.properties.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wouldCreateGeometryBoundWithCycle(
|
||||||
|
features: Feature[],
|
||||||
|
childId: string | number,
|
||||||
|
parentId: string | number
|
||||||
|
): boolean {
|
||||||
|
const normalizedChildId = normalizeBoundWithId(childId);
|
||||||
|
const normalizedParentId = normalizeBoundWithId(parentId);
|
||||||
|
if (!normalizedChildId || !normalizedParentId) return false;
|
||||||
|
|
||||||
|
const parentByChild = new Map<string, string>();
|
||||||
|
for (const feature of features) {
|
||||||
|
const id = String(feature.properties.id);
|
||||||
|
const boundWith = normalizeFeatureBoundWith(feature);
|
||||||
|
if (boundWith) parentByChild.set(id, boundWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
let cursor: string | null = normalizedParentId;
|
||||||
|
while (cursor) {
|
||||||
|
if (cursor === normalizedChildId) return true;
|
||||||
|
if (seen.has(cursor)) return false;
|
||||||
|
seen.add(cursor);
|
||||||
|
cursor = parentByChild.get(cursor) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { Feature, FeatureProperties } from "@/uhm/types/geo";
|
import type { Feature, FeatureProperties } from "@/uhm/types/geo";
|
||||||
import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
import {
|
import { normalizeFeatureBoundWith } from "@/uhm/lib/editor/geometry/geometryBinding";
|
||||||
normalizeFeatureBindingIds,
|
|
||||||
parseBindingInput,
|
|
||||||
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
|
||||||
|
|
||||||
export type GeometryMetadataPatch = {
|
export type GeometryMetadataPatch = {
|
||||||
patch: Partial<FeatureProperties>;
|
patch: Partial<FeatureProperties>;
|
||||||
@@ -18,27 +15,22 @@ export function buildGeometryMetadataPatch(form: GeometryMetaFormState): Geometr
|
|||||||
throw new Error("time_start phải <= time_end.");
|
throw new Error("time_start phải <= time_end.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingIds = parseBindingInput(form.binding);
|
|
||||||
return {
|
return {
|
||||||
patch: {
|
patch: {
|
||||||
type: typeKey.length ? typeKey : undefined,
|
type: typeKey.length ? typeKey : undefined,
|
||||||
time_start: timeStart,
|
time_start: timeStart,
|
||||||
time_end: timeEnd,
|
time_end: timeEnd,
|
||||||
binding: bindingIds,
|
|
||||||
},
|
},
|
||||||
formState: {
|
formState: {
|
||||||
type_key: typeKey,
|
type_key: typeKey,
|
||||||
time_start: timeStart != null ? String(timeStart) : "",
|
time_start: timeStart != null ? String(timeStart) : "",
|
||||||
time_end: timeEnd != null ? String(timeEnd) : "",
|
time_end: timeEnd != null ? String(timeEnd) : "",
|
||||||
binding: bindingIds.join(", "),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBindingIdsForDisplay(feature: Feature): string {
|
export function formatBoundWithForDisplay(feature: Feature): string {
|
||||||
const bindingIds = normalizeFeatureBindingIds(feature);
|
return normalizeFeatureBoundWith(feature) || "Không có";
|
||||||
if (!bindingIds.length) return "Không có";
|
|
||||||
return bindingIds.join(", ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOptionalYearInput(raw: string, fieldName: string): number | null {
|
function parseOptionalYearInput(raw: string, fieldName: string): number | null {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useFeatureCommands(options: Options) {
|
|||||||
setEntityFormStatus,
|
setEntityFormStatus,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Áp metadata GEO (type/time/binding) cho toàn bộ selectedFeatures.
|
// Áp metadata GEO (type/time) cho toàn bộ selectedFeatures.
|
||||||
const applyGeometryMetadata = useCallback(async (): Promise<{ ok: boolean; error?: string }> => {
|
const applyGeometryMetadata = useCallback(async (): Promise<{ ok: boolean; error?: string }> => {
|
||||||
if (!selectedFeatures || selectedFeatures.length === 0) {
|
if (!selectedFeatures || selectedFeatures.length === 0) {
|
||||||
const msg = "Hãy chọn ít nhất một geometry trước.";
|
const msg = "Hãy chọn ít nhất một geometry trước.";
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildEditorSnapshot,
|
buildEditorSnapshot,
|
||||||
normalizeEditorSnapshot,
|
normalizeEditorSnapshot,
|
||||||
normalizeFeatureEntityIds,
|
|
||||||
toApiEditorSnapshot,
|
toApiEditorSnapshot,
|
||||||
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||||
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
||||||
@@ -230,7 +229,7 @@ export function useProjectCommands(options: Options) {
|
|||||||
} finally {
|
} finally {
|
||||||
state.setIsSubmitting(false);
|
state.setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [options.editor.mainDraft, options.pendingSaveCount, options.store]);
|
}, [options.pendingSaveCount, options.store]);
|
||||||
|
|
||||||
const restoreCommit = useCallback(async (commitId: string) => {
|
const restoreCommit = useCallback(async (commitId: string) => {
|
||||||
const state = options.store.getState();
|
const state = options.store.getState();
|
||||||
@@ -344,7 +343,7 @@ function toEditorSessionGeometries(input: EditorSnapshot["geometries"]): Geometr
|
|||||||
type: g.type ?? undefined,
|
type: g.type ?? undefined,
|
||||||
draw_geometry: g.draw_geometry,
|
draw_geometry: g.draw_geometry,
|
||||||
geometry: g.geometry,
|
geometry: g.geometry,
|
||||||
binding: Array.isArray(g.binding) ? [...g.binding] : undefined,
|
bound_with: g.bound_with ?? null,
|
||||||
time_start: normalizeTimelineYearValue(g.time_start) ?? undefined,
|
time_start: normalizeTimelineYearValue(g.time_start) ?? undefined,
|
||||||
time_end: normalizeTimelineYearValue(g.time_end) ?? undefined,
|
time_end: normalizeTimelineYearValue(g.time_end) ?? undefined,
|
||||||
bbox: g.bbox
|
bbox: g.bbox
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export type GeometryMetaFormState = {
|
|||||||
type_key: string;
|
type_key: string;
|
||||||
time_start: string;
|
time_start: string;
|
||||||
time_end: string;
|
time_end: string;
|
||||||
binding: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PendingEntityCreate = {
|
export type PendingEntityCreate = {
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export function useEntitySessionState() {
|
|||||||
});
|
});
|
||||||
// Danh sách entity IDs đang chọn để bind vào geometry hiện tại.
|
// Danh sách entity IDs đang chọn để bind vào geometry hiện tại.
|
||||||
const [selectedGeometryEntityIds, setSelectedGeometryEntityIds] = useState<string[]>([]);
|
const [selectedGeometryEntityIds, setSelectedGeometryEntityIds] = useState<string[]>([]);
|
||||||
// Form metadata geometry (time range + binding ids).
|
// Form metadata geometry (type + time range).
|
||||||
const [geometryMetaForm, setGeometryMetaForm] = useState<GeometryMetaFormState>({
|
const [geometryMetaForm, setGeometryMetaForm] = useState<GeometryMetaFormState>({
|
||||||
type_key: "",
|
type_key: "",
|
||||||
time_start: "",
|
time_start: "",
|
||||||
time_end: "",
|
time_end: "",
|
||||||
binding: "",
|
|
||||||
});
|
});
|
||||||
// Cờ loading khi apply entity/metadata (local submit).
|
// Cờ loading khi apply entity/metadata (local submit).
|
||||||
const [isEntitySubmitting, setIsEntitySubmitting] = useState(false);
|
const [isEntitySubmitting, setIsEntitySubmitting] = useState(false);
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { DEFAULT_GEOMETRY_TYPE_ID } from "@/uhm/lib/map/geo/geometryTypeOptions"
|
|||||||
import { normalizeGeoTypeKey, typeKeyToGeoTypeCode } from "@/uhm/lib/map/geo/geoTypeMap";
|
import { normalizeGeoTypeKey, typeKeyToGeoTypeCode } from "@/uhm/lib/map/geo/geoTypeMap";
|
||||||
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
||||||
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
||||||
|
import {
|
||||||
|
normalizeBoundWithId,
|
||||||
|
normalizeFeatureBoundWith,
|
||||||
|
normalizeLegacyGeometryBindingIds,
|
||||||
|
} from "@/uhm/lib/editor/geometry/geometryBinding";
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
|
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
|
||||||
import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
||||||
@@ -55,6 +60,8 @@ interface RawGeometryRow extends UnknownRecord {
|
|||||||
geo_type?: unknown;
|
geo_type?: unknown;
|
||||||
draw_geometry?: unknown;
|
draw_geometry?: unknown;
|
||||||
geometry?: unknown;
|
geometry?: unknown;
|
||||||
|
bound_with?: unknown;
|
||||||
|
// Legacy parent->children geometry binding, migrated to child.bound_with on load.
|
||||||
binding?: unknown;
|
binding?: unknown;
|
||||||
time_start?: unknown;
|
time_start?: unknown;
|
||||||
time_end?: unknown;
|
time_end?: unknown;
|
||||||
@@ -100,6 +107,35 @@ function normalizeApiTimeFields(row: UnknownRecord): void {
|
|||||||
if ("time_end" in row) row.time_end = normalizeTimelineYearValue(row.time_end);
|
if ("time_end" in row) row.time_end = normalizeTimelineYearValue(row.time_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLegacyBoundWithByChildId(
|
||||||
|
geometriesRaw: unknown,
|
||||||
|
fc: FeatureCollection | undefined
|
||||||
|
): Map<string, string> {
|
||||||
|
const boundWithByChildId = new Map<string, string>();
|
||||||
|
|
||||||
|
const ingestParentBinding = (parentId: unknown, rawBinding: unknown) => {
|
||||||
|
const normalizedParentId = normalizeBoundWithId(parentId);
|
||||||
|
if (!normalizedParentId) return;
|
||||||
|
for (const childId of normalizeLegacyGeometryBindingIds(rawBinding)) {
|
||||||
|
if (childId === normalizedParentId || boundWithByChildId.has(childId)) continue;
|
||||||
|
boundWithByChildId.set(childId, normalizedParentId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(geometriesRaw)) {
|
||||||
|
for (const raw of geometriesRaw) {
|
||||||
|
if (!isRecord(raw)) continue;
|
||||||
|
ingestParentBinding(raw.id, raw.binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const feature of fc?.features || []) {
|
||||||
|
ingestParentBinding(feature.properties?.id, (feature.properties as unknown as UnknownRecord)?.binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundWithByChildId;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
||||||
if (!isRecord(raw)) return null;
|
if (!isRecord(raw)) return null;
|
||||||
const snapshot = raw as UnknownRecord;
|
const snapshot = raw as UnknownRecord;
|
||||||
@@ -139,6 +175,7 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const geometriesRaw = snapshot.geometries;
|
const geometriesRaw = snapshot.geometries;
|
||||||
|
const legacyBoundWithByChildId = buildLegacyBoundWithByChildId(geometriesRaw, fc);
|
||||||
const geometries: GeometrySnapshot[] | undefined = Array.isArray(geometriesRaw)
|
const geometries: GeometrySnapshot[] | undefined = Array.isArray(geometriesRaw)
|
||||||
? geometriesRaw
|
? geometriesRaw
|
||||||
.filter(isRecord)
|
.filter(isRecord)
|
||||||
@@ -153,6 +190,10 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
const hasInlineGeometry = "draw_geometry" in row || "geometry" in row;
|
const hasInlineGeometry = "draw_geometry" in row || "geometry" in row;
|
||||||
const source: "inline" | "ref" = existingSource || (refId || !hasInlineGeometry ? "ref" : "inline");
|
const source: "inline" | "ref" = existingSource || (refId || !hasInlineGeometry ? "ref" : "inline");
|
||||||
const typeKey = normalizeGeoTypeKey(row.type) || normalizeGeoTypeKey(row.geo_type);
|
const typeKey = normalizeGeoTypeKey(row.type) || normalizeGeoTypeKey(row.geo_type);
|
||||||
|
const canonicalBoundWith = "bound_with" in row ? normalizeBoundWithId(row.bound_with) : undefined;
|
||||||
|
const boundWith = canonicalBoundWith !== undefined
|
||||||
|
? canonicalBoundWith
|
||||||
|
: legacyBoundWithByChildId.get(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -161,7 +202,7 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
type: typeKey,
|
type: typeKey,
|
||||||
draw_geometry: row.draw_geometry as GeometrySnapshot["draw_geometry"],
|
draw_geometry: row.draw_geometry as GeometrySnapshot["draw_geometry"],
|
||||||
geometry: row.geometry as GeometrySnapshot["geometry"],
|
geometry: row.geometry as GeometrySnapshot["geometry"],
|
||||||
binding: Array.isArray(row.binding) ? row.binding as string[] : undefined,
|
bound_with: boundWith,
|
||||||
time_start: normalizeTimelineYearValue(row.time_start) ?? undefined,
|
time_start: normalizeTimelineYearValue(row.time_start) ?? undefined,
|
||||||
time_end: normalizeTimelineYearValue(row.time_end) ?? undefined,
|
time_end: normalizeTimelineYearValue(row.time_end) ?? undefined,
|
||||||
bbox: isRecord(row.bbox)
|
bbox: isRecord(row.bbox)
|
||||||
@@ -299,6 +340,17 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
const gid = String(feature.properties.id);
|
const gid = String(feature.properties.id);
|
||||||
const entity_ids = byGeom.get(gid) || [];
|
const entity_ids = byGeom.get(gid) || [];
|
||||||
const p = feature.properties as unknown as UnknownRecord;
|
const p = feature.properties as unknown as UnknownRecord;
|
||||||
|
const existingBoundWith = "bound_with" in p ? normalizeBoundWithId(p.bound_with) : undefined;
|
||||||
|
const migratedBoundWith = legacyBoundWithByChildId.get(gid);
|
||||||
|
if (existingBoundWith !== undefined) {
|
||||||
|
p.bound_with = existingBoundWith;
|
||||||
|
} else if (migratedBoundWith !== undefined) {
|
||||||
|
p.bound_with = migratedBoundWith;
|
||||||
|
} else {
|
||||||
|
delete p.bound_with;
|
||||||
|
}
|
||||||
|
delete p.binding;
|
||||||
|
|
||||||
const existingTimeStart = normalizeTimelineYearValue(p.time_start);
|
const existingTimeStart = normalizeTimelineYearValue(p.time_start);
|
||||||
const existingTimeEnd = normalizeTimelineYearValue(p.time_end);
|
const existingTimeEnd = normalizeTimelineYearValue(p.time_end);
|
||||||
if (existingTimeStart !== null) {
|
if (existingTimeStart !== null) {
|
||||||
@@ -351,7 +403,9 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
|| existingTypeKey
|
|| existingTypeKey
|
||||||
|| fallbackTypeKey;
|
|| fallbackTypeKey;
|
||||||
if (typeKey) p.type = typeKey;
|
if (typeKey) p.type = typeKey;
|
||||||
if (Array.isArray(geo.binding) && geo.binding.length) p.binding = geo.binding;
|
if (geo.bound_with !== undefined) {
|
||||||
|
p.bound_with = geo.bound_with;
|
||||||
|
}
|
||||||
const timeStart = normalizeTimelineYearValue(geo.time_start);
|
const timeStart = normalizeTimelineYearValue(geo.time_start);
|
||||||
const timeEnd = normalizeTimelineYearValue(geo.time_end);
|
const timeEnd = normalizeTimelineYearValue(geo.time_end);
|
||||||
if (timeStart !== null) {
|
if (timeStart !== null) {
|
||||||
@@ -519,7 +573,7 @@ export function buildEditorSnapshot(options: {
|
|||||||
source: "inline",
|
source: "inline",
|
||||||
type: typeKey,
|
type: typeKey,
|
||||||
draw_geometry: feature.geometry,
|
draw_geometry: feature.geometry,
|
||||||
binding: normalizeFeatureBindingIds(feature),
|
bound_with: normalizeFeatureBoundWith(feature),
|
||||||
time_start: timeStart,
|
time_start: timeStart,
|
||||||
time_end: timeEnd,
|
time_end: timeEnd,
|
||||||
bbox: bbox
|
bbox: bbox
|
||||||
@@ -588,6 +642,7 @@ export function buildEditorSnapshot(options: {
|
|||||||
delete p.type;
|
delete p.type;
|
||||||
delete p.time_start;
|
delete p.time_start;
|
||||||
delete p.time_end;
|
delete p.time_end;
|
||||||
|
delete p.bound_with;
|
||||||
delete p.binding;
|
delete p.binding;
|
||||||
delete p.entity_id;
|
delete p.entity_id;
|
||||||
delete p.entity_ids;
|
delete p.entity_ids;
|
||||||
@@ -1113,27 +1168,6 @@ export function normalizeFeatureEntityIds(feature: Feature): string[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFeatureBindingIds(feature: Feature): string[] {
|
|
||||||
const rawBinding = feature.properties.binding;
|
|
||||||
if (!Array.isArray(rawBinding)) return [];
|
|
||||||
return uniqueEntityIds(rawBinding
|
|
||||||
.map((id) => {
|
|
||||||
if (typeof id !== "string" && typeof id !== "number") return "";
|
|
||||||
return String(id).trim();
|
|
||||||
})
|
|
||||||
.filter((id) => id.length > 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseBindingInput(raw: string): string[] {
|
|
||||||
if (!raw.trim().length) return [];
|
|
||||||
return uniqueEntityIds(
|
|
||||||
raw
|
|
||||||
.split(/[,\n]/)
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter((item) => item.length > 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uniqueEntityIds(ids: string[]): string[] {
|
export function uniqueEntityIds(ids: string[]): string[] {
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { WikiSnapshot } from "@/uhm/types/wiki";
|
|||||||
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
||||||
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
import { newId } from "@/uhm/lib/utils/id";
|
import { normalizeFeatureBoundWith } from "@/uhm/lib/editor/geometry/geometryBinding";
|
||||||
|
|
||||||
export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo";
|
export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo";
|
||||||
export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
||||||
@@ -552,12 +552,25 @@ export function useEditorState(
|
|||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
const feature = mainDraftRef.current.features[idx];
|
const feature = mainDraftRef.current.features[idx];
|
||||||
|
const deletedId = String(feature.properties.id);
|
||||||
const nextFeatures = [...mainDraftRef.current.features];
|
const nextFeatures = [...mainDraftRef.current.features];
|
||||||
nextFeatures.splice(idx, 1);
|
nextFeatures.splice(idx, 1);
|
||||||
|
|
||||||
const undoActions: UndoAction[] = [];
|
const undoActions: UndoAction[] = [];
|
||||||
const replayUndoAction = pruneReplaysForDeletedGeometryIds([feature.properties.id], `Xóa replay theo GEO #${feature.properties.id}`);
|
const replayUndoAction = pruneReplaysForDeletedGeometryIds([feature.properties.id], `Xóa replay theo GEO #${feature.properties.id}`);
|
||||||
if (replayUndoAction) undoActions.push(replayUndoAction);
|
if (replayUndoAction) undoActions.push(replayUndoAction);
|
||||||
|
for (let i = 0; i < nextFeatures.length; i += 1) {
|
||||||
|
if (normalizeFeatureBoundWith(nextFeatures[i]) !== deletedId) continue;
|
||||||
|
const prevProperties = deepClone(nextFeatures[i].properties);
|
||||||
|
nextFeatures[i] = {
|
||||||
|
...nextFeatures[i],
|
||||||
|
properties: {
|
||||||
|
...nextFeatures[i].properties,
|
||||||
|
bound_with: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
undoActions.push({ type: "properties", id: nextFeatures[i].properties.id, prevProperties });
|
||||||
|
}
|
||||||
undoActions.push({ type: "delete", feature: deepClone(feature), index: idx });
|
undoActions.push({ type: "delete", feature: deepClone(feature), index: idx });
|
||||||
pushMainUndo(
|
pushMainUndo(
|
||||||
undoActions.length === 1
|
undoActions.length === 1
|
||||||
@@ -574,28 +587,45 @@ export function useEditorState(
|
|||||||
|
|
||||||
const idsSet = new Set(ids.map(String));
|
const idsSet = new Set(ids.map(String));
|
||||||
const nextFeatures: Feature[] = [];
|
const nextFeatures: Feature[] = [];
|
||||||
const undoActions: UndoAction[] = [];
|
const deleteUndoActions: UndoAction[] = [];
|
||||||
|
const boundWithUndoActions: UndoAction[] = [];
|
||||||
|
|
||||||
mainDraftRef.current.features.forEach((feature, index) => {
|
mainDraftRef.current.features.forEach((feature, index) => {
|
||||||
if (idsSet.has(String(feature.properties.id))) {
|
if (idsSet.has(String(feature.properties.id))) {
|
||||||
undoActions.push({ type: "delete", feature: deepClone(feature), index });
|
deleteUndoActions.push({ type: "delete", feature: deepClone(feature), index });
|
||||||
|
} else {
|
||||||
|
const parentId = normalizeFeatureBoundWith(feature);
|
||||||
|
if (parentId && idsSet.has(parentId)) {
|
||||||
|
boundWithUndoActions.push({
|
||||||
|
type: "properties",
|
||||||
|
id: feature.properties.id,
|
||||||
|
prevProperties: deepClone(feature.properties),
|
||||||
|
});
|
||||||
|
nextFeatures.push({
|
||||||
|
...feature,
|
||||||
|
properties: {
|
||||||
|
...feature.properties,
|
||||||
|
bound_with: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
nextFeatures.push(feature);
|
nextFeatures.push(feature);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (undoActions.length === 0) return;
|
if (deleteUndoActions.length === 0) return;
|
||||||
|
|
||||||
const replayUndoAction = pruneReplaysForDeletedGeometryIds(ids, `Xóa replay theo ${undoActions.length} GEO`);
|
const replayUndoAction = pruneReplaysForDeletedGeometryIds(ids, `Xóa replay theo ${deleteUndoActions.length} GEO`);
|
||||||
const groupedActions = replayUndoAction
|
const groupedActions = replayUndoAction
|
||||||
? [replayUndoAction, ...undoActions.slice().reverse()]
|
? [replayUndoAction, ...boundWithUndoActions, ...deleteUndoActions.slice().reverse()]
|
||||||
: undoActions.length === 1
|
: deleteUndoActions.length === 1 && boundWithUndoActions.length === 0
|
||||||
? undoActions
|
? deleteUndoActions
|
||||||
: undoActions.slice().reverse();
|
: [...boundWithUndoActions, ...deleteUndoActions.slice().reverse()];
|
||||||
pushMainUndo(
|
pushMainUndo(
|
||||||
groupedActions.length === 1
|
groupedActions.length === 1
|
||||||
? groupedActions[0]
|
? groupedActions[0]
|
||||||
: { type: "group", label: `Xóa ${undoActions.length} geometry`, actions: groupedActions }
|
: { type: "group", label: `Xóa ${deleteUndoActions.length} geometry`, actions: groupedActions }
|
||||||
);
|
);
|
||||||
commitMainDraft({ ...mainDraftRef.current, features: nextFeatures });
|
commitMainDraft({ ...mainDraftRef.current, features: nextFeatures });
|
||||||
}
|
}
|
||||||
@@ -620,6 +650,21 @@ export function useEditorState(
|
|||||||
};
|
};
|
||||||
nextFeatures[idx] = newFeature;
|
nextFeatures[idx] = newFeature;
|
||||||
|
|
||||||
|
const boundWithUndoActions: UndoAction[] = [];
|
||||||
|
for (let i = 0; i < nextFeatures.length; i += 1) {
|
||||||
|
if (i === idx) continue;
|
||||||
|
if (normalizeFeatureBoundWith(nextFeatures[i]) !== String(oldId)) continue;
|
||||||
|
const prevProperties = deepClone(nextFeatures[i].properties);
|
||||||
|
nextFeatures[i] = {
|
||||||
|
...nextFeatures[i],
|
||||||
|
properties: {
|
||||||
|
...nextFeatures[i].properties,
|
||||||
|
bound_with: String(newId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
boundWithUndoActions.push({ type: "properties", id: nextFeatures[i].properties.id, prevProperties });
|
||||||
|
}
|
||||||
|
|
||||||
// Cập nhật replays nếu có
|
// Cập nhật replays nếu có
|
||||||
const prevReplays = replaysRef.current || [];
|
const prevReplays = replaysRef.current || [];
|
||||||
const nextReplays = prevReplays.map((replay) => {
|
const nextReplays = prevReplays.map((replay) => {
|
||||||
@@ -640,6 +685,7 @@ export function useEditorState(
|
|||||||
type: "group",
|
type: "group",
|
||||||
label: `Đổi ID GEO #${oldId} -> #${newId}`,
|
label: `Đổi ID GEO #${oldId} -> #${newId}`,
|
||||||
actions: [
|
actions: [
|
||||||
|
...boundWithUndoActions,
|
||||||
{ type: "replays", label: "Phục hồi replay cũ", prevReplays: deepClone(prevReplays) },
|
{ type: "replays", label: "Phục hồi replay cũ", prevReplays: deepClone(prevReplays) },
|
||||||
{ type: "create", id: newId },
|
{ type: "create", id: newId },
|
||||||
{ type: "delete", feature: deepClone(prevFeature), index: idx },
|
{ type: "delete", feature: deepClone(prevFeature), index: idx },
|
||||||
@@ -910,7 +956,7 @@ function createReplaySessionSeed(
|
|||||||
id: geometryId,
|
id: geometryId,
|
||||||
geometry_id: geometryId,
|
geometry_id: geometryId,
|
||||||
target_geometry_ids: buildReplaySeedTargetIds(
|
target_geometry_ids: buildReplaySeedTargetIds(
|
||||||
sourceDraft.features.find((feature) => String(feature.properties.id) === geometryId),
|
sourceDraft,
|
||||||
geometryId,
|
geometryId,
|
||||||
selectedIds
|
selectedIds
|
||||||
),
|
),
|
||||||
@@ -926,8 +972,7 @@ function normalizeReplaySessionSeed(
|
|||||||
): BattleReplay {
|
): BattleReplay {
|
||||||
const nextReplay = deepClone(replay);
|
const nextReplay = deepClone(replay);
|
||||||
nextReplay.id = geometryId;
|
nextReplay.id = geometryId;
|
||||||
const triggerFeature = sourceDraft.features.find((feature) => String(feature.properties.id) === geometryId);
|
const seedTargetIds = buildReplaySeedTargetIds(sourceDraft, geometryId, selectedIds);
|
||||||
const seedTargetIds = buildReplaySeedTargetIds(triggerFeature, geometryId, selectedIds);
|
|
||||||
nextReplay.target_geometry_ids = normalizeReplayTargetGeometryIds(
|
nextReplay.target_geometry_ids = normalizeReplayTargetGeometryIds(
|
||||||
nextReplay.target_geometry_ids,
|
nextReplay.target_geometry_ids,
|
||||||
geometryId,
|
geometryId,
|
||||||
@@ -937,7 +982,7 @@ function normalizeReplaySessionSeed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildReplaySeedTargetIds(
|
function buildReplaySeedTargetIds(
|
||||||
triggerFeature: Feature | undefined,
|
sourceDraft: FeatureCollection,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
selectedIds: (string | number)[]
|
selectedIds: (string | number)[]
|
||||||
) {
|
) {
|
||||||
@@ -958,9 +1003,9 @@ function buildReplaySeedTargetIds(
|
|||||||
pushId(rawId);
|
pushId(rawId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(triggerFeature?.properties?.binding)) {
|
for (const feature of sourceDraft.features) {
|
||||||
for (const rawId of triggerFeature.properties.binding) {
|
if (normalizeFeatureBoundWith(feature) === featureId) {
|
||||||
pushId(rawId);
|
pushId(feature.properties.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1020,7 +1065,7 @@ function sanitizeReplayFeature(feature: Feature): Feature {
|
|||||||
...feature,
|
...feature,
|
||||||
properties: {
|
properties: {
|
||||||
...feature.properties,
|
...feature.properties,
|
||||||
binding: [],
|
bound_with: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,6 @@ export function createEditorStore(options: EditorStoreOptions): EditorStoreApi {
|
|||||||
type_key: "",
|
type_key: "",
|
||||||
time_start: "",
|
time_start: "",
|
||||||
time_end: "",
|
time_end: "",
|
||||||
binding: "",
|
|
||||||
},
|
},
|
||||||
isEntitySubmitting: false,
|
isEntitySubmitting: false,
|
||||||
entityFormStatus: null,
|
entityFormStatus: null,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type FeatureProperties = {
|
|||||||
geometry_preset?: GeometryPreset | null;
|
geometry_preset?: GeometryPreset | null;
|
||||||
time_start?: number | null;
|
time_start?: number | null;
|
||||||
time_end?: number | null;
|
time_end?: number | null;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
entity_id?: string | null;
|
entity_id?: string | null;
|
||||||
entity_ids?: string[];
|
entity_ids?: string[];
|
||||||
entity_name?: string | null;
|
entity_name?: string | null;
|
||||||
@@ -60,7 +60,7 @@ export type GeometrySnapshot = {
|
|||||||
type?: string | null;
|
type?: string | null;
|
||||||
draw_geometry?: Geometry;
|
draw_geometry?: Geometry;
|
||||||
geometry?: Geometry;
|
geometry?: Geometry;
|
||||||
binding?: string[];
|
bound_with?: string | null;
|
||||||
time_start?: number | null;
|
time_start?: number | null;
|
||||||
time_end?: number | null;
|
time_end?: number | null;
|
||||||
bbox?: {
|
bbox?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user