feat(oceanpark): add Ocean Park Hong Kong#130
feat(oceanpark): add Ocean Park Hong Kong#130technodisney wants to merge 3 commits intoThemeParks:mainfrom
Conversation
- Auth token (optoken) injected via @Inject; device UUID persisted 90 days via @cache; dynamic TTL from API tokenExpire field - Coordinate affine transform: fetches reference_points.json from map subdomain and projects pixel positions to lat/lng for all entities - Entities: rides, transport, shows, dining — with height restriction tags, wet rides, pregnant warning, and FastPass (paidReturnTime) - Live data: wait times + today's operating hours (rides/transport), showtimes from activityList (shows) - Schedules: 30-day park operating hours, parking, Summit zone entries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cubehouse
left a comment
There was a problem hiding this comment.
Nice work — well-structured implementation with clean auth flow and good use of the decorator patterns. A few items to address:
| m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + | ||
| m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); | ||
|
|
||
| const D = det(M); |
There was a problem hiding this comment.
If the map API ever returns collinear or duplicate reference points, D will be zero and all coordinates become NaN/Infinity — which silently propagates through entity locations (the if (coords) check passes because {latitude: NaN, longitude: NaN} is truthy).
Add a guard here:
const D = det(M);
if (Math.abs(D) < 1e-10) return null;Then handle the null in getCoordinateMapEntries by falling back to no coordinates for that category.
| // ── Implementation ────────────────────────────────────────────────────────── | ||
|
|
||
| @destinationController({category: 'Ocean Park'}) | ||
| @config |
There was a problem hiding this comment.
The @config class decorator is not needed here — @destinationController already applies @config internally (see destinationRegistry.ts line 208). Having both double-wraps the class in config proxies. Remove this line.
| const coeffs = computeAffineTransform(refPoints); | ||
| const entries: [string, {latitude: number; longitude: number}][] = []; | ||
|
|
||
| for (const category of MAP_CATEGORIES) { |
There was a problem hiding this comment.
These 6 map category fetches are independent but awaited sequentially. With the HTTP queue rate limit (250ms), this adds ~1.5s of wall time on a cold cache.
Use Promise.all to parallelize:
const responses = await Promise.all(
MAP_CATEGORIES.map(cat => this.fetchMapCategoryData(cat))
);
for (const resp of responses) {
const entities: OceanParkMapEntity[] = await resp.json();
// ...
}|
Hey @technodisney — just checking in on this. There are 3 inline review comments from April 8 that still need addressing:
Let us know if you need any help with these or if you have questions! |
- Guard affine transform against degenerate reference points (determinant ≈ 0 → return null, caller returns empty coords) - Remove duplicate @config class decorator (already applied by @destinationController) - Parallelize map category fetches with Promise.all Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Hi @technodisney — I've pushed a fixup commit (
CI didn't trigger automatically on the fork branch. @cubehouse — this should be good to merge if you're happy with the implementation after a local |
…ber guards Three issues surfaced when reviewing the output: 1. **Schedule timestamps were `2026-05-01T10:00:00GMT+8`** instead of `+08:00`. The pre-existing `formatInTimezone(... 'iso')` helper passed `Intl.DateTimeFormat`'s `shortOffset` value through verbatim, but for timezones like Asia/Hong_Kong the API returns `GMT+8` rather than the RFC 3339 `+HH:MM` form. Other parks dodge this by routing through `constructDateTime`, which has its own GMT-style normalizer (lines 183-191 of datetime.ts). Move the same normalization into `formatInTimezone` so callers always get a valid ISO 8601 string. 2. **Hardcoded URL defaults** in `@config baseURL` / `@config mapURL` violated the project convention (CLAUDE.md: "no hardcoded URLs/secrets; all in @config with empty defaults, loaded from .env"). Set both to `''`; configuration is via `OCEANPARK_BASEURL` / `OCEANPARK_MAPURL` env vars per the project's standard config-prefix mechanism. 3. **`Number()` coercion on schedule timestamps** could silently produce `NaN` → `Invalid Date` for malformed `parkOpenTime` / `parkCloseTime` / `parkingOpenTime` / `parkingCloseTime` / `summitCloseTime`. Add a `parseTs` helper that uses `Number.isFinite()` (per CLAUDE.md numeric- validation guidance) and returns null for non-finite values; skip the day or sub-block when any required timestamp fails to parse. Coordinate transform was not dead code as initially feared — after a fresh cache, ~50 of 94 entities receive real coordinates (the rest are newer attractions not yet present in the static map JSONs and fall back to the destination centroid). No change required there. Also updates one DST-transition test that asserted on the legacy `GMT-5` / `GMT-4` substring; relaxed to accept the new canonical `-05:00` / `-04:00` form alongside the old shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi @technodisney — really nice contribution, thank you for the thorough work on this. The Sanrio/etc. coordinate fallback behaviour is sensible and the affine-transform approach for deriving lat/lng from map pixels is elegant. I've pushed a maintainer-edit commit (e1bd7c0) addressing three small issues that came up during review. Quick rundown so you can see what changed and weigh in: 1. Schedule timestamps were not RFC 3339. Output looked like: Root cause turned out to be a pre-existing bug in (Side benefit: any future park hitting this same case is now safe.) 2. Hardcoded URL defaults. The two @config baseURL: string = 'https://sop.oceanpark.com.hk';
@config mapURL: string = 'https://map.oceanpark.com.hk';This repo's convention is empty 3. Number coercion guard on schedule timestamps. One thing I checked but didn't change: I initially thought the affine-transform was dead code (every entity was getting the destination fallback location), but that turned out to be a stale-cache artifact. After a clean run, ~50 of 94 entities receive real coordinates. The unmatched ones are newer attractions (Sanrio, Pompompurin, etc.) not yet present in the static map JSONs — falling back to destination centroid is reasonable for those. All 1094 tests pass; verified live output. Happy to revert any of this if you'd prefer to handle it differently — and again, thanks for the contribution! 🙏 |
|
Thank you for that. Give me a few more days to check out the unmapped attractions. Sanrio wasn't present in my initial testing. |
Summary
Adds Ocean Park Hong Kong as a new destination.
Single destination, one park. No API credentials required — the park's mobile API uses a short-lived bearer token obtained from a public endpoint.
Implementation
Auth
optokenheader injected via@injecton all requests tosop.oceanpark.com.hk@cachetokenExpirefield in the API responseCoordinates
reference_points.jsonfrommap.oceanpark.com.hkprovides pixel→lat/lng anchor pointsEntities
sortId: 8), transport (sortId: 7), shows (sortId: 15), dining (sortId: 17)paidReturnTime)Live data
pflowInfo)activityListon entity detail endpointSchedules
Test plan
npm run dev -- oceanparkhongkong— entities, live data, and schedules return datanpm run dev -- oceanparkhongkong --ignore-cache— fresh fetch works correctlynpm test— existing tests unaffected🤖 Generated with Claude Code