Skip to content

Commit 54266e1

Browse files
committed
Handle null geometry in GeoJSON query responses
Replace null/undefined geometries in GeoJSON query responses with a synthesized point at the click location before passing to geojson2mapml. This prevents geojson2mapml from crashing on null geometry and ensures feature properties are properly rendered as an HTML table (the default geojson2mapml behavior) rather than falling through to the raw HTML fallback which dumps the entire JSON response as text. Adds test for null geometry scenario and updates existing test to account for the new fourth query extent (1/3 → 1/4).
1 parent f75a5de commit 54266e1

File tree

5 files changed

+87
-20
lines changed

5 files changed

+87
-20
lines changed

src/mapml/handlers/QueryHandler.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,23 @@ export var QueryHandler = Handler.extend({
188188
) {
189189
try {
190190
let json = JSON.parse(response.text);
191+
// Replace null geometries with a point at the click location
192+
// so geojson2mapml can process the features without error
193+
let clickPoint = {
194+
type: 'Point',
195+
coordinates: [e.latlng.lng, e.latlng.lat]
196+
};
197+
if (json.type === 'FeatureCollection' && json.features) {
198+
for (let f of json.features) {
199+
if (f.geometry === null || f.geometry === undefined) {
200+
f.geometry = clickPoint;
201+
}
202+
}
203+
} else if (json.type === 'Feature') {
204+
if (json.geometry === null || json.geometry === undefined) {
205+
json.geometry = clickPoint;
206+
}
207+
}
191208
let mapmlLayer = M.geojson2mapml(json, {
192209
projection: layer.options.projection
193210
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"geometry": null,
7+
"properties": {
8+
"Source": "PIEN",
9+
"Ordre": "4",
10+
"Superficie": "286350.56",
11+
"Shape": "Polygon"
12+
}
13+
}
14+
]
15+
}

test/e2e/layers/queryGeoJSON.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@
4343
<map-input name="h" type="height"></map-input>
4444
<map-link rel="query" tref="data/query/geojsonProjectedNoCrs?{i}{j}{xmin}{ymin}{xmax}{ymax}{w}{h}"></map-link>
4545
</map-extent>
46+
<map-extent label="GeoJSON null geometry" units="OSMTILE" checked>
47+
<map-input name="i" type="location" units="map" axis="i"></map-input>
48+
<map-input name="j" type="location" units="map" axis="j"></map-input>
49+
<map-input name="xmin" type="location" units="gcrs" axis="longitude" position="top-left" min="-180" max="180"></map-input>
50+
<map-input name="ymin" type="location" units="gcrs" axis="latitude" position="bottom-right" min="-90" max="90"></map-input>
51+
<map-input name="xmax" type="location" units="gcrs" axis="longitude" position="bottom-right" min="-180" max="180"></map-input>
52+
<map-input name="ymax" type="location" units="gcrs" axis="latitude" position="top-left" min="-90" max="90"></map-input>
53+
<map-input name="w" type="width"></map-input>
54+
<map-input name="h" type="height"></map-input>
55+
<map-link rel="query" tref="data/query/geojsonNullGeometry?{i}{j}{xmin}{ymin}{xmax}{ymax}{w}{h}"></map-link>
56+
</map-extent>
4657
</map-layer>
4758
</mapml-viewer>
4859
</body>

test/e2e/layers/queryGeoJSON.test.js

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,25 @@ test.describe('GeoJSON Query Response', () => {
1515
test.afterAll(async function () {
1616
await context.close();
1717
});
18-
test('Query returns features from all three GeoJSON extents', async () => {
18+
test('Query returns features from all four GeoJSON extents', async () => {
1919
await page.click('mapml-viewer');
2020
const popupContainer = page.locator('.mapml-popup-content > iframe');
2121
await expect(popupContainer).toBeVisible();
2222
const popupFeatureCount = page.locator('.mapml-feature-count');
23-
await expect(popupFeatureCount).toHaveText('1/3', { useInnerText: true });
23+
await expect(popupFeatureCount).toHaveText('1/4', { useInnerText: true });
2424
});
2525
test('Standard CRS:84 GeoJSON feature has cs meta set to gcrs', async () => {
2626
// The first feature comes from the CRS:84 extent (geojsonFeature)
2727
// Its meta should have cs=gcrs since coordinates are standard lon/lat
2828
let csMeta = await page.evaluate(() => {
29-
let layer =
30-
document.querySelector('mapml-viewer').layers[0]._layer;
29+
let layer = document.querySelector('mapml-viewer').layers[0]._layer;
3130
let features = layer._mapmlFeatures;
3231
// find the feature from CRS:84 response (the polygon from geojsonFeature)
3332
let f = features.find(
3433
(feat) => feat.querySelector('map-polygon') !== null
3534
);
3635
if (f && f.meta) {
37-
let cs = f.meta.find(
38-
(m) => m.getAttribute('name') === 'cs'
39-
);
36+
let cs = f.meta.find((m) => m.getAttribute('name') === 'cs');
4037
return cs ? cs.getAttribute('content') : null;
4138
}
4239
return null;
@@ -47,18 +44,15 @@ test.describe('GeoJSON Query Response', () => {
4744
// The feature from geojsonProjectedWithCrs has a "crs" member
4845
// Its meta should have cs=pcrs
4946
let csMeta = await page.evaluate(() => {
50-
let layer =
51-
document.querySelector('mapml-viewer').layers[0]._layer;
47+
let layer = document.querySelector('mapml-viewer').layers[0]._layer;
5248
let features = layer._mapmlFeatures;
5349
// find the feature with properties containing "Test Point with CRS"
5450
let f = features.find((feat) => {
5551
let props = feat.querySelector('map-properties');
5652
return props && props.innerHTML.includes('Test Point with CRS');
5753
});
5854
if (f && f.meta) {
59-
let cs = f.meta.find(
60-
(m) => m.getAttribute('name') === 'cs'
61-
);
55+
let cs = f.meta.find((m) => m.getAttribute('name') === 'cs');
6256
return cs ? cs.getAttribute('content') : null;
6357
}
6458
return null;
@@ -69,24 +63,43 @@ test.describe('GeoJSON Query Response', () => {
6963
// The feature from geojsonProjectedNoCrs has large coordinate values
7064
// but no "crs" member — the magnitude heuristic should detect this
7165
let csMeta = await page.evaluate(() => {
72-
let layer =
73-
document.querySelector('mapml-viewer').layers[0]._layer;
66+
let layer = document.querySelector('mapml-viewer').layers[0]._layer;
7467
let features = layer._mapmlFeatures;
7568
// find the feature with properties containing "Test Point projected no CRS"
7669
let f = features.find((feat) => {
7770
let props = feat.querySelector('map-properties');
78-
return (
79-
props && props.innerHTML.includes('Test Point projected no CRS')
80-
);
71+
return props && props.innerHTML.includes('Test Point projected no CRS');
8172
});
8273
if (f && f.meta) {
83-
let cs = f.meta.find(
84-
(m) => m.getAttribute('name') === 'cs'
85-
);
74+
let cs = f.meta.find((m) => m.getAttribute('name') === 'cs');
8675
return cs ? cs.getAttribute('content') : null;
8776
}
8877
return null;
8978
});
9079
expect(csMeta).toBe('pcrs');
9180
});
81+
test('GeoJSON with null geometry is processed via geojson2mapml with synthesized click-point geometry', async () => {
82+
// The feature from geojsonNullGeometry has geometry: null
83+
// It should still be processed by geojson2mapml (properties as table)
84+
// with a synthesized point geometry at the click location
85+
let result = await page.evaluate(() => {
86+
let layer = document.querySelector('mapml-viewer').layers[0]._layer;
87+
let features = layer._mapmlFeatures;
88+
// find the feature with properties containing "PIEN"
89+
let f = features.find((feat) => {
90+
let props = feat.querySelector('map-properties');
91+
return props && props.innerHTML.includes('PIEN');
92+
});
93+
if (!f) return { found: false };
94+
// check that properties are rendered as a table (geojson2mapml default)
95+
let props = f.querySelector('map-properties');
96+
let hasTable = props.querySelector('table') !== null;
97+
// check that a point geometry was synthesized
98+
let hasPoint = f.querySelector('map-geometry map-point') !== null;
99+
return { found: true, hasTable: hasTable, hasPoint: hasPoint };
100+
});
101+
expect(result.found).toBe(true);
102+
expect(result.hasTable).toBe(true);
103+
expect(result.hasPoint).toBe(true);
104+
});
92105
});

test/server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ app.get('/data/query/geojsonProjectedNoCrs', (req, res, next) => {
159159
}
160160
);
161161
});
162+
app.get('/data/query/geojsonNullGeometry', (req, res, next) => {
163+
res.sendFile(
164+
__dirname + '/e2e/data/geojson/geojsonNullGeometry.json',
165+
{ headers: { 'Content-Type': 'application/geo+json' } },
166+
(err) => {
167+
if (err) {
168+
res.status(403).send('Error.');
169+
}
170+
}
171+
);
172+
});
162173
app.get('/data/query/geojsonFeature.geojson', (req, res, next) => {
163174
res.sendFile(
164175
__dirname + '/e2e/data/geojson/geojsonFeature.geojson',

0 commit comments

Comments
 (0)