map layer management
This commit is contained in:
@@ -4,24 +4,35 @@ import { useEffect, useRef, useCallback } from "react";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import { getRasterTileTemplateUrl, getVectorTileTemplateUrl } from "@/api/tiles";
|
||||
import { initDrawing } from "@/lib/drawingEngine";
|
||||
import { initSelect } from "@/lib/selectingEngine";
|
||||
import { initPoint } from "@/lib/pointEngine";
|
||||
import { createEditingEngine } from "@/lib/editingEngine";
|
||||
import { FeatureCollection, Geometry } from "@/lib/useEditorState";
|
||||
import { BACKGROUND_LAYER_OPTIONS, BackgroundLayerVisibility } from "@/lib/backgroundLayers";
|
||||
|
||||
type MapProps = {
|
||||
mode: "idle" | "draw" | "select" | "add-point";
|
||||
draft: FeatureCollection;
|
||||
backgroundVisibility: BackgroundLayerVisibility;
|
||||
onCreateFeature: (feature: FeatureCollection["features"][number]) => void;
|
||||
onDeleteFeature: (id: string | number) => void;
|
||||
onUpdateFeature: (id: string | number, geometry: Geometry) => void;
|
||||
};
|
||||
|
||||
export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onUpdateFeature }: MapProps) {
|
||||
export default function Map({
|
||||
mode,
|
||||
draft,
|
||||
backgroundVisibility,
|
||||
onCreateFeature,
|
||||
onDeleteFeature,
|
||||
onUpdateFeature,
|
||||
}: MapProps) {
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
const modeRef = useRef<MapProps["mode"]>(mode);
|
||||
const draftRef = useRef<FeatureCollection>(draft);
|
||||
const backgroundVisibilityRef = useRef<BackgroundLayerVisibility>(backgroundVisibility);
|
||||
const onCreateRef = useRef(onCreateFeature);
|
||||
const onDeleteRef = useRef(onDeleteFeature);
|
||||
const onUpdateRef = useRef(onUpdateFeature);
|
||||
@@ -36,6 +47,13 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
draftRef.current = draft;
|
||||
}, [draft]);
|
||||
|
||||
useEffect(() => {
|
||||
backgroundVisibilityRef.current = backgroundVisibility;
|
||||
const map = mapRef.current;
|
||||
if (!map || !map.isStyleLoaded()) return;
|
||||
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||
}, [backgroundVisibility]);
|
||||
|
||||
useEffect(() => {
|
||||
onCreateRef.current = onCreateFeature;
|
||||
}, [onCreateFeature]);
|
||||
@@ -86,9 +104,16 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
rasterBase: {
|
||||
type: "raster",
|
||||
tiles: [getRasterTileTemplateUrl()],
|
||||
tileSize: 256,
|
||||
minzoom: 0,
|
||||
maxzoom: 6,
|
||||
},
|
||||
base: {
|
||||
type: "vector",
|
||||
tiles: ["http://localhost:3000/tiles/{z}/{x}/{y}"],
|
||||
tiles: [getVectorTileTemplateUrl()],
|
||||
minzoom: 0,
|
||||
maxzoom: 6,
|
||||
},
|
||||
@@ -101,6 +126,33 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
"background-color": "#0b1220",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "raster-base-layer",
|
||||
type: "raster",
|
||||
source: "rasterBase",
|
||||
paint: {
|
||||
"raster-opacity": 0.92,
|
||||
"raster-resampling": "linear",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "graticules-line",
|
||||
type: "line",
|
||||
source: "base",
|
||||
"source-layer": "graticules",
|
||||
paint: {
|
||||
"line-color": "#334155",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0, 0.3,
|
||||
4, 0.6,
|
||||
6, 0.8,
|
||||
],
|
||||
"line-opacity": 0.55,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "land",
|
||||
type: "fill",
|
||||
@@ -108,6 +160,92 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
"source-layer": "land",
|
||||
paint: {
|
||||
"fill-color": "#1e293b",
|
||||
"fill-opacity": 0.25,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bg-countries-fill",
|
||||
type: "fill",
|
||||
source: "base",
|
||||
"source-layer": "countries",
|
||||
paint: {
|
||||
"fill-color": "#334155",
|
||||
"fill-opacity": 0.28,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bg-country-borders-line",
|
||||
type: "line",
|
||||
source: "base",
|
||||
"source-layer": "country_borders",
|
||||
paint: {
|
||||
"line-color": "#cbd5e1",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0, 0.2,
|
||||
4, 0.5,
|
||||
6, 1.1,
|
||||
],
|
||||
"line-opacity": 0.85,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "regions-line",
|
||||
type: "line",
|
||||
source: "base",
|
||||
"source-layer": "regions",
|
||||
paint: {
|
||||
"line-color": "#475569",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0, 0.2,
|
||||
4, 0.6,
|
||||
6, 1,
|
||||
],
|
||||
"line-opacity": 0.6,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "lakes-fill",
|
||||
type: "fill",
|
||||
source: "base",
|
||||
"source-layer": "lakes",
|
||||
paint: {
|
||||
"fill-color": "#1d4ed8",
|
||||
"fill-opacity": 0.45,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rivers-line",
|
||||
type: "line",
|
||||
source: "base",
|
||||
"source-layer": "rivers",
|
||||
paint: {
|
||||
"line-color": "#38bdf8",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0, 0.25,
|
||||
4, 0.8,
|
||||
6, 1.5,
|
||||
],
|
||||
"line-opacity": 0.85,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "geolines-line",
|
||||
type: "line",
|
||||
source: "base",
|
||||
"source-layer": "geolines",
|
||||
paint: {
|
||||
"line-color": "#94a3b8",
|
||||
"line-width": 1.2,
|
||||
"line-opacity": 0.8,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -119,6 +257,10 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
mapRef.current = map;
|
||||
|
||||
map.on("load", async () => {
|
||||
const placesMinZoom = 5;
|
||||
|
||||
applyBackgroundLayerVisibility(map, backgroundVisibilityRef.current);
|
||||
|
||||
// preview (drawing)
|
||||
map.addSource("draw-preview", {
|
||||
type: "geojson",
|
||||
@@ -224,27 +366,46 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
},
|
||||
});
|
||||
|
||||
// load icon từ /public
|
||||
map.loadImage("/point.png", (err, image) => {
|
||||
if (err) throw err;
|
||||
|
||||
if (!map.hasImage("point-icon")) {
|
||||
map.addImage("point-icon", image);
|
||||
}
|
||||
});
|
||||
|
||||
// layer
|
||||
// fallback layer so points are still visible even if icon cannot be loaded
|
||||
map.addLayer({
|
||||
id: "places-symbol",
|
||||
type: "symbol",
|
||||
id: "places-circle",
|
||||
type: "circle",
|
||||
source: "places",
|
||||
layout: {
|
||||
"icon-image": "point-icon",
|
||||
"icon-size": 0.5,
|
||||
"icon-anchor": "bottom",
|
||||
minzoom: placesMinZoom,
|
||||
paint: {
|
||||
"circle-color": "#ef4444",
|
||||
"circle-radius": 3,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-width": 1,
|
||||
},
|
||||
});
|
||||
|
||||
// load icon from /public and hide circle fallback when available
|
||||
try {
|
||||
const imageResponse = await map.loadImage("/point.png");
|
||||
if (!map.hasImage("point-icon")) {
|
||||
map.addImage("point-icon", imageResponse.data);
|
||||
}
|
||||
|
||||
if (!map.getLayer("places-symbol")) {
|
||||
map.addLayer({
|
||||
id: "places-symbol",
|
||||
type: "symbol",
|
||||
source: "places",
|
||||
minzoom: placesMinZoom,
|
||||
layout: {
|
||||
"icon-image": "point-icon",
|
||||
"icon-size": 0.25,
|
||||
"icon-anchor": "bottom",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
map.setLayoutProperty("places-circle", "visibility", "none");
|
||||
} catch (err) {
|
||||
console.warn("Failed to load point icon, using circle fallback.", err);
|
||||
}
|
||||
|
||||
// init drawing
|
||||
const cleanup = initDrawing(
|
||||
map,
|
||||
@@ -313,6 +474,20 @@ export default function Map({ mode, draft, onCreateFeature, onDeleteFeature, onU
|
||||
return <div id="map" style={{ flex: 1, height: "100vh" }} />;
|
||||
}
|
||||
|
||||
function applyBackgroundLayerVisibility(
|
||||
map: maplibregl.Map,
|
||||
visibility: BackgroundLayerVisibility
|
||||
) {
|
||||
for (const layer of BACKGROUND_LAYER_OPTIONS) {
|
||||
if (!map.getLayer(layer.id)) continue;
|
||||
map.setLayoutProperty(
|
||||
layer.id,
|
||||
"visibility",
|
||||
visibility[layer.id] ? "visible" : "none"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function splitDraftFeatures(fc: FeatureCollection) {
|
||||
const polygons = {
|
||||
type: "FeatureCollection",
|
||||
|
||||
Reference in New Issue
Block a user