Skip to content

Commit f5253c8

Browse files
authored
Merge pull request #236 from TimBroddin/feat/plopsa-de-panne-locations
feat(plopsa): add per-POI coordinates for Plopsaland De Panne
2 parents 912d4b9 + 18bb972 commit f5253c8

4 files changed

Lines changed: 543 additions & 5 deletions

File tree

src/parks/plopsa/__tests__/plopsa.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
* disagrees between instances.
99
*/
1010
import {describe, test, expect} from 'vitest';
11-
import {plopsaDecideOperating} from '../plopsa.js';
11+
import type {Entity} from '@themeparks/typelib';
12+
import {plopsaDecideOperating, Plopsaland} from '../plopsa.js';
1213

1314
describe('plopsaDecideOperating', () => {
1415
test('park closed → ride always CLOSED regardless of other inputs', () => {
@@ -36,3 +37,100 @@ describe('plopsaDecideOperating', () => {
3637
expect(plopsaDecideOperating(true, true, false)).toBe(false);
3738
});
3839
});
40+
41+
describe('De Panne POI location wiring', () => {
42+
const fallbackLocation = {latitude: 1, longitude: 2};
43+
44+
class Probe extends Plopsaland {
45+
protected override mapCoordinates(
46+
coords: {x: number; y: number} | undefined,
47+
): {latitude: number; longitude: number} | undefined {
48+
return coords ? fallbackLocation : undefined;
49+
}
50+
51+
public buildEntitiesForTest(): Promise<Entity[]> {
52+
return this.buildEntityList();
53+
}
54+
}
55+
56+
function jsonResponse(payload: unknown) {
57+
return {json: async () => payload};
58+
}
59+
60+
function createProbe(poiItems: unknown[]): Probe {
61+
const park = new Probe();
62+
park.fetchPOI = async () => jsonResponse({items: poiItems}) as any;
63+
park.fetchEntertainments = async () => jsonResponse({items: []}) as any;
64+
return park;
65+
}
66+
67+
test('populates attraction location from the bundled snapshot', async () => {
68+
const entities = await createProbe([{
69+
id: 'poi-1',
70+
title: 'Attractions',
71+
type: {label: 'Attractions'},
72+
map_coordinates: {x: 10, y: 20},
73+
contains: [{
74+
id: 'anubis',
75+
title: 'Anubis The Ride',
76+
type: 'attraction',
77+
}],
78+
}]).buildEntitiesForTest();
79+
80+
const anubis = entities.find((e) => e.id === 'anubis');
81+
expect(anubis?.location).toEqual({latitude: 51.081837, longitude: 2.597878});
82+
});
83+
84+
test('prefers snapshot coordinates over mapped POI coordinates', async () => {
85+
const entities = await createProbe([{
86+
id: 'poi-1',
87+
title: 'Attractions',
88+
type: {label: 'Attractions'},
89+
map_coordinates: {x: 10, y: 20},
90+
contains: [{
91+
id: 'anubis',
92+
title: 'Anubis The Ride',
93+
type: 'attraction',
94+
}],
95+
}]).buildEntitiesForTest();
96+
97+
const anubis = entities.find((e) => e.id === 'anubis');
98+
expect(anubis?.location).toEqual({latitude: 51.081837, longitude: 2.597878});
99+
expect(anubis?.location).not.toEqual(fallbackLocation);
100+
});
101+
102+
test('matches snapshot titles with curly apostrophes through buildEntityList', async () => {
103+
const entities = await createProbe([{
104+
id: 'poi-1',
105+
title: 'Attractions',
106+
type: {label: 'Attractions'},
107+
map_coordinates: {x: 10, y: 20},
108+
contains: [{
109+
id: 'vics',
110+
title: 'Vic’s Whirlwind',
111+
type: 'attraction',
112+
}],
113+
}]).buildEntitiesForTest();
114+
115+
const vics = entities.find((e) => e.id === 'vics');
116+
expect(vics?.location).toEqual({latitude: 51.082082, longitude: 2.5958043});
117+
});
118+
119+
test('skips POI children without a usable title', async () => {
120+
const entities = await createProbe([{
121+
id: 'poi-1',
122+
title: 'Attractions',
123+
type: {label: 'Attractions'},
124+
map_coordinates: {x: 10, y: 20},
125+
contains: [
126+
{id: 'missing-title', type: 'attraction'},
127+
{id: 'null-title', title: null, type: 'foods_and_drinks'},
128+
{id: 'anubis', title: 'Anubis The Ride', type: 'attraction'},
129+
],
130+
}]).buildEntitiesForTest();
131+
132+
expect(entities.find((e) => e.id === 'missing-title')).toBeUndefined();
133+
expect(entities.find((e) => e.id === 'null-title')).toBeUndefined();
134+
expect(entities.find((e) => e.id === 'anubis')).toBeDefined();
135+
});
136+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Plopsaland De Panne — POI location snapshot
2+
3+
`plopsaland-de-panne.json` maps each POI **title** (exactly as the middleware
4+
feed emits it in `contains[].title`) to a hand-verified `{latitude, longitude}`.
5+
6+
## Why a static snapshot
7+
8+
The Plopsa middleware feed (`/api/points-of-interest`) exposes only
9+
`map_coordinates` — pixel coordinates on the park-map image — not lat/lng. De
10+
Panne, unlike Deutschland, has no pixel→geo transform (`mapCoordinates()`
11+
returns `undefined`), so without this snapshot its rides and restaurants have no
12+
location. The base class prefers this per-title lookup and falls back to the
13+
pixel transform (Deutschland only).
14+
15+
## How the coordinates were derived
16+
17+
1. **Titles** came from the live `points-of-interest` feed (attraction +
18+
`foods_and_drinks` items).
19+
2. **Seed coordinates** came from Google Places (New) Text Search and the Apple
20+
Maps Server API, biased to the park and filtered to an in-park bounding box.
21+
Only name-verified matches were trusted; generic "Plopsaland Belgium" park
22+
pins were rejected.
23+
3. **Manual pinning** — the remaining rides (kids' rides, food stalls that no
24+
API indexes precisely) were placed by hand on a satellite basemap, cross-
25+
referenced against the official parkplan. The website's own marker
26+
coordinates (`point_x/point_y`, authored against `PLB_Parkplan2026_0.jpg` via
27+
a Leaflet `imageOverlay`) were used as a visual reference.
28+
29+
## Coverage / maintenance
30+
31+
- 86 of 87 POIs are located. **Merry-go-Round** is intentionally absent — it is
32+
not shown on the park map.
33+
- Matching folds curly/straight apostrophes and is case-insensitive
34+
(`lookupPoiLocation` in `../plopsa.ts`).
35+
- POIs Plopsa adds later will land without a location until this file is
36+
refreshed. To add one, append a `"<exact feed title>": {latitude, longitude}`
37+
entry.
38+
39+
## Regenerating / refreshing the snapshot
40+
41+
This file was produced with the **[plopsa-pinning-tool](https://github.com/TimBroddin/plopsa-pinning-tool)**
42+
— a three-pane tool (ride list · Apple satellite map · official parkplan
43+
overlay) that seeds coordinates from Google Places + Apple Maps and lets you
44+
place the rest by hand. Use it to refresh this snapshot for a new season or to
45+
add newly-opened POIs, then copy its `data/plopsaland-de-panne.json` output
46+
here.

0 commit comments

Comments
 (0)