diff --git a/addons.txt b/addons.txt new file mode 100644 index 0000000..2733fda --- /dev/null +++ b/addons.txt @@ -0,0 +1,16 @@ +xlsx +pdfjs-dist +uuid +framer-motion + + + +open-layers +geostyler-sld-parser +geostyler-openlayers-parser + + + + +npm install xlsx pdfjs-dist uuid framer-motion ol geostyler-sld-parser geostyler-openlayers-parser +npm uninstall xlsx pdfjs-dist uuid framer-motion ol geostyler-sld-parser geostyler-openlayers-parser diff --git a/app/(modules)/admin/_components/sidebar.tsx b/app/(modules)/admin/_components/sidebar.tsx index 414bd40..c810185 100644 --- a/app/(modules)/admin/_components/sidebar.tsx +++ b/app/(modules)/admin/_components/sidebar.tsx @@ -13,6 +13,7 @@ import { Key, ChartBarIncreasing, BookOpen, + FolderUp, WashingMachine, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; @@ -38,6 +39,12 @@ const menuItems: MenuItem[] = [ module: "mapset", icon: , }, + { + name: "Upload Peta", + href: "/admin/mapset-upload", + module: "mapset", + icon: , + }, { name: "Manajemen User", href: "/admin/user", diff --git a/app/(modules)/admin/mapset-upload/_components/map/CustomLayerStyle.tsx b/app/(modules)/admin/mapset-upload/_components/map/CustomLayerStyle.tsx new file mode 100644 index 0000000..dc06170 --- /dev/null +++ b/app/(modules)/admin/mapset-upload/_components/map/CustomLayerStyle.tsx @@ -0,0 +1,305 @@ +// app/admin/upload/_components/map/CustomLayerStyle.tsx +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button } from "@/shared/components/ui/button"; +import { toast } from "sonner"; + +// --- HELPER FUNCTION SLD GENERATOR --- +const sldHeader = ` + +`; +const sldFooter = ``; + +// Helper symbolizer generator +const symbolizer = (geometryType: string, color: string) => { + const geomUpper = geometryType?.toUpperCase(); + + if (geomUpper === "POINT" || geomUpper === "MULTIPOINT") { + return ` + + + + circle + + ${color} + + + #000000 + 1 + + + 10 + + + `; + } + + if (geomUpper === "LINE" || geomUpper === "LINESTRING" || geomUpper === "MULTILINESTRING") { + return ` + + + ${color} + 2 + + + `; + } + + // Polygon default + return ` + + + ${color} + 0.5 + + + #232323 + 1 + + + `; +}; + +const singleColorSLD = (color: string, geometryType: string) => `${sldHeader} + + layer + + + + ${symbolizer(geometryType, color)} + + + + +${sldFooter} +`; + +const uniqueValueSLD = (column: string, rules: any[], geometryType: string) => `${sldHeader} + + layer + + + ${rules.map(r => ` + + + + ${column} + ${r.value} + + + ${symbolizer(geometryType, r.color)} + + `).join("")} + + + +${sldFooter} +`; +const globalIconSLD = (iconCode:string) => `${sldHeader} + + layer + + + + + + + + image/png + + 10 + + + + + + +${sldFooter} +`; + +// Helper Random Color +const randomColor = () => { + let color = "#000000" + while (color === "#000000") { + color = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0"); + } + return color; +} + +interface CustomLayerStyleProps { + data: any[]; + geometryType: string; + onSubmit: (val: any) => void; + onChange?: (val: any) => void; +} + +const CustomLayerStyle = ({ data = [], geometryType, onSubmit }: CustomLayerStyleProps) => { + const [columns, setColumns] = useState([]); + const [selectedStyle, setSelectedStyle] = useState("single"); + const [singleColor, setSingleColor] = useState("#3388ff"); + const [uniqueColumn, setUniqueColumn] = useState(""); + const [uniqueRules, setUniqueRules] = useState<{value: any, color: string}[]>([]); + const [iconGlobal, setIconGlobal] = useState(""); + const [validIcon, setValidIcon] = useState(true); + + useEffect(() => { + if (data && data.length > 0) { + // Ambil keys dari row pertama, filter geometry + const keys = Object.keys(data[0]).filter((k) => k !== "geometry"); + setColumns(keys); + } + }, [data]); + + useEffect(() => { + if (!iconGlobal) return; + + const img = new Image(); + img.onload = () => setValidIcon(true); + img.onerror = () => { + setValidIcon(false); + toast.error("URL icon tidak valid, Mohon ganti."); + } + img.src = iconGlobal; + }, [iconGlobal]); + + + const generateUniqueRules = (column: string) => { + const values = [...new Set(data.map((d) => d[column]))].slice(0, 20); // Limit 20 unique values agar tidak berat + const rules = values.map((v) => ({ + value: v, + color: randomColor(), + })); + setUniqueRules(rules); + }; + + const submit = () => { + if (selectedStyle === "icon" && !validIcon) { + return + } + + let xml = ""; + if (selectedStyle === "single") { + xml = singleColorSLD(singleColor, geometryType); + } else if (selectedStyle === "unique_value") { + xml = uniqueValueSLD(uniqueColumn, uniqueRules, geometryType); + } else if (selectedStyle === "icon") { + const iconCode = 'https://cdn-icons-png.flaticon.com/512/0/614.png' + // xml = globalIconSLD(iconCode); + xml = globalIconSLD(iconGlobal); + } + + // Kirim SLD string ke parent + onSubmit({ + styleType: "sld", + sldContent: xml + }); + }; + + return ( +
+ {/*
๐ŸŽจ Pengaturan Style
*/} + +
+ + +
+ + {/* --- SINGLE COLOR --- */} + {selectedStyle === "single" && ( +
+ +
+ setSingleColor(e.target.value)} + /> + {singleColor} +
+
+ )} + + {/* --- UNIQUE VALUE --- */} + {selectedStyle === "unique_value" && ( +
+ + + + {uniqueRules.length > 0 && ( +
+

Preview Kategori (Max 20)

+ {uniqueRules.map((r, i) => ( +
+ { + const copy = [...uniqueRules]; + copy[i].color = e.target.value; + setUniqueRules(copy); + }} + /> + {String(r.value)} +
+ ))} +
+ )} +
+ )} + + {/* ---------------- ICON PER FEATURE ---------------- */} + {selectedStyle === "icon" && geometryType === "Point" && ( +
+ + setIconGlobal(e.target.value)} + /> +
+ )} + +
+ +
+
+ ); +}; + +export default CustomLayerStyle; \ No newline at end of file diff --git a/app/(modules)/admin/mapset-upload/_components/map/GeoPreview.tsx b/app/(modules)/admin/mapset-upload/_components/map/GeoPreview.tsx new file mode 100644 index 0000000..f9b27ec --- /dev/null +++ b/app/(modules)/admin/mapset-upload/_components/map/GeoPreview.tsx @@ -0,0 +1,101 @@ +// app/admin/upload/_components/map/GeoPreview.tsx +"use client"; + +import React, { useEffect, useRef } from "react"; +import Map from "ol/Map"; +import View from "ol/View"; +import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer"; +import { OSM } from "ol/source"; +import VectorSource from "ol/source/Vector"; +import WKT from "ol/format/WKT"; +import { Circle as CircleStyle, Fill, Stroke, Style } from "ol/style"; +import { fromLonLat } from "ol/proj"; + +const GeoPreview = ({ features }: { features: any[] }) => { + const mapRef = useRef(null); + const mapObj = useRef(null); + + useEffect(() => { + if (!features || features.length === 0 || !mapRef.current) return; + + // Bersihkan map lama jika ada re-render strict mode + if (mapObj.current) { + mapObj.current.setTarget(undefined); + } + + const wktFormat = new WKT(); + const vectorSource = new VectorSource(); + + features.forEach((item) => { + try { + const feature = wktFormat.readFeature(item.geometry, { + dataProjection: `EPSG:4326`, + featureProjection: "EPSG:3857", + }); + vectorSource.addFeature(feature); + } catch (err) { + console.error("WKT parse error:", err); + } + }); + + const vectorLayer = new VectorLayer({ + source: vectorSource, + style: (feature) => { + const type = feature.getGeometry()?.getType(); + + if (type === "Polygon" || type === "MultiPolygon") { + return new Style({ + fill: new Fill({ color: "rgba(0, 153, 255, 0.4)" }), + stroke: new Stroke({ color: "#0099ff", width: 2 }), + }); + } + if (type === "LineString" || type === "MultiLineString") { + return new Style({ + stroke: new Stroke({ color: "#0099ff", width: 3 }), + }); + } + if (type === "Point" || type === "MultiPoint") { + return new Style({ + image: new CircleStyle({ + radius: 6, + fill: new Fill({ color: "#0099ff" }), + stroke: new Stroke({ color: "#ffffff", width: 2 }), + }), + }); + } + }, + }); + + mapObj.current = new Map({ + target: mapRef.current, + layers: [ + new TileLayer({ source: new OSM() }), + vectorLayer, + ], + view: new View({ + center: fromLonLat([110, -6]), + zoom: 5, + }), + }); + + 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); + }; + }, [features]); + + return ( +
+ ); +}; + +export default GeoPreview; \ No newline at end of file diff --git a/app/(modules)/admin/mapset-upload/_components/map/MetadataForm.tsx b/app/(modules)/admin/mapset-upload/_components/map/MetadataForm.tsx new file mode 100644 index 0000000..54b0245 --- /dev/null +++ b/app/(modules)/admin/mapset-upload/_components/map/MetadataForm.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/components/ui/tabs"; +import FormMultiSelect from "@/shared/components/forms/form-multi-select"; + +const satupetaCategory = [ + "Batas Wilayah", + "Kependudukan", + "Lingkungan Hidup", + "Pemerintah Desa", + "Pendidikan", + "Sosial", + "Pendidikan SD", + "Pariwisata Kebudayaan", + "Kesehatan", + "Ekonomi", + "Kemiskinan", + "Infrastruktur" + ] + const satupetaCategoryId = [ + "019a0997-5b42-7c34-9ab8-35b4765ecb39", + "0196c80b-e680-7dca-9b90-b5ebe65de70d", + "0196c80c-855f-77f9-abd0-0c8a30b8c2f5", + "0196c80c-f805-76a8-82c7-af50b794871b", + "0196c80d-228d-7e1e-9116-78ba912b812c", + "0196c80d-3f05-7750-ab2a-f58655fef6ea", + "019936a6-4a5b-719f-8d88-d2df0af5aa20", + "0196c80c-c4fc-7ea6-afc0-3672a1b44b5b", + "0196c80c-61d8-7616-9abc-550a89283a57", + "0196c809-a0b0-79fb-b597-422d716fdce8", + "0196c80b-bb09-7424-9cd5-e3ec4946c7af", + "0196c80b-8710-7577-bc28-3ce66a02f56f" + ] + +export const getInitialMetadata = (initialValues: any = {}) => { + const today = new Date().toISOString().split('T')[0]; + + + function getIndexCategoryId(value: string) { + return satupetaCategory.indexOf(value); + } + + return { + // Mapping dari API Suggestion (jika ada) + title: initialValues.judul || "", + abstract: initialValues.abstrak || "", + keywords: initialValues.keyword ? (Array.isArray(initialValues.keyword) ? initialValues.keyword.join(', ') : initialValues.keyword) : "", + topicCategory: initialValues.kategori || "", + + // Default Values Hardcoded Anda + mapsetCategory: satupetaCategoryId[getIndexCategoryId(initialValues.kategori_mapset)] || "", + dateCreated: today, + status: "completed", + language: "eng", + + organization: "PUPR", + contactName: "Dimas", + contactEmail: "pu@gmail.com", + contactPhone: "08222222222", + + // ๐ŸŒ Distribusi + downloadLink: "", + serviceLink: "", + format: "", + license: "Copyright", + + // ๐Ÿงญ Referensi Spasial + crs: "EPSG:4326", + geometryType: "", + xmin: "", + xmax: "", + ymin: "", + ymax: "", + + // ๐Ÿงพ Metadata Umum + metadataStandard: "ISO 19115:2003/19139", + metadataVersion: "1.0", + metadataUUID: "", // Nanti di-generate di useEffect agar tidak hydration error + metadataDate: "", + charset: "utf8", + rsIdentifier: "WGS 1984", + + // Override dengan apapun yang ada di initialValues jika key-nya cocok + // ...initialValues + }; +}; + +export default function MetadataForm({ onChange, initialValues = {} }: { onChange: (val: any) => void, initialValues?: any }) { + const [formData, setFormData] = useState(initialValues); + + // Generate UUID saat mount (Client Side Only) + // useEffect(() => { + // if (!formData.metadataUUID) { + // setFormData((prev: any) => { + // const updated = { + // ...prev, + // metadataUUID: uuidv4(), + // metadataDate: new Date().toISOString().split("T")[0], + // }; + // // Penting: Kabari parent tentang update UUID ini + // onChange(updated); + // return updated; + // }); + // } + // }, []); + + useEffect(() => { + // Cek jika UUID belum ada (hanya saat mount) + if (!formData.metadataUUID) { + const generatedUUID = uuidv4(); + const generatedDate = new Date().toISOString().split("T")[0]; + + // 1. Siapkan data baru + const updatedData = { + ...formData, // Menggunakan state saat ini + metadataUUID: generatedUUID, + metadataDate: generatedDate, + }; + + // 2. Update State Lokal + setFormData((prev: any) => ({ + ...prev, + metadataUUID: generatedUUID, + metadataDate: generatedDate, + })); + + // 3. Update State Parent (PENTING: Dilakukan terpisah di sini, BUKAN di dalam setFormData) + onChange(updatedData); + } + }, []); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + const updated = { ...formData, [name]: value }; + setFormData(updated); + onChange(updated); + }; + + return ( +
+ + {/* + Identifikasi + Kontak + */} + + + +