satupeta-main/app/(modules)/admin/mapset-upload/_components/map/StylePreview.tsx

147 lines
4.4 KiB
TypeScript

// app/admin/upload/_components/map/StylePreview.tsx
"use client";
import React, { useEffect, useRef } from "react";
import Map from "ol/Map";
import View from "ol/View";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import WKT from "ol/format/WKT";
import Feature from "ol/Feature";
import Style from "ol/style/Style";
import Fill from "ol/style/Fill";
import Stroke from "ol/style/Stroke";
import CircleStyle from "ol/style/Circle";
import { defaults as defaultControls } from 'ol/control';
// Import Geostyler dynamic/client side only
import SldStyleParser from "geostyler-sld-parser";
import OlStyleParser from "geostyler-openlayers-parser";
// ... (Kode helper parseWKT, normalizeKey, createFeatures sama seperti sebelumnya) ...
const wkt = new WKT();
function parseWKT(str: string) {
try {
return wkt.readGeometry(str, {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857",
});
} catch (e) {
return null;
}
}
function normalizeKey(key: string) {
return key.replace(/[^a-zA-Z0-9_]/g, "_");
}
function createFeatures(data: any[]) {
if (!data) return [];
return data.map((row) => {
const geometry = parseWKT(row.geometry);
if (!geometry) return null; // Handle invalid geometry
const feat = new Feature();
Object.entries(row).forEach(([key, value]) => {
if (key === "geometry") return;
feat.set(key, value);
const normalized = normalizeKey(key).toUpperCase();
if (normalized !== key) {
feat.set(normalized, value);
}
});
feat.setGeometry(geometry);
return feat;
}).filter((f): f is Feature => f !== null);
}
const defaultStyle = new Style({
image: new CircleStyle({
radius: 4,
fill: new Fill({ color: "#3388ff" }),
stroke: new Stroke({ color: "#333", width: 1 }),
}),
stroke: new Stroke({ color: "#3388ff", width: 2 }),
fill: new Fill({ color: "rgba(51,136,255,0.5)" }),
});
const SpatialStylePreview = ({ data, styleConfig }: { data: any[], styleConfig: any }) => {
const mapRef = useRef<HTMLDivElement>(null);
const mapObj = useRef<Map | null>(null);
const vectorLayer = useRef<VectorLayer<VectorSource> | null>(null);
useEffect(() => {
if (!mapRef.current || !data) return;
if (mapObj.current) mapObj.current.setTarget(undefined);
const features = createFeatures(data);
const vectorSource = new VectorSource({ features });
vectorLayer.current = new VectorLayer({
source: vectorSource,
style: defaultStyle,
});
mapObj.current = new Map({
target: mapRef.current,
controls: defaultControls({ attribution: false, zoom: true }),
layers: [
new TileLayer({ source: new OSM() }),
vectorLayer.current,
],
view: new View({
center: [12600000, -830000], // Sesuaikan center default Indonesia
zoom: 5,
}),
});
// Auto zoom to extent
const extent = vectorSource.getExtent();
if (extent && extent[0] !== Infinity) {
mapObj.current.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
return () => { if (mapObj.current) mapObj.current.setTarget(undefined); };
}, [data]);
// Apply SLD Logic
useEffect(() => {
if (!vectorLayer.current) return;
if (!styleConfig || styleConfig.styleType !== "sld") {
vectorLayer.current.setStyle(defaultStyle);
return;
}
const applySLD = async () => {
try {
const sldParser = new SldStyleParser();
const olParser = new OlStyleParser();
const sldResult = await sldParser.readStyle(styleConfig.sldContent);
if (!sldResult.output) throw new Error("Empty style");
const olResult = await olParser.writeStyle(sldResult.output);
const olStyle = olResult.output;
if (typeof olStyle === "function") {
vectorLayer.current?.setStyle((f, r) => olStyle(f, r));
} else {
vectorLayer.current?.setStyle(olStyle);
}
vectorLayer.current?.getSource()?.changed();
} catch (err) {
console.warn("SLD parsing failed", err);
vectorLayer.current?.setStyle(defaultStyle);
}
};
applySLD();
}, [styleConfig]);
return <div ref={mapRef} className="w-full h-full rounded-lg border shadow-sm min-h-[400px]" />;
};
export default SpatialStylePreview;