From ea77b49482a0fa79c11d4103e0ba13b203fc1b42 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 12 Sep 2026 22:55:16 -0700 Subject: [PATCH] refactor: isolate Location controls and cancellable search --- CHANGELOG.md | 2 + docs/CODE-BOUNDARIES.md | 8 ++ docs/CURRENT-STATE.md | 10 ++ package.json | 3 +- scripts/format-scope.json | 9 +- scripts/package-boundaries.json | 220 ++++++++++++++++++++++++------- scripts/qa-location-controls.mjs | 54 ++++++++ src/cameraHandoff.test.mjs | 55 ++++---- src/locationReadouts.test.mjs | 6 +- src/sharelink.celestial.test.mjs | 2 + src/ui.js | 208 +++++++---------------------- src/ui/location.js | 3 + src/ui/locationControls.js | 119 +++++++++++++++++ src/ui/locationControls.test.mjs | 44 +++++++ src/ui/locationSearch.js | 49 +++++++ src/ui/locationSearch.test.mjs | 57 ++++++++ 16 files changed, 604 insertions(+), 245 deletions(-) create mode 100644 scripts/qa-location-controls.mjs create mode 100644 src/ui/location.js create mode 100644 src/ui/locationControls.js create mode 100644 src/ui/locationControls.test.mjs create mode 100644 src/ui/locationSearch.js create mode 100644 src/ui/locationSearch.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5609b843d..7d16fc822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ## [Unreleased] +- Extract Location controls and cancellable search presentation; preserve navigation handoff and prevent delayed POI expansion after closing the row. + - Separate Layers panel presentation and clear-control bindings from layer lifecycle operations; revoke listeners and subscriptions on replacement or teardown. - Extract Map Source controls with listener cleanup and protection against obsolete selection feedback. diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index 9eec38dc5..78f04247d 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -279,3 +279,11 @@ the caller. Hidden-page refresh scheduling remains an explicit callback. `ui/layers/feedback` exposes the existing pure loading/notice reducers separately from DOM controls. Scheduling and presentation stay with their callers. + +## Location controls + +`ui/location` exports Location controls, the cancellable lookup controller and +the existing location-status formatter. Callers supply city data, search and +navigation operations. The component owns DOM listeners and pending expansion; +it imports no geocoder, camera engine, layer or application bootstrap. Existing +camera authority and search providers remain supplied by the application. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 7c5b2ec96..85d03ca5c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,15 @@ # God's Eye View Current State +## Location control ownership + +City/POI rows, search/reset bindings, location readouts and the orbit indicator +have a dedicated component with explicit navigation actions. A separate lookup +controller cancels superseded searches and checks camera authority before flight +and result presentation. Existing search providers and camera handoff policy are +preserved. Replacing or closing a POI row cancels its pending expansion frame; +destruction releases listeners, pending searches and the orbit indicator. + + ## Layer panel ownership Layer rows, feed feedback, counts, focus-preserving chips and toggle listeners diff --git a/package.json b/package.json index e78bcbf8b..57d264802 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "./ui/effects/bloom": "./src/bloom.js", "./ui/maps": "./src/ui/mapSource.js", "./ui/layers": "./src/ui/layers.js", - "./ui/layers/feedback": "./src/loadingFeedback.js" + "./ui/layers/feedback": "./src/loadingFeedback.js", + "./ui/location": "./src/ui/location.js" } } diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 2dfafec41..8aa998b56 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -159,5 +159,12 @@ "src/ui/clearLayersControl.js", "scripts/qa-layer-panel.mjs", "src/loadingFeedback.js", - "src/data/installationFeedback.js" + "src/data/installationFeedback.js", + "src/ui/location.js", + "src/ui/locationControls.js", + "src/ui/locationSearch.js", + "src/locationStatus.js", + "src/ui/locationControls.test.mjs", + "src/ui/locationSearch.test.mjs", + "scripts/qa-location-controls.mjs" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 971bed61b..232869182 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -10,27 +10,47 @@ "src/data/localGeojsonCore.js", "src/data/localGeojsonLod.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "application": { - "exports": ["./application"], - "modules": ["src/app/application.js"], + "exports": [ + "./application" + ], + "modules": [ + "src/app/application.js" + ], "external": [] }, "viewer": { - "exports": ["./application/viewer"], - "modules": ["src/app/viewer.js"], - "external": ["cesium"] + "exports": [ + "./application/viewer" + ], + "modules": [ + "src/app/viewer.js" + ], + "external": [ + "cesium" + ] }, "vite-build": { "runtime": "node", - "exports": ["./build/vite"], - "modules": ["build/vite.js"], - "external": ["vite-plugin-cesium"] + "exports": [ + "./build/vite" + ], + "modules": [ + "build/vite.js" + ], + "external": [ + "vite-plugin-cesium" + ] }, "live-providers": { "runtime": "node", - "exports": ["./server/providers/live"], + "exports": [ + "./server/providers/live" + ], "modules": [ "server/providers/live.js", "server/providers/common/http.js", @@ -45,16 +65,24 @@ "src/data/aisStreamAdapter.js", "src/data/aisWatchdog.js" ], - "external": ["ws"] + "external": [ + "ws" + ] }, "adsb-lol-source": { - "exports": ["./sources/adsb-lol"], - "modules": ["src/data/adsbLolFallback.js"], + "exports": [ + "./sources/adsb-lol" + ], + "modules": [ + "src/data/adsbLolFallback.js" + ], "external": [] }, "place-providers": { "runtime": "node", - "exports": ["./server/providers/places"], + "exports": [ + "./server/providers/places" + ], "modules": [ "server/providers/places.js", "server/providers/places/google.js", @@ -69,13 +97,19 @@ "external": [] }, "place-payloads": { - "exports": ["./sources/places"], - "modules": ["src/data/placeProviderPayloads.js"], + "exports": [ + "./sources/places" + ], + "modules": [ + "src/data/placeProviderPayloads.js" + ], "external": [] }, "space-providers": { "runtime": "node", - "exports": ["./server/providers/space"], + "exports": [ + "./server/providers/space" + ], "modules": [ "server/providers/space.js", "server/providers/space/celestrak.js", @@ -86,13 +120,19 @@ "external": [] }, "space-requests": { - "exports": ["./sources/space"], - "modules": ["src/data/spaceProviderRequests.js"], + "exports": [ + "./sources/space" + ], + "modules": [ + "src/data/spaceProviderRequests.js" + ], "external": [] }, "terrain-provider": { "runtime": "node", - "exports": ["./server/providers/terrain"], + "exports": [ + "./server/providers/terrain" + ], "modules": [ "server/providers/terrain.js", "src/data/terrainHeightsProxy.js" @@ -100,30 +140,50 @@ "external": [] }, "terrain-source": { - "exports": ["./sources/terrain"], - "modules": ["src/data/terrainHeightsProxy.js"], + "exports": [ + "./sources/terrain" + ], + "modules": [ + "src/data/terrainHeightsProxy.js" + ], "external": [] }, "traffic-provider": { "runtime": "node", - "exports": ["./server/providers/traffic"], - "modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"], + "exports": [ + "./server/providers/traffic" + ], + "modules": [ + "server/providers/traffic.js", + "src/data/tomtomTiles.js" + ], "external": [] }, "traffic-source": { - "exports": ["./sources/traffic"], - "modules": ["src/data/tomtomTiles.js"], + "exports": [ + "./sources/traffic" + ], + "modules": [ + "src/data/tomtomTiles.js" + ], "external": [] }, "firms-provider": { "runtime": "node", - "exports": ["./server/providers/firms"], - "modules": ["server/providers/firms.js", "src/data/firmsCsv.js"], + "exports": [ + "./server/providers/firms" + ], + "modules": [ + "server/providers/firms.js", + "src/data/firmsCsv.js" + ], "external": [] }, "gbfs-provider": { "runtime": "node", - "exports": ["./server/providers/gbfs"], + "exports": [ + "./server/providers/gbfs" + ], "modules": [ "server/providers/gbfs.js", "server/providers/common/http.js", @@ -132,13 +192,19 @@ "external": [] }, "gbfs-source": { - "exports": ["./sources/gbfs"], - "modules": ["src/data/gbfsSource.js"], + "exports": [ + "./sources/gbfs" + ], + "modules": [ + "src/data/gbfsSource.js" + ], "external": [] }, "cctv-provider": { "runtime": "node", - "exports": ["./server/providers/cctv"], + "exports": [ + "./server/providers/cctv" + ], "modules": [ "server/providers/cctv.js", "server/providers/cctv/catalog.js", @@ -155,7 +221,9 @@ }, "radio-provider": { "runtime": "node", - "exports": ["./server/providers/radio"], + "exports": [ + "./server/providers/radio" + ], "modules": [ "server/providers/radio.js", "server/providers/radio/catalog.js", @@ -169,7 +237,9 @@ }, "overpass-provider": { "runtime": "node", - "exports": ["./server/providers/overpass"], + "exports": [ + "./server/providers/overpass" + ], "modules": [ "server/providers/common/geo.js", "server/providers/common/http.js", @@ -188,7 +258,9 @@ }, "military-installations-provider": { "runtime": "node", - "exports": ["./server/providers/military-installations"], + "exports": [ + "./server/providers/military-installations" + ], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -205,7 +277,9 @@ }, "regional-provider": { "runtime": "node", - "exports": ["./server/providers/regional"], + "exports": [ + "./server/providers/regional" + ], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -224,7 +298,9 @@ }, "openai-provider": { "runtime": "node", - "exports": ["./server/providers/openai"], + "exports": [ + "./server/providers/openai" + ], "modules": [ "server/providers/common/rate-limit.js", "server/providers/common/request.js", @@ -244,7 +320,9 @@ }, "standalone-key-setup": { "runtime": "node", - "exports": ["./server/standalone/key-setup"], + "exports": [ + "./server/standalone/key-setup" + ], "modules": [ "scripts/pinokio-environment.mjs", "server/providers/common/source-root.js", @@ -255,7 +333,9 @@ "external": [] }, "place-search": { - "exports": ["./search"], + "exports": [ + "./search" + ], "modules": [ "src/search/index.js", "src/search/placeSearch.js", @@ -265,17 +345,27 @@ "external": [] }, "panel-disclosure": { - "exports": ["./ui/panels"], - "modules": ["src/ui/panelDisclosure.js"], + "exports": [ + "./ui/panels" + ], + "modules": [ + "src/ui/panelDisclosure.js" + ], "external": [] }, "surface-keyboard": { - "exports": ["./ui/surfaces"], - "modules": ["src/ui/surfaceKeyboard.js"], + "exports": [ + "./ui/surfaces" + ], + "modules": [ + "src/ui/surfaceKeyboard.js" + ], "external": [] }, "panel-rail-layout": { - "exports": ["./ui/layout"], + "exports": [ + "./ui/layout" + ], "modules": [ "src/ui/panelRails.js", "src/ui/leftPanelRail.js", @@ -287,7 +377,9 @@ "external": [] }, "visual-input": { - "exports": ["./ui/input"], + "exports": [ + "./ui/input" + ], "modules": [ "src/ui/visualInput.js", "src/ui/applicationShortcuts.js", @@ -296,12 +388,19 @@ "external": [] }, "display-controls": { - "exports": ["./ui/display"], - "modules": ["src/ui/displayControls.js"], + "exports": [ + "./ui/display" + ], + "modules": [ + "src/ui/displayControls.js" + ], "external": [] }, "visual-effects": { - "exports": ["./ui/effects", "./ui/effects/bloom"], + "exports": [ + "./ui/effects", + "./ui/effects/bloom" + ], "modules": [ "src/ui/effects.js", "src/ui/visualEffects.js", @@ -314,10 +413,14 @@ "src/styles/noir.js", "src/styles/snow.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "map-source-controls": { - "exports": ["./ui/maps"], + "exports": [ + "./ui/maps" + ], "modules": [ "src/ui/mapSource.js", "src/ui/mapSourceControls.js", @@ -327,7 +430,10 @@ "external": [] }, "layer-panel": { - "exports": ["./ui/layers", "./ui/layers/feedback"], + "exports": [ + "./ui/layers", + "./ui/layers/feedback" + ], "modules": [ "src/ui/layers.js", "src/ui/layerPanel.js", @@ -336,5 +442,17 @@ "src/data/installationFeedback.js" ], "external": [] + }, + "location-controls": { + "exports": [ + "./ui/location" + ], + "modules": [ + "src/ui/location.js", + "src/ui/locationControls.js", + "src/ui/locationSearch.js", + "src/locationStatus.js" + ], + "external": [] } } diff --git a/scripts/qa-location-controls.mjs b/scripts/qa-location-controls.mjs new file mode 100644 index 000000000..bc761a913 --- /dev/null +++ b/scripts/qa-location-controls.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +/** Location UI acceptance; controlled search results isolate UI races from geocoder availability. */ +import fs from 'node:fs'; +import puppeteer from 'puppeteer'; +const browser = await puppeteer.launch({headless:true,args:['--no-sandbox',...(process.platform === 'darwin' ? ['--use-angle=metal','--enable-gpu'] : ['--use-gl=angle','--use-angle=swiftshader'])]}); +const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));let failures=0; +const check=(name,ok)=>{console.log(`[${ok?'PASS':'FAIL'}] ${name}`);if(!ok)failures++}; +try { + await page.setViewport({width:1440,height:900}); + await page.goto(`${process.env.QA_BASE_URL||'http://localhost:4173'}/?welcome=0`,{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>window.__godsEyeView?.styleManager?._locationControls&&document.getElementById('loading-screen')?.classList.contains('hidden'),{timeout:60000}); + await page.click('#location-bar-toggle'); + await page.click('[data-pin-target="location-bar"]'); + const cityId=await page.$eval('.location-pill',node=>node.dataset.locationId); + await page.click('.location-pill'); + await page.waitForFunction(()=>document.getElementById('poi-row').classList.contains('expanded')); + check('native city selection highlights and expands its POIs',await page.evaluate(id=>window.__godsEyeView.styleManager._activeLocationId===id&&document.querySelector('.location-pill.active')?.dataset.locationId===id,cityId)); + await page.keyboard.press('w'); + check('POI keyboard action selects the second landmark',await page.evaluate(()=>window.__godsEyeView.styleManager._activePoiIndex===1&&document.querySelector('.poi-pill.active')?.dataset.poiIndex==='1')); + check('orbit controls and indicator agree',await page.evaluate(()=>{const ui=window.__godsEyeView.styleManager;ui.setOrbit(true);const active=ui.orbitController.active&&document.getElementById('orbit-indicator').classList.contains('active');ui.setOrbit(false);return active&&!ui.orbitController.active&&!document.getElementById('orbit-indicator').classList.contains('active')})); + await page.click('#search-toggle'); + await page.type('#location-search','qwerty'); + check('typing in search leaves POI selection unchanged',await page.evaluate(()=>window.__godsEyeView.styleManager._activePoiIndex===1)); + await page.evaluate(()=>{ + const lookup=window.__godsEyeView.styleManager._locationLookup; + window.__qaSearchRequests=[]; + lookup.search=(query,options)=>new Promise(resolve=>window.__qaSearchRequests.push({query,options,resolve})); + document.getElementById('location-search').value='First'; + }); + await page.keyboard.press('Enter'); + await page.waitForFunction(()=>window.__qaSearchRequests.length===1); + await page.evaluate(()=>{document.getElementById('location-search').value='Second';document.getElementById('location-search').focus()}); + await page.keyboard.press('Enter'); + await page.waitForFunction(()=>window.__qaSearchRequests.length===2); + check('second native submit aborts the first request and keeps current busy state',await page.evaluate(()=>window.__qaSearchRequests[0].options.signal.aborted&&document.getElementById('location-search').classList.contains('searching'))); + await page.evaluate(()=>window.__qaSearchRequests[1].resolve({label:'Second landmark, Test city'})); + await page.waitForFunction(()=>window.__godsEyeView.styleManager._searchedLocationLabel==='Second landmark, Test city'); + await page.evaluate(()=>window.__qaSearchRequests[0].resolve({label:'First landmark, Test city'})); + check('late result cannot overwrite the current location readout or style',await page.evaluate(()=>{const ui=window.__godsEyeView.styleManager;return ui._searchedLocationLabel==='Second landmark, Test city'&&ui._locationMiniPoi.textContent.includes('Second landmark')&&ui.activeStyle==='normal'&&!ui._locationSearch.classList.contains('searching')})); + check('cancelled POI expansion stays closed after a frame',await page.evaluate(async id=>{const ui=window.__godsEyeView.styleManager;ui._expandPOIRow(id);ui._collapsePOIRow();await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));return !ui._poiRow.classList.contains('expanded')&&!ui._locationBarDivider.classList.contains('visible')},cityId)); + await page.evaluate(id=>window.__godsEyeView.styleManager._onCityPillClick(id),cityId); + await page.waitForFunction(()=>document.getElementById('poi-row').classList.contains('expanded')); + // Finish the camera's existing city flight before inspecting both layouts. + await page.waitForFunction(()=>!window.__godsEyeView.viewer.camera._currentFlight,{timeout:10000}); + fs.mkdirSync('qa-shots/location-controls',{recursive:true}); + await page.screenshot({path:'qa-shots/location-controls/desktop.png'}); + await page.setViewport({width:620,height:900}); + await page.waitForFunction(()=>document.getElementById('left-panel-stack')?.dataset.layoutMode==='mobile'); + await page.screenshot({path:'qa-shots/location-controls/narrow.png'}); + check('native reset returns through the existing camera handoff',await page.evaluate(async()=>{const ui=window.__godsEyeView.styleManager;ui._resetGlobeBtn.click();const pending=ui._globeResetPromise;if(!pending)return false;await pending;return !ui.orbitController.active&&!window.__godsEyeView.viewer.trackedEntity})); + check('destroyed controls cannot navigate or submit another lookup',await page.evaluate(()=>{const ui=window.__godsEyeView.styleManager;const count=window.__qaSearchRequests.length;ui._locationControls.destroy();ui._locationLookup.destroy();const before=ui._navigationGeneration;ui._locationPills.querySelector('button').click();ui._resetGlobeBtn.click();ui._locationSearch.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}));return before===ui._navigationGeneration&&count===window.__qaSearchRequests.length})); + check('no uncaught browser errors',errors.length===0); +}finally{await browser.close()} +console.log(`RESULT: ${failures} failures`);process.exitCode=failures?1:0; diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index e7d5b0268..39b1b27d0 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -243,22 +243,22 @@ test('validated voice camera destinations share the UI navigation authority faca }); test('deferred search releases only after its final authority check', () => { - const handler = body( - ui, - /this\._locationSearch\.addEventListener\('keydown', async \(e\) => \{([\s\S]*?)\n \}\);/, - 'search handler', - ); - ordered(handler, [ - "this._beginDeferredNavigation('location')", - 'this._activeLocationSearchGeneration = generation;', - 'searchAndFlyTo(this.viewer, query', - 'beforeFly: () => this._reassertNavigationHandoff(generation)', - 'generation !== this._navigationGeneration', + const search = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'locationSearch.js'), 'utf8'); + ordered(search, [ + 'const authority = this.begin();', + 'this.onStart(authority);', + 'await this.search(query', + 'beforeFly: () => current() && this.beforeFly(authority)', + 'if (!current() || controller.signal.aborted) return;', 'destination?.cancelled', 'finally', - 'this._settleLocationSearchUi(generation)', - ], 'deferred search'); - assert.doesNotMatch(handler.slice(0, handler.indexOf('searchAndFlyTo')), /_releaseFollowCamera/); + 'this.onSettled(authority)', + ], 'deferred search component'); + assert.match(ui, /begin: \(\) => this\._beginDeferredNavigation\('location'\)/); + assert.match(ui, /beforeFly: \(generation\) => this\._reassertNavigationHandoff\(generation\)/); + assert.match(ui, /isCurrent: \(generation\) => !this\._disposed && generation === this\._navigationGeneration/); + assert.match(ui, /onSettled: \(generation\) => this\._settleLocationSearchUi\(generation\)/); + assert.doesNotMatch(search.slice(0, search.indexOf('await this.search')), /_releaseFollowCamera/); }); test('a direct globe gesture retires delayed camera and selection restore only', () => { @@ -337,25 +337,18 @@ test('teardown synchronously closes immediate camera entry points', () => { }); test('teardown refuses deferred location work before geocoding begins', () => { - const deferred = body( - ui, - /_beginDeferredNavigation\(noun = 'location', \{ cancelPendingSelection = true \} = \{\}\) \{([\s\S]*?)\n \}/, - 'deferred navigation', - ); + const deferred = body(ui, /_beginDeferredNavigation\(noun = 'location', \{ cancelPendingSelection = true \} = \{\}\) \{([\s\S]*?)\n \}/, 'deferred navigation'); assert.match(deferred, /disposed: this\._disposed/); - - const handler = body( - ui, - /this\._locationSearch\.addEventListener\('keydown', async \(e\) => \{([\s\S]*?)\n \}\);/, - 'search handler', - ); - ordered(handler, [ - "const generation = this._beginDeferredNavigation('location');", - 'if (generation === false)', - 'this._locationSearch.blur();', - 'searchAndFlyTo(this.viewer, query', + const search = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'locationSearch.js'), 'utf8'); + ordered(search, [ + 'if (!query || this.destroyed) return;', + 'const authority = this.begin();', + 'if (authority === false)', + 'this.input.blur();', + 'this.onStart(authority);', + 'await this.search(query', ], 'disposed search refusal'); - assert.match(handler, /if \(generation === false\) \{[\s\S]*?return;[\s\S]*?\}\s*this\._activeLocationSearchGeneration/); + assert.match(search, /if \(authority === false\) \{[\s\S]*?return;[\s\S]*?\}\s*this\.controller/); }); test('refused canned destinations commit no location or POI state', () => { diff --git a/src/locationReadouts.test.mjs b/src/locationReadouts.test.mjs index 8bcbf6955..62c11e36f 100644 --- a/src/locationReadouts.test.mjs +++ b/src/locationReadouts.test.mjs @@ -44,11 +44,13 @@ test('a free-text search records its destination for the LOCATION mini-status', }); test('the mini-status reads its copy from the shared formatter', () => { - assert.match(ui, /import \{ locationMiniStatus \} from '\.\/locationStatus\.js';/); + const controls = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'locationControls.js'), 'utf8'); + assert.match(controls, /import \{ locationMiniStatus \} from '\.\.\/locationStatus\.js';/); + assert.match(controls, /const lines = locationMiniStatus\(state\)/); const start = ui.indexOf(' _updateLocationMiniStatus() {'); assert.ok(start > 0, '_updateLocationMiniStatus is missing'); const body = ui.slice(start, ui.indexOf('\n }', start)); - assert.match(body, /locationMiniStatus\(\{[\s\S]*?searchedLabel: this\._searchedLocationLabel,[\s\S]*?\}\)/); + assert.match(body, /_locationControls\?\.renderStatus\(\{[\s\S]*?searchedLabel: this\._searchedLocationLabel,[\s\S]*?\}\)/); // No second copy of the placeholder strings to drift out of sync. assert.doesNotMatch(body, /Location: --/); }); diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 5c1df1fc9..fd076cbcf 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -693,6 +693,8 @@ test('visual input listeners are revoked before asynchronous UI teardown', () => assert.match(synchronous, /this\._visualEffects\.stop\(\)/); assert.match(synchronous, /this\._mapSourceControls\?\.destroy\(\)/); assert.match(synchronous, /this\._clearLayersControl\?\.destroy\(\)/); + assert.match(synchronous, /this\._locationControls\?\.destroy\(\)/); + assert.match(synchronous, /this\._locationLookup\?\.destroy\(\)/); assert.match(synchronous, /this\._displayControls\?\.destroy\(\)/); assert.match(synchronous, /this\._styleParameters\?\.destroy\(\)/); }); diff --git a/src/ui.js b/src/ui.js index 9cee03d13..a7abdff95 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,3 +1,4 @@ +import { LocationControls, LocationSearch } from './ui/location.js'; import { bindClearLayersControl } from './ui/layers.js'; import { createMapSourceControls } from './ui/mapSource.js'; import { VisualEffects, STYLES, GLOBAL_POST_DEFAULTS, STYLE_PRESET_DEFAULTS, MILITARY_DETECTION_PRESET } from './ui/effects.js'; @@ -13,7 +14,6 @@ import { decodeBloomIntensity, } from './bloom.js'; import { LOCATIONS, CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToPresetLocation, flyToPOI, searchAndFlyTo } from './locations.js'; -import { locationMiniStatus } from './locationStatus.js'; import { interruptCameraMotion } from './cameraVerbs.js'; import { aircraftTrackingTarget, @@ -2085,7 +2085,6 @@ export class StyleManager { this._shareTrackingAcquiringKey = null; this._shareTrackingNoticeGeneration = 0; this._globeResetPromise = null; - this._globeResetHandler = null; this._clearSelectedLayersPromise = null; this._clearSelectedLayersManagerPromise = null; this._dataManager = null; @@ -2565,7 +2564,6 @@ export class StyleManager { this._initLocationBar(); this._initShareButton(); this._initClearSelectedLayersButton(); - this._initResetGlobeButton(); this._initHUDToggle(); this._initModels3dToggle(); this._applyGlobalPostDefaults(); @@ -8528,97 +8526,45 @@ export class StyleManager { * @returns {void} */ _initLocationBar() { - const QWERTY_KEYS = ['Q', 'W', 'E', 'R', 'T']; - - // Render city pills (no submenu wrappers — POI row is separate) - for (const [cityId, city] of Object.entries(CITY_POIS)) { - const pill = document.createElement('button'); - pill.className = 'location-pill'; - pill.dataset.locationId = cityId; - pill.textContent = city.name; - pill.addEventListener('click', () => this._onCityPillClick(cityId)); - this._locationPills.appendChild(pill); - } - - // QWERTY keyboard navigation for POIs - this._poiKeydownHandler = (e) => { - if (!this._expandedCityId) return; - // Bail while a form control is focused so POI hotkeys don't fire from a - //