diff --git a/CHANGELOG.md b/CHANGELOG.md index baa596564..9782fd8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Font size and weight for Print's text output can now be correctly set in Admin, see issue [#1752](https://github.com/hajkmap/Hajk/issues/1752). +### Changed +- Make the CQL filter in Client more interactive and user-friendly, see issue [#1731](https://github.com/hajkmap/Hajk/issues/1731). + ## [4.2.0] - 2026-01-23 ### Fixed diff --git a/apps/client/src/plugins/LayerSwitcher/components/CQLFilter.js b/apps/client/src/plugins/LayerSwitcher/components/CQLFilter.js index 86ab4d0db..92b2839e0 100644 --- a/apps/client/src/plugins/LayerSwitcher/components/CQLFilter.js +++ b/apps/client/src/plugins/LayerSwitcher/components/CQLFilter.js @@ -7,13 +7,22 @@ import { Typography, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; +import AddBoxIcon from "@mui/icons-material/AddBox"; +import RemoveCircleIcon from "@mui/icons-material/RemoveCircle"; +import Tooltip from "@mui/material/Tooltip"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; import HajkToolTip from "components/HajkToolTip"; -const CQLFilter = ({ layer }) => { +export default function CQLFilter({ layer }) { + const operatorOptions = ["=", "!=", ">", "<", ">=", "<=", "LIKE", "ILIKE"]; const [cqlFilter, setCqlFilter] = useState(""); + const [fieldNames, setFieldNames] = useState([]); useEffect(() => { const source = layer?.getSource(); + getFieldNames(layer).then(setFieldNames); + console.log(getFieldNames(layer)); const currentCqlFilterValue = (typeof source?.getParams === "function" && source?.getParams()?.CQL_FILTER) || @@ -21,41 +30,345 @@ const CQLFilter = ({ layer }) => { setCqlFilter(currentCqlFilterValue); }, [layer]); + const getFieldNames = async (layer) => { + const source = layer.getSource(); + if (!source) return []; + + // --- Get URL whether source is TileWMS or ImageWMS --- + let wmsUrl = null; + + if (typeof source.getUrls === "function") { + const urls = source.getUrls(); + wmsUrl = urls && urls.length > 0 ? urls[0] : null; + } else if (typeof source.getUrl === "function") { + wmsUrl = source.getUrl(); + } + + if (!wmsUrl) { + console.error("Unable to determine WMS URL"); + return []; + } + + const typeName = source.getParams().LAYERS; + + // Convert WMS url to WFS + let wfsUrl; + if (wmsUrl.includes("/wms")) { + wfsUrl = wmsUrl.replace(/\/wms.*/, "/wfs"); + } else if (wmsUrl.includes("/ows")) { + wfsUrl = wmsUrl.replace(/\/ows.*/, "/ows"); + } else { + console.error("Could not derive WFS URL from:", wmsUrl); + return []; + } + + const describeUrl = `${wfsUrl}?service=WFS&version=1.1.0&request=DescribeFeatureType&typename=${typeName}`; + + // try to get the field names + + try { + const response = await fetch(describeUrl); + const text = await response.text(); + + const parser = new DOMParser(); + const xml = parser.parseFromString(text, "text/xml"); + + const elements = + xml.getElementsByTagName("xsd:element").length > 0 + ? xml.getElementsByTagName("xsd:element") + : xml.getElementsByTagName("element"); + + const fields = []; + for (let i = 0; i < elements.length; i++) { + const name = elements[i].getAttribute("name"); + fields.push(name); + } + + return fields; + } catch (err) { + console.error("DescribeFeatureType error:", err); + return []; + } + }; + const updateFilter = () => { let filter = cqlFilter.trim(); - if (filter.length === 0) filter = undefined; // If length === 0, unset filter. + if (filter.length === 0) filter = undefined; layer?.getSource().updateParams({ CQL_FILTER: filter }); }; + const [rows, setRows] = useState([ + { + open: false, + field: "", + operator: "=", + value: "", + logic: "AND", + close: false, + }, + ]); + + // Build CQL string from rows + const buildCQL = (rows) => { + return rows + .map((r, i) => { + if (!r.field || !r.operator || !r.value) return ""; + let part = ""; + if (r.open) part += "("; + part += `${r.field} ${r.operator} '${r.value}'`; + if (r.close) part += ")"; + if (i > 0) part = `${r.logic} ${part}`; + return part; + }) + .join(" "); + }; + + // Update row + sync text field + const updateRow = (i, key, val) => { + const updated = [...rows]; + updated[i][key] = val; + setRows(updated); + setCqlFilter(buildCQL(updated)); + }; + + // Toggle bracket + const toggleRow = (i, key) => { + const updated = [...rows]; + updated[i][key] = !updated[i][key]; + setRows(updated); + setCqlFilter(buildCQL(updated)); + }; + + // Add row + const addRow = () => { + const updated = [ + ...rows, + { + open: false, + field: "", + operator: "=", + value: "", + logic: "AND", + close: false, + }, + ]; + setRows(updated); + setCqlFilter(buildCQL(updated)); + }; + + // Remove row + const removeRow = (i) => { + const updated = rows.filter((_, idx) => idx !== i); + setRows(updated); + setCqlFilter(buildCQL(updated)); + }; + return ( - + - CQL-filter + Attributbaserad filtrering - setCqlFilter(e.target.value)} - endAdornment={ - - ( + + {/* AND/OR connector */} + {i > 0 && ( + + updateRow(i, "logic", row.logic === "AND" ? "OR" : "AND") + } + sx={(theme) => ({ + border: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.action.selected, + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + color: theme.palette.text.primary, + fontSize: "0.75rem", + width: 40, + height: 28, + borderRadius: 0, + })} + > + {row.logic} + + )} + + {/* ( toggle */} + + + toggleRow(i, "open")} + sx={(theme) => ({ + position: "relative", + width: 20, + height: 28, + borderRadius: 0, + border: `1px solid ${theme.palette.divider}`, + + backgroundColor: row.open + ? theme.palette.primary.dark + : theme.palette.action.hover, + + color: row.open + ? theme.palette.primary.contrastText + : theme.palette.text.primary, + + "&:hover": { + backgroundColor: row.open + ? theme.palette.primary.dark + : theme.palette.action.hover, + border: `1px solid ${theme.palette.divider}`, + }, + })} + > + ( + + + + {/* Select or write field name in case field names cannot be accessed */} + {fieldNames.length > 0 ? ( + + ) : ( + updateRow(i, "field", e.target.value)} + sx={{ height: 30, width: "30%" }} + /> + )} + + {/* Select operator */} + + + + + {/* Value input */} + updateRow(i, "value", e.target.value)} + > + + {/* ) toggle */} + + toggleRow(i, "close")} + disableRipple + disableFocusRipple + sx={(theme) => ({ + border: `1px solid ${theme.palette.divider}`, + width: 20, + height: 28, + borderRadius: 0, + + backgroundColor: row.close + ? theme.palette.primary.dark + : theme.palette.action.hover, + + color: row.close + ? theme.palette.primary.contrastText + : theme.palette.text.primary, + + "&:hover": { + backgroundColor: row.close + ? theme.palette.primary.dark + : theme.palette.action.hover, + border: `1px solid ${theme.palette.divider}`, + }, + })} + > + ) + + + + {/* Remove row */} + {i > 0 && ( + + removeRow(i)} + > + - - - } - /> + + )} + + ))} + + {/* Add row */} + + + + + + + + + Filtreringstillstånd + + + setCqlFilter(e.target.value)} + endAdornment={ + + + + + + + + } + /> + ); -}; - -export default CQLFilter; +}