file_table_reader/src/components/layers_style/CustomLayerStyle.jsx
2025-12-22 15:26:37 +07:00

416 lines
12 KiB
JavaScript

import React, { useState, useEffect } from "react";
const sldHeader = `
<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor version="1.0.0"
xmlns="http://www.opengis.net/sld"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/sld
http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd">
`;
const sldFooter = `</StyledLayerDescriptor>`;
const singleColorSLD = (color, geometryType) => `
${sldHeader}
<NamedLayer>
<Name>layer</Name>
<UserStyle>
<FeatureTypeStyle>
<Rule>
${symbolizer(geometryType, color)}
</Rule>
</FeatureTypeStyle>
</UserStyle>
</NamedLayer>
${sldFooter}
`;
const uniqueValueSLD = (column, rules, geometryType) => `
${sldHeader}
<NamedLayer>
<Name>layer</Name>
<UserStyle>
<FeatureTypeStyle>
${rules.map(r => `
<Rule>
<ogc:Filter>
<ogc:PropertyIsEqualTo>
<ogc:PropertyName>${column}</ogc:PropertyName>
<ogc:Literal>${r.value}</ogc:Literal>
</ogc:PropertyIsEqualTo>
</ogc:Filter>
${symbolizer(geometryType, r.color)}
</Rule>
`).join("")}
</FeatureTypeStyle>
</UserStyle>
</NamedLayer>
${sldFooter}
`;
const globalIconSLD = (iconCode) => `
${sldHeader}
<NamedLayer>
<Name>layer</Name>
<UserStyle>
<FeatureTypeStyle>
<Rule>
<PointSymbolizer>
<Graphic>
<ExternalGraphic>
<OnlineResource
xlink:type="simple"
xlink:href="${iconCode}"/>
<Format>image/png</Format>
</ExternalGraphic>
<Size>10</Size>
</Graphic>
</PointSymbolizer>
</Rule>
</FeatureTypeStyle>
</UserStyle>
</NamedLayer>
${sldFooter}
`;
const symbolizer = (geometryType, color) => {
if (geometryType.toUpperCase() === "POINT" || geometryType.toUpperCase() === "MULTIPOINT") {
return `
<PointSymbolizer>
<Graphic>
<Mark>
<WellKnownName>circle</WellKnownName>
<Fill>
<CssParameter name="fill">${color}</CssParameter>
</Fill>
<Stroke>
<CssParameter name="stroke">#000000</CssParameter>
<CssParameter name="stroke-width">2</CssParameter>
</Stroke>
</Mark>
<Size>10</Size>
</Graphic>
</PointSymbolizer>
`;
}
if (geometryType === "line") {
return `
<LineSymbolizer>
<Stroke>
<CssParameter name="stroke">${color}</CssParameter>
<CssParameter name="stroke-width">2</CssParameter>
</Stroke>
</LineSymbolizer>
`;
}
return `
<PolygonSymbolizer>
<Fill>
<CssParameter name="fill">${color}</CssParameter>
<CssParameter name="fill-opacity">0.5</CssParameter>
</Fill>
<Stroke>
<CssParameter name="stroke">#232323</CssParameter>
</Stroke>
</PolygonSymbolizer>
`;
};
// const randomColor = () =>
// "#" + Math.floor(Math.random() * 16777215).toString(16);
const randomColor = () => {
let color = "#000000"
while (color === "#000000") {
color =
"#" + Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, "0")
}
return color
}
const CustomLayerStyle = ({ data = [], geometryType, onSubmit, onChange }) => {
const [columns, setColumns] = useState([]);
const [selectedStyle, setSelectedStyle] = useState("single");
// STYLE STATE
const [singleColor, setSingleColor] = useState("#3388ff");
const [uniqueColumn, setUniqueColumn] = useState("");
const [uniqueRules, setUniqueRules] = useState([]);
const [randomRules, setRandomRules] = useState([]);
const [propColumn, setPropColumn] = useState("");
const [propMin, setPropMin] = useState(3);
const [propMax, setPropMax] = useState(12);
const [iconMode, setIconMode] = useState("global"); // global | per-feature
const [iconGlobal, setIconGlobal] = useState("");
const [iconRules, setIconRules] = useState([]);
// Extract columns
useEffect(() => {
if (data.length > 0) {
const keys = Object.keys(data[0]).filter((k) => k !== "geometry");
setColumns(keys);
}
}, [data]);
useEffect(() => {
onChange({
type: selectedStyle,
color: singleColor,
unique: uniqueRules,
random: randomRules,
proportional: { propColumn, propMin, propMax }
});
}, [selectedStyle, singleColor, uniqueRules, randomRules, propColumn, propMin, propMax]);
// Handle unique value column selection
const generateUniqueRules = (column) => {
const values = [...new Set(data.map((d) => d[column]))];
const rules = values.map((v) => ({
value: v,
color: randomColor(),
}));
setUniqueRules(rules);
};
// Handle random per row
const generateRandomRules = () => {
const rules = data.map((row) => ({
id: row.id,
color: randomColor(),
}));
setRandomRules(rules);
};
// Handle icon rules
const generateIconRules = () => {
const rules = data.map((row) => ({
id: row.id,
iconUrl: "",
}));
setIconRules(rules);
};
async function generateBase64(iconUrl) {
const response = await fetch(iconUrl, {
mode: "cors",
});
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Final Submit
const submit = async () => {
let xml = "";
if (selectedStyle === "single") {
xml = singleColorSLD(singleColor, geometryType);
}
if (selectedStyle === "unique_value") {
xml = uniqueValueSLD(uniqueColumn, uniqueRules, geometryType);
}
if (selectedStyle === "icon") {
// const iconCode = await generateBase64('https://cdn-icons-png.flaticon.com/512/0/614.png')
const iconCode = 'https://cdn-icons-png.flaticon.com/512/0/614.png'
xml = globalIconSLD(iconCode);
}
// xml = ``
onSubmit({
styleType: "sld",
sldContent: xml
});
};
return (
<div className="bg-white relative p-4 pb-0 rounded shadow h-full overflow-auto">
<h5 className="font-bold mb-3">🎨 Pengaturan Styling Data Spasial</h5>
{/* PILIH STYLE */}
<div className="mb-3">
<label className="font-semibold block mb-1">Jenis Styling</label>
<select
className="w-full border rounded p-2"
value={selectedStyle}
onChange={(e) => setSelectedStyle(e.target.value)}
>
<option value="single">Single Color</option>
<option value="unique_value">Unique Value (Kategori)</option>
{/* <option value="random">Random per Feature</option> */}
{/* <option value="proportional">Proportional (Ukuran)</option> */}
{geometryType === "Point" && <option value="icon">Icon per Feature</option>}
</select>
</div>
{/* ---------------- SINGLE COLOR ---------------- */}
{selectedStyle === "single" && (
<div className="mb-3">
<label className="font-semibold block mb-1">Pilih Warna</label>
<input
type="color"
className="w-16 h-10 p-1 border rounded"
value={singleColor}
onChange={(e) => setSingleColor(e.target.value)}
/>
</div>
)}
{/* ---------------- UNIQUE VALUE ---------------- */}
{selectedStyle === "unique_value" && (
<div className="mb-3">
<label className="font-semibold block mb-1">Pilih Kolom Kategori</label>
<select
className="w-full border rounded p-2"
onChange={(e) => {
setUniqueColumn(e.target.value);
generateUniqueRules(e.target.value);
}}
>
<option value="">-- pilih kolom --</option>
{columns.map((c) => (
<option key={c}>{c}</option>
))}
</select>
{/* RULE LIST */}
<div className="mt-3">
{uniqueRules.map((r, i) => (
<div key={i} className="flex items-center mb-2">
<div className="w-36 mr-2">{r.value}</div>
<input
type="color"
className="w-12 h-8 border rounded"
value={r.color}
onChange={(e) => {
const copy = [...uniqueRules];
copy[i].color = e.target.value;
setUniqueRules(copy);
}}
/>
</div>
))}
</div>
</div>
)}
{/* ---------------- RANDOM COLOR ---------------- */}
{selectedStyle === "random" && (
<div className="mb-3">
<button
className="bg-gray-600 text-white px-3 py-2 rounded mb-2"
onClick={generateRandomRules}
>
Generate Random Colors
</button>
{randomRules.map((r, i) => (
<div key={i} className="flex items-center mb-2">
<span className="mr-2">ID {r.id}</span>
<input
type="color"
className="w-12 h-8 border rounded"
value={r.color}
onChange={(e) => {
const copy = [...randomRules];
copy[i].color = e.target.value;
setRandomRules(copy);
}}
/>
</div>
))}
</div>
)}
{/* ---------------- PROPORTIONAL ---------------- */}
{selectedStyle === "proportional" && (
<div className="mb-3">
<label className="font-semibold block mb-1">Pilih Kolom Angka</label>
<select
className="w-full border rounded p-2 mb-2"
onChange={(e) => setPropColumn(e.target.value)}
>
<option value="">-- pilih kolom --</option>
{columns.map((c) => {
const isNumber = typeof data[0][c] === "number";
return isNumber && <option key={c}>{c}</option>;
})}
</select>
<div className="mb-2">
<label className="block mb-1">Ukuran Minimum</label>
<input
type="number"
className="w-full border rounded p-2"
value={propMin}
onChange={(e) => setPropMin(Number(e.target.value))}
/>
</div>
<div>
<label className="block mb-1">Ukuran Maksimum</label>
<input
type="number"
className="w-full border rounded p-2"
value={propMax}
onChange={(e) => setPropMax(Number(e.target.value))}
/>
</div>
</div>
)}
{/* ---------------- ICON PER FEATURE ---------------- */}
{selectedStyle === "icon" && geometryType === "Point" && (
<div className="mb-3">
<label className="block mb-1">Masukkan URL Icon</label>
<input
type="text"
className="w-full border rounded p-2"
placeholder="https://example.com/icon.png"
value={iconGlobal}
onChange={(e) => setIconGlobal(e.target.value)}
/>
</div>
)}
<div className="sticky bottom-0 bg-white pt-2">
<button
className="bg-blue-600 hover:bg-blue-700 text-white w-full py-1 rounded"
onClick={submit}
>
Terapkan
</button>
</div>
</div>
);
};
export default CustomLayerStyle;