Skip to content

Commit 3be12d2

Browse files
committed
fix sld legend
1 parent 45b9e76 commit 3be12d2

5 files changed

Lines changed: 189 additions & 37 deletions

File tree

public/leaflet/leaflet-sld.js

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ var comparisionOperatorMapping = {
4646
'ogc:PropertyIsGreaterThan': '>',
4747
'ogc:PropertyIsLessThanOrEqualTo': '<=',
4848
'ogc:PropertyIsGreaterThanOrEqualTo': '>=',
49+
'ogc:PropertyIsLike': 'like',
4950
//'ogc:PropertyIsNull': 'isNull',
5051
//'ogc:PropertyIsBetween'
51-
// ogc:PropertyIsLike
5252
};
5353

5454
// namespaces for Tag lookup in XML
@@ -87,6 +87,52 @@ function getTagNameArray(element, tagName, childrens) {
8787
return tags;
8888
};
8989

90+
function escapeRegex(value) {
91+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92+
}
93+
94+
function getFilterPatternRegex(comp) {
95+
var wildCard = typeof comp.wildCard === 'string' && comp.wildCard.length
96+
? comp.wildCard
97+
: '*';
98+
var singleChar = typeof comp.singleChar === 'string' && comp.singleChar.length
99+
? comp.singleChar
100+
: '?';
101+
var escapeChar = typeof comp.escapeChar === 'string' && comp.escapeChar.length
102+
? comp.escapeChar
103+
: '!';
104+
var literal = String(comp.literal ?? '');
105+
var pattern = '^';
106+
var escaped = false;
107+
108+
for (var i = 0; i < literal.length; i++) {
109+
var char = literal.charAt(i);
110+
111+
if (!escaped && char === escapeChar) {
112+
escaped = true;
113+
continue;
114+
}
115+
116+
if (!escaped && char === wildCard) {
117+
pattern += '.*';
118+
} else if (!escaped && char === singleChar) {
119+
pattern += '.';
120+
} else {
121+
pattern += escapeRegex(char);
122+
}
123+
124+
escaped = false;
125+
}
126+
127+
if (escaped) {
128+
pattern += escapeRegex(escapeChar);
129+
}
130+
131+
pattern += '$';
132+
133+
return new RegExp(pattern);
134+
}
135+
90136
/**
91137
* SLD Styler. Reads SLD 1.1.0.
92138
*
@@ -175,7 +221,10 @@ L.SLDStyler = L.Class.extend({
175221
filterJson.comparisions.push({
176222
operator: comparisionOperator,
177223
property: property,
178-
literal: literal
224+
literal: literal,
225+
wildCard: comparisionElement.getAttribute('wildCard') || '*',
226+
singleChar: comparisionElement.getAttribute('singleChar') || '?',
227+
escapeChar: comparisionElement.getAttribute('escapeChar') || '!'
179228
})
180229
})
181230
});
@@ -247,18 +296,25 @@ L.SLDStyler = L.Class.extend({
247296
if (filter) {
248297
var operator = filter.operator == null || filter.operator == 'and' ? 'every' : 'some';
249298
return filter.comparisions[operator](function(comp) {
299+
var value = properties ? properties[comp.property] : undefined;
300+
250301
if (comp.operator == '==') {
251-
return properties[comp.property] == comp.literal;
302+
return value == comp.literal;
252303
} else if (comp.operator == '!=') {
253-
return properties[comp.property] != comp.literal;
304+
return value != comp.literal;
254305
} else if (comp.operator == '<') {
255-
return properties[comp.property] < comp.literal;
306+
return value < comp.literal;
256307
} else if (comp.operator == '>') {
257-
return properties[comp.property] > comp.literal;
308+
return value > comp.literal;
258309
} else if (comp.operator == '<=') {
259-
return properties[comp.property] <= comp.literal;
310+
return value <= comp.literal;
260311
} else if (comp.operator == '>=') {
261-
return properties[comp.property] >= comp.literal;
312+
return value >= comp.literal;
313+
} else if (comp.operator == 'like') {
314+
if (typeof value === 'undefined' || value === null) {
315+
return false;
316+
}
317+
return getFilterPatternRegex(comp).test(String(value));
262318
} else {
263319
console.error('Unknown comparision operator', comp.operator);
264320
}
@@ -341,4 +397,4 @@ L.SLDStyler = L.Class.extend({
341397

342398
L.SLDStyler.defaultStyle = defaultStyle;
343399

344-
})();
400+
})();

src/components/map/SldLegend.tsx

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ function Swatch({ item }: { item: LegendItem }) {
6969
);
7070
}
7171

72-
function groupByKind(items: LegendItem[]) {
72+
function groupByField(items: LegendItem[]) {
7373
const map = new Map<string, LegendItem[]>();
7474
for (const it of items) {
75-
const key = it.kind;
75+
const key = it.fieldLabel || "All fields";
7676
if (!map.has(key)) map.set(key, []);
7777
map.get(key)!.push(it);
7878
}
@@ -160,26 +160,37 @@ export default function SldLegend({
160160
const rawItems = useMemo(() => {
161161
if (!styler) return [];
162162
const base = legendFromStyler(styler, layerNameOrIndex);
163-
164-
// If labels are useless (e.g. "All features" repeated), replace by rule titles in order.
165-
const allDefault = base.length > 0 && base.every((it) => isDefaultLegendLabel(it.label));
166-
if (!allDefault) return base;
167-
168-
return base.map((it, idx) => ({
169-
...it,
170-
label: ruleTitles[idx] ?? it.label ?? t("Map.sldLegend.untitled"),
171-
}));
163+
const titlesMatchRuleCount = ruleTitles.length >= base.length;
164+
165+
return base.map((it, idx) => {
166+
const fallbackLabel = it.label ?? t("Map.sldLegend.untitled");
167+
const label =
168+
titlesMatchRuleCount && ruleTitles[idx]
169+
? ruleTitles[idx]
170+
: isDefaultLegendLabel(fallbackLabel)
171+
? ruleTitles[idx] ?? fallbackLabel
172+
: fallbackLabel;
173+
174+
return {
175+
...it,
176+
label,
177+
};
178+
});
172179
}, [styler, layerNameOrIndex, ruleTitles, t]);
173180

174181
const [q, setQ] = useState("");
175182

176183
const items = useMemo(() => {
177184
const query = q.trim().toLowerCase();
178185
if (!query) return rawItems;
179-
return rawItems.filter((it) => (it.label ?? "").toLowerCase().includes(query));
186+
return rawItems.filter((it) => {
187+
const label = (it.label ?? "").toLowerCase();
188+
const field = (it.fieldLabel ?? "").toLowerCase();
189+
return label.includes(query) || field.includes(query);
190+
});
180191
}, [rawItems, q]);
181192

182-
const groups = useMemo(() => groupByKind(items), [items]);
193+
const groups = useMemo(() => groupByField(items), [items]);
183194

184195
if (rawItems.length === 0) return null;
185196

@@ -223,26 +234,31 @@ export default function SldLegend({
223234

224235
<CardContent className="pt-2">
225236
<div className="max-h-[40vh] space-y-4 overflow-y-auto pr-1 sm:max-h-[260px]">
226-
{groups.map(([kind, groupItems]) => (
227-
<div key={kind} className="space-y-2">
237+
{groups.map(([field, groupItems]) => (
238+
<div key={field} className="space-y-2">
228239
<div className="text-xs font-medium text-muted-foreground">
229-
{kind === "polygon"
230-
? t("Map.sldLegend.groups.areas")
231-
: kind === "line"
232-
? t("Map.sldLegend.groups.lines")
233-
: t("Map.sldLegend.groups.points")}
240+
{field}
234241
</div>
235242

236243
<div className="space-y-1.5">
237244
{groupItems.map((it, idx) => (
238245
<div
239-
key={`${it.kind}-${it.label}-${idx}`}
246+
key={`${field}-${it.kind}-${it.label}-${idx}`}
240247
className="flex items-center gap-3 rounded-lg border px-2.5 py-2 bg-card/50 hover:bg-card transition-colors"
241248
style={{ borderColor: "rgba(0,0,0,0.08)" }}
242249
>
243250
<Swatch item={it} />
244-
<div className="text-sm flex-1">
245-
{it.label || t("Map.sldLegend.untitled")}
251+
<div className="flex-1">
252+
<div className="text-sm">
253+
{it.label || t("Map.sldLegend.untitled")}
254+
</div>
255+
<div className="text-xs text-muted-foreground">
256+
{it.kind === "polygon"
257+
? t("Map.sldLegend.groups.areas")
258+
: it.kind === "line"
259+
? t("Map.sldLegend.groups.lines")
260+
: t("Map.sldLegend.groups.points")}
261+
</div>
246262
</div>
247263
</div>
248264
))}

src/hooks/sld.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ export function useSldStyler(sldXml: string) {
88

99
useEffect(() => {
1010
let mounted = true;
11+
const normalizedSldXml = sanitizeSldXml(sldXml);
1112

1213
const tryCreate = () => {
1314
if ("SLDStyler" in L) {
14-
const s = new L.SLDStyler(sldXml);
15+
const s = new L.SLDStyler(normalizedSldXml);
1516
if (mounted) setStyler(s);
1617
} else {
1718
setTimeout(tryCreate, 50);
@@ -50,4 +51,4 @@ export function sanitizeSldXml(input: string): string {
5051
} catch {
5152
return input;
5253
}
53-
}
54+
}

src/lib/sld.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,88 @@
1-
import { SldFilter, SldRule } from "@/types/leaflet-sld";
1+
import { SldComparison, SldFilter, SldRule } from "@/types/leaflet-sld";
22
import L from "leaflet";
33

44
export type LegendItem = {
55
label: string;
66
kind: "point" | "line" | "polygon";
7+
fieldLabel: string;
8+
fields: string[];
79
fillColor?: string;
810
color?: string;
911
weight?: number;
1012
size?: number;
1113
};
1214

15+
function normalizeLikeLiteral(comparison: SldComparison): string {
16+
const literal = comparison.literal ?? "";
17+
const wildCard = comparison.wildCard || "*";
18+
const singleChar = comparison.singleChar || "?";
19+
const escapeChar = comparison.escapeChar || "!";
20+
let value = "";
21+
let escaped = false;
22+
23+
for (const char of literal) {
24+
if (!escaped && char === escapeChar) {
25+
escaped = true;
26+
continue;
27+
}
28+
29+
if (!escaped && (char === wildCard || char === singleChar)) {
30+
break;
31+
}
32+
33+
value += char;
34+
escaped = false;
35+
}
36+
37+
return value.trim();
38+
}
39+
40+
function buildComparisonLabel(comparison: SldComparison): string {
41+
if (comparison.operator !== "like") {
42+
return `${comparison.property} ${comparison.operator} ${comparison.literal}`;
43+
}
44+
45+
const literal = comparison.literal ?? "";
46+
const wildCard = comparison.wildCard || "*";
47+
const singleChar = comparison.singleChar || "?";
48+
const startsWithWildcard = literal.startsWith(wildCard);
49+
const endsWithWildcard = literal.endsWith(wildCard);
50+
const hasSingleChar = literal.includes(singleChar);
51+
const normalizedValue = normalizeLikeLiteral(comparison) || literal;
52+
53+
if (hasSingleChar) {
54+
return `${comparison.property} matches ${literal}`;
55+
}
56+
57+
if (startsWithWildcard && endsWithWildcard) {
58+
return `${comparison.property} contains ${normalizedValue}`;
59+
}
60+
61+
if (endsWithWildcard) {
62+
return `${comparison.property} starts with ${normalizedValue}`;
63+
}
64+
65+
if (startsWithWildcard) {
66+
return `${comparison.property} ends with ${normalizedValue}`;
67+
}
68+
69+
return `${comparison.property} matches ${literal}`;
70+
}
71+
1372
function buildLabel(filter?: SldFilter): string {
1473
if (!filter || filter.comparisions.length === 0) return "All features";
1574
const joiner = filter.operator === "or" ? " OR " : " AND ";
1675
return filter.comparisions
17-
.map((c) => `${c.property} ${c.operator} ${c.literal}`)
76+
.map(buildComparisonLabel)
1877
.join(joiner);
1978
}
2079

80+
function extractFields(filter?: SldFilter): string[] {
81+
if (!filter || filter.comparisions.length === 0) return [];
82+
83+
return [...new Set(filter.comparisions.map((comparison) => comparison.property.trim()).filter(Boolean))];
84+
}
85+
2186
function ruleKind(rule: SldRule): LegendItem["kind"] | null {
2287
if (rule.pointSymbolizer) return "point";
2388
if (rule.lineSymbolizer) return "line";
@@ -58,9 +123,13 @@ export function legendFromStyler(
58123

59124
if (!sym) continue;
60125

126+
const fields = extractFields(rule.filter);
127+
61128
items.push({
62129
label: buildLabel(rule.filter),
63130
kind,
131+
fieldLabel: fields.join(", ") || "All fields",
132+
fields,
64133
fillColor: sym.fillColor,
65134
color: sym.color,
66135
weight: sym.weight,

src/types/leaflet-sld.d.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ import type { Feature, Geometry, GeoJsonProperties } from "geojson";
77
* These are NOT official Leaflet types; they reflect that specific plugin's output.
88
*/
99

10-
export type SldComparisonOperator = "==" | "!=" | "<" | ">" | "<=" | ">=";
10+
export type SldComparisonOperator =
11+
| "=="
12+
| "!="
13+
| "<"
14+
| ">"
15+
| "<="
16+
| ">="
17+
| "like";
1118

1219
export interface SldComparison {
1320
operator: SldComparisonOperator;
1421
property: string;
1522
literal: string;
23+
wildCard?: string;
24+
singleChar?: string;
25+
escapeChar?: string;
1626
}
1727

1828
export interface SldFilter {

0 commit comments

Comments
 (0)