perf: optimize map rendering by memoizing Map component and caching expensive geo-spatial calculations
This commit is contained in:
@@ -1456,6 +1456,16 @@ function EditorPageContent() {
|
|||||||
: EMPTY_FEATURE_COLLECTION;
|
: EMPTY_FEATURE_COLLECTION;
|
||||||
const isReplayPreviewWikiSidebarOpen = isAnyPreviewMode && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen);
|
const isReplayPreviewWikiSidebarOpen = isAnyPreviewMode && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen);
|
||||||
|
|
||||||
|
const mapFocusPadding = useMemo(() => {
|
||||||
|
if (!isAnyPreviewMode) return 96;
|
||||||
|
return {
|
||||||
|
top: 84,
|
||||||
|
right: isReplayPreviewWikiSidebarOpen ? previewSidebarWidth + 80 : 84,
|
||||||
|
bottom: 116,
|
||||||
|
left: 84,
|
||||||
|
};
|
||||||
|
}, [isAnyPreviewMode, isReplayPreviewWikiSidebarOpen, previewSidebarWidth]);
|
||||||
|
|
||||||
const handleFocusHistoricalGeometry = useCallback((payload: HistoricalGeometryFocusPayload) => {
|
const handleFocusHistoricalGeometry = useCallback((payload: HistoricalGeometryFocusPayload) => {
|
||||||
const map = getCurrentMapInstance();
|
const map = getCurrentMapInstance();
|
||||||
const geometryId = String(payload.geometry.id || "").trim();
|
const geometryId = String(payload.geometry.id || "").trim();
|
||||||
@@ -2743,11 +2753,19 @@ function EditorPageContent() {
|
|||||||
? sectionCommits.find((commit) => commit.id === projectState.head_commit_id) || null
|
? sectionCommits.find((commit) => commit.id === projectState.head_commit_id) || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const handleDeleteFeature = useCallback((id: string | number | (string | number)[]) => {
|
||||||
|
if (Array.isArray(id)) {
|
||||||
|
editor.deleteFeatures(id);
|
||||||
|
} else {
|
||||||
|
editor.deleteFeature(id);
|
||||||
|
}
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
// Tạo geometry từ map engine rồi select ngay geometry mới.
|
// Tạo geometry từ map engine rồi select ngay geometry mới.
|
||||||
const handleCreateFeature = (feature: Feature) => {
|
const handleCreateFeature = useCallback((feature: Feature) => {
|
||||||
editor.createFeature(feature);
|
editor.createFeature(feature);
|
||||||
setSelectedFeatureIds([feature.properties.id]);
|
setSelectedFeatureIds([feature.properties.id]);
|
||||||
};
|
}, [editor]);
|
||||||
|
|
||||||
// Base draft for label lookup only. It must not decide which geometry is rendered.
|
// Base draft for label lookup only. It must not decide which geometry is rendered.
|
||||||
const labelContextBaseDraft = useMemo(() => {
|
const labelContextBaseDraft = useMemo(() => {
|
||||||
@@ -2993,13 +3011,7 @@ function EditorPageContent() {
|
|||||||
onSelectFeatureIds={setSelectedFeatureIds}
|
onSelectFeatureIds={setSelectedFeatureIds}
|
||||||
onCreateFeature={handleCreateFeature}
|
onCreateFeature={handleCreateFeature}
|
||||||
onAddFeatureToProject={handleAddGlobalGeometryToProject}
|
onAddFeatureToProject={handleAddGlobalGeometryToProject}
|
||||||
onDeleteFeature={(id) => {
|
onDeleteFeature={handleDeleteFeature}
|
||||||
if (Array.isArray(id)) {
|
|
||||||
editor.deleteFeatures(id);
|
|
||||||
} else {
|
|
||||||
editor.deleteFeature(id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onHideFeature={handleHideGeometryLocal}
|
onHideFeature={handleHideGeometryLocal}
|
||||||
onUpdateFeature={editor.updateFeature}
|
onUpdateFeature={editor.updateFeature}
|
||||||
allowGeometryEditing={!isAnyPreviewMode}
|
allowGeometryEditing={!isAnyPreviewMode}
|
||||||
@@ -3024,16 +3036,7 @@ function EditorPageContent() {
|
|||||||
? (replayPreviewActiveEntity ? previewEntityFocusToken : null)
|
? (replayPreviewActiveEntity ? previewEntityFocusToken : null)
|
||||||
: geometryFocusRequest?.key ?? null
|
: geometryFocusRequest?.key ?? null
|
||||||
}
|
}
|
||||||
focusPadding={
|
focusPadding={mapFocusPadding}
|
||||||
isAnyPreviewMode
|
|
||||||
? {
|
|
||||||
top: 84,
|
|
||||||
right: isReplayPreviewWikiSidebarOpen ? previewSidebarWidth + 80 : 84,
|
|
||||||
bottom: 116,
|
|
||||||
left: 84,
|
|
||||||
}
|
|
||||||
: 96
|
|
||||||
}
|
|
||||||
imageOverlay={imageOverlay}
|
imageOverlay={imageOverlay}
|
||||||
onImageOverlayChange={setImageOverlay}
|
onImageOverlayChange={setImageOverlay}
|
||||||
onBindGeometries={handleBindGeometries}
|
onBindGeometries={handleBindGeometries}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle } from "react";
|
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle, memo } from "react";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||||
@@ -72,7 +72,7 @@ type MapProps = {
|
|||||||
onViewModeChange?: (mode: "local" | "global") => void;
|
onViewModeChange?: (mode: "local" | "global") => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Map = forwardRef<MapHandle, MapProps>(function Map({
|
const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
||||||
mode,
|
mode,
|
||||||
onSetMode,
|
onSetMode,
|
||||||
renderDraft,
|
renderDraft,
|
||||||
@@ -551,7 +551,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}));
|
||||||
|
|
||||||
export default Map;
|
export default Map;
|
||||||
|
|
||||||
|
|||||||
@@ -266,16 +266,22 @@ export function decoratePointFeaturesWithLabels(
|
|||||||
timelineYear?: number | null
|
timelineYear?: number | null
|
||||||
): FeatureCollection {
|
): FeatureCollection {
|
||||||
const getLabel = createFeatureLabelResolver(labelContext, timelineYear);
|
const getLabel = createFeatureLabelResolver(labelContext, timelineYear);
|
||||||
return {
|
let changed = false;
|
||||||
...fc,
|
const nextFeatures = fc.features.map((feature) => {
|
||||||
features: fc.features.map((feature) => ({
|
const point_label = getLabel(feature);
|
||||||
|
if (feature.properties.point_label === point_label) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
...feature,
|
...feature,
|
||||||
properties: {
|
properties: {
|
||||||
...feature.properties,
|
...feature.properties,
|
||||||
point_label: getLabel(feature),
|
point_label,
|
||||||
},
|
},
|
||||||
})),
|
};
|
||||||
};
|
});
|
||||||
|
return changed ? { ...fc, features: nextFeatures } : fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decorateLineFeaturesWithLabels(
|
export function decorateLineFeaturesWithLabels(
|
||||||
@@ -284,18 +290,26 @@ export function decorateLineFeaturesWithLabels(
|
|||||||
timelineYear?: number | null
|
timelineYear?: number | null
|
||||||
): FeatureCollection {
|
): FeatureCollection {
|
||||||
const getLabel = createFeatureLabelResolver(labelContext, timelineYear);
|
const getLabel = createFeatureLabelResolver(labelContext, timelineYear);
|
||||||
return {
|
let changed = false;
|
||||||
...fc,
|
const nextFeatures = fc.features.map((feature) => {
|
||||||
features: fc.features.map((feature) => ({
|
const line_label = isLineGeometry(feature.geometry) ? getLabel(feature) : null;
|
||||||
|
if (feature.properties.line_label === line_label) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
...feature,
|
...feature,
|
||||||
properties: {
|
properties: {
|
||||||
...feature.properties,
|
...feature.properties,
|
||||||
line_label: isLineGeometry(feature.geometry) ? getLabel(feature) : null,
|
line_label,
|
||||||
},
|
},
|
||||||
})),
|
};
|
||||||
};
|
});
|
||||||
|
return changed ? { ...fc, features: nextFeatures } : fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const polygonLabelFeaturesCache = new WeakMap<any, { label: string; feature: Feature }>();
|
||||||
|
|
||||||
export function buildPolygonLabelFeatureCollection(
|
export function buildPolygonLabelFeatureCollection(
|
||||||
fc: FeatureCollection,
|
fc: FeatureCollection,
|
||||||
labelContext: FeatureCollection = fc,
|
labelContext: FeatureCollection = fc,
|
||||||
@@ -308,10 +322,16 @@ export function buildPolygonLabelFeatureCollection(
|
|||||||
const label = getLabel(feature);
|
const label = getLabel(feature);
|
||||||
if (!label) continue;
|
if (!label) continue;
|
||||||
|
|
||||||
|
const cached = polygonLabelFeaturesCache.get(feature);
|
||||||
|
if (cached && cached.label === label) {
|
||||||
|
features.push(cached.feature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const labelPoint = getPolygonLabelPoint(feature.geometry);
|
const labelPoint = getPolygonLabelPoint(feature.geometry);
|
||||||
if (!labelPoint) continue;
|
if (!labelPoint) continue;
|
||||||
|
|
||||||
features.push({
|
const labelFeature: Feature = {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
...feature.properties,
|
...feature.properties,
|
||||||
@@ -322,7 +342,9 @@ export function buildPolygonLabelFeatureCollection(
|
|||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: labelPoint,
|
coordinates: labelPoint,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
polygonLabelFeaturesCache.set(feature, { label, feature: labelFeature });
|
||||||
|
features.push(labelFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "FeatureCollection", features };
|
return { type: "FeatureCollection", features };
|
||||||
@@ -452,16 +474,26 @@ export function getGeometryRepresentativePoint(geometry: Geometry): Coordinate |
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathArrowGeometriesCache = new WeakMap<any, Geometry[]>();
|
||||||
|
|
||||||
export function buildPathArrowFeatureCollection(fc: FeatureCollection): FeatureCollection {
|
export function buildPathArrowFeatureCollection(fc: FeatureCollection): FeatureCollection {
|
||||||
const features: Feature[] = [];
|
const features: Feature[] = [];
|
||||||
|
|
||||||
for (const feature of fc.features) {
|
for (const feature of fc.features) {
|
||||||
if (!isPathFeature(feature)) continue;
|
if (!isPathFeature(feature)) continue;
|
||||||
|
|
||||||
const coordinateGroups = getLineCoordinateGroups(feature.geometry);
|
let arrowGeometries = pathArrowGeometriesCache.get(feature.geometry);
|
||||||
for (const coordinates of coordinateGroups) {
|
if (!arrowGeometries) {
|
||||||
const geometry = buildPathArrowGeometry(coordinates);
|
arrowGeometries = [];
|
||||||
if (!geometry) continue;
|
const coordinateGroups = getLineCoordinateGroups(feature.geometry);
|
||||||
|
for (const coordinates of coordinateGroups) {
|
||||||
|
const geometry = buildPathArrowGeometry(coordinates);
|
||||||
|
if (geometry) arrowGeometries.push(geometry);
|
||||||
|
}
|
||||||
|
pathArrowGeometriesCache.set(feature.geometry, arrowGeometries);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const geometry of arrowGeometries) {
|
||||||
features.push({
|
features.push({
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: { ...feature.properties },
|
properties: { ...feature.properties },
|
||||||
@@ -986,12 +1018,16 @@ function getLineCoordinateGroups(geometry: Geometry): Coordinate[][] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPolygonLabelPoint(geometry: Geometry): Coordinate | null {
|
const polygonLabelPointCache = new WeakMap<any, Coordinate | null>();
|
||||||
if (geometry.type === "Polygon") {
|
|
||||||
return getPolygonLabelCandidate(geometry.coordinates)?.point || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geometry.type === "MultiPolygon") {
|
function getPolygonLabelPoint(geometry: Geometry): Coordinate | null {
|
||||||
|
if (polygonLabelPointCache.has(geometry)) {
|
||||||
|
return polygonLabelPointCache.get(geometry)!;
|
||||||
|
}
|
||||||
|
let result: Coordinate | null = null;
|
||||||
|
if (geometry.type === "Polygon") {
|
||||||
|
result = getPolygonLabelCandidate(geometry.coordinates)?.point || null;
|
||||||
|
} else if (geometry.type === "MultiPolygon") {
|
||||||
let best: { point: Coordinate; distance: number } | null = null;
|
let best: { point: Coordinate; distance: number } | null = null;
|
||||||
for (const polygon of geometry.coordinates) {
|
for (const polygon of geometry.coordinates) {
|
||||||
const candidate = getPolygonLabelCandidate(polygon);
|
const candidate = getPolygonLabelCandidate(polygon);
|
||||||
@@ -1000,10 +1036,10 @@ function getPolygonLabelPoint(geometry: Geometry): Coordinate | null {
|
|||||||
best = candidate;
|
best = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best?.point || null;
|
result = best?.point || null;
|
||||||
}
|
}
|
||||||
|
polygonLabelPointCache.set(geometry, result);
|
||||||
return null;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPolygonLabelCandidate(polygon: PolygonCoordinates): { point: Coordinate; distance: number } | null {
|
function getPolygonLabelCandidate(polygon: PolygonCoordinates): { point: Coordinate; distance: number } | null {
|
||||||
@@ -1074,45 +1110,39 @@ export function hashStringToColor(str: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function decorateFeaturesWithEntityColors(fc: FeatureCollection): FeatureCollection {
|
export function decorateFeaturesWithEntityColors(fc: FeatureCollection): FeatureCollection {
|
||||||
return {
|
let changed = false;
|
||||||
...fc,
|
const nextFeatures = fc.features.map((feature) => {
|
||||||
features: fc.features.map((feature) => {
|
const geomType = feature.geometry?.type;
|
||||||
const geomType = feature.geometry?.type;
|
if (geomType === "Point" || geomType === "MultiPoint") {
|
||||||
if (geomType === "Point" || geomType === "MultiPoint") {
|
// Point - giữ nguyên màu của preset/icon
|
||||||
// Point - giữ nguyên màu của preset/icon
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geomType === "LineString" || geomType === "MultiLineString") {
|
|
||||||
const entityIds = getFeatureEntityIds(feature);
|
|
||||||
if (entityIds.length > 0) {
|
|
||||||
const sortedCombined = [...entityIds].sort().join("+");
|
|
||||||
return {
|
|
||||||
...feature,
|
|
||||||
properties: {
|
|
||||||
...feature.properties,
|
|
||||||
entity_color: hashStringToColor(sortedCombined),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geomType === "Polygon" || geomType === "MultiPolygon") {
|
|
||||||
const geoId = String(feature.properties?.id || "");
|
|
||||||
if (geoId) {
|
|
||||||
return {
|
|
||||||
...feature,
|
|
||||||
properties: {
|
|
||||||
...feature.properties,
|
|
||||||
entity_color: hashStringToColor(geoId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
|
|
||||||
return feature;
|
return feature;
|
||||||
}),
|
}
|
||||||
};
|
|
||||||
|
let entity_color: string | undefined;
|
||||||
|
if (geomType === "LineString" || geomType === "MultiLineString") {
|
||||||
|
const entityIds = getFeatureEntityIds(feature);
|
||||||
|
if (entityIds.length > 0) {
|
||||||
|
const sortedCombined = [...entityIds].sort().join("+");
|
||||||
|
entity_color = hashStringToColor(sortedCombined);
|
||||||
|
}
|
||||||
|
} else if (geomType === "Polygon" || geomType === "MultiPolygon") {
|
||||||
|
const geoId = String(feature.properties?.id || "");
|
||||||
|
if (geoId) {
|
||||||
|
entity_color = hashStringToColor(geoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((feature.properties as any).entity_color === entity_color) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
properties: {
|
||||||
|
...feature.properties,
|
||||||
|
entity_color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changed ? { ...fc, features: nextFeatures } : fc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ export function useMapSync({
|
|||||||
countriesSource.setData(labeledGeometries);
|
countriesSource.setData(labeledGeometries);
|
||||||
placesSource.setData(labeledPoints);
|
placesSource.setData(labeledPoints);
|
||||||
polygonLabelSource.setData(polygonLabels);
|
polygonLabelSource.setData(polygonLabels);
|
||||||
(map.getSource(PATH_ARROW_SOURCE_ID) as maplibregl.GeoJSONSource | undefined)?.setData(pathArrowShapes);
|
|
||||||
|
const pathArrowSource = map.getSource(PATH_ARROW_SOURCE_ID) as maplibregl.GeoJSONSource | undefined;
|
||||||
|
if (pathArrowSource) {
|
||||||
|
pathArrowSource.setData(pathArrowShapes);
|
||||||
|
}
|
||||||
|
|
||||||
currentSelectedIds.forEach((id) => {
|
currentSelectedIds.forEach((id) => {
|
||||||
setSelectedFeatureState(map, id, true);
|
setSelectedFeatureState(map, id, true);
|
||||||
@@ -153,8 +157,6 @@ export function useMapSync({
|
|||||||
}
|
}
|
||||||
}, [mapRef]);
|
}, [mapRef]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const tryCenterToUserLocation = useCallback(() => {
|
const tryCenterToUserLocation = useCallback(() => {
|
||||||
if (geolocationCenteredRef.current) return;
|
if (geolocationCenteredRef.current) return;
|
||||||
if (fitToDraftBoundsRef.current) return;
|
if (fitToDraftBoundsRef.current) return;
|
||||||
@@ -256,5 +258,3 @@ export function useMapSync({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user