Skip to main content

Static scene resources and layer visibility

GeoJSON source ownership, multiple layers, controls, queries, credits and complete HTML

15.2 connects static GeoJSON sources and all their shared circle/line/fill style layers to shared scenes. Browser main, strict Worker, independent native QuickJS and desktop scripts use the same API. Applications implement their own layer panels and tool buttons. Playback, tracks, line animation, image/terrain ownership and real mobile-device acceptance remain later work; sceneStaticResources means only the documented scope here.

import {map} from 'sqsmap';
if (!(await map.sqs.getCapabilities()).sceneStaticResources) throw Error('Static scenes unavailable');
const scene = await map.sqs.scenes.create({id:'survey'});
map.addSource('survey',{type:'geojson',data:{type:'FeatureCollection',features:[]}});
await map.sqs.flush();
const source = await map.getSource('survey');
await scene.attachSource(source);
map.addLayer({id:'points',type:'circle',source:'survey'});
await map.sqs.flush();
await scene.start();
await scene.pause();
await scene.resume();
await scene.stop();
await scene.detachSource(source);
await map.sqs.scenes.remove(scene);

addSource/getSource/addLayer follow MapLibre-style syntax; extensions live under map.sqs.scenes without changing style JSON. Await attachSource/detachSource with a current source handle or plain {id,sourceInstanceId}, never a string ID alone. Maps still own source data; all shared style layers referencing the owned source follow its visibility. Individual child-layer ownership is unavailable. Attach before adding layers to avoid a draft flashing visible data. URL GeoJSON retains its normal resource loading; static ownership controls visibility and does not suspend independent URL loads.

StateStatic displayLive receivers
draftHiddenOwned receivers make no network request
runningFollows each layer's layout/zoomRuns under existing ownership rules
pausedKeeps visible committed dataCloses owned connections and retains data
stoppedHidden; cannot start/resume againTerminal stop
removeReleases static ownership; restores each layer's settingsStopped receivers remain stopped

Effective visibility intersects scene state with layout.visibility and zoom range. Scenes neither override user-hidden layers nor rewrite layout. queryRenderedFeatures and getAttributions use the same effective gate; source queries still access hidden data. detachSource deletes no data or layer and stops no independent receiver. remove retains sources/layers and restores ordinary visibility, releasing scene control. Script exit does not detach; map destruction releases resources.

A static source has at most one scene; another owner yields CONFLICT. An explicitly owned live receiver on the source must share its static owner. Receivers without owner stay independent; static binding does not adopt them. Sources and receivers count separately against 128 total registrations, including stopped receivers; the 16-scene and 8-receiver map budgets still apply. Repeated same-owner attach/detach is idempotent; stopped scenes cannot attach new resources. Stale, foreign-map and deleted/recreated source or scene identities yield STALE_HANDLE instead of resolving a replacement by name.

getState adds immutable sources:[{id,sourceInstanceId}]; resourceCount=sources.length+receivers.length. sqs.scenechange adds attachSource/detachSource reasons with immutable historical lists. Actual static display changes publish sqs.layerschange with method sqs.scenes.attachSource/start/stop/remove, etc. A pause without a static visibility change publishes no extra layer notice. Attribution changes use sqs.attributionschange. Subscribe before reading state; unsubscribe and destroy explicitly. Ownership, display caches, GPU plans, events and foreign output prepare before commit; failure retains previous state and rendered output.

Native C reuses sqsm_map_scene_json and ABI1. attachSource/detachSource requests are {"method":"attachSource","args":[{"id":"survey","sceneInstanceId":"..."},{"id":"survey","sourceInstanceId":"..."}]}; lifecycle requests take only scene identity. IDs/revisions are opaque strings, never numbers. Free outputs using sqsm_map_bytes_free. The complete C example includes creation, source/layer registration, binding, controls, detach, removal and cleanup.

See complete HTML, the same portable script, matched SDK and receiver ownership rules. Put HTML in the extracted SDK root and serve over HTTP. All CSS, button handlers, state, credits and destruction are included below.

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>SqsMap static scenes</title>
<style>body{margin:0;font:16px system-ui}#map{height:70vh}#tools{display:flex;flex-wrap:wrap;gap:.5rem;padding:1rem}button{font:inherit;padding:.4rem}pre,footer{padding:0 1rem;white-space:pre-wrap}</style>
<div id="map"></div><div id="tools"><button id="start">Start</button><button id="pause">Pause</button><button id="resume">Resume</button><button id="stop">Stop</button><button id="detach">Detach source</button><button id="remove">Remove scene</button></div><pre id="status">Loading</pre><footer id="credits"></footer>
<script type="module">
import {MapView} from './dist/index.js';
let map,scene,source;const offs=[],status=document.querySelector('#status');
async function show(){status.textContent=JSON.stringify({scene:scene?await scene.getState():null,layer:(await map.getStyleLayers()).find(layer=>layer.id==='points')},null,2);}
async function act(fn){try{await fn();await show();}catch(error){status.textContent=String(error);}}
function requireScene(){if(!scene)throw Error('Scene has been removed.');return scene;}
try{
 map=await MapView.create(document.querySelector('#map'),{worker:false});if(!map.capabilities.sceneStaticResources)throw Error('Matching static scene SDK/WASM required.');
 map.jumpTo({center:[0,0],zoom:12});scene=await map.sqs.scenes.create({id:'sample',name:'Static points'});
 map.addSource('points',{type:'geojson',attribution:'Synthetic example data, supplied by this page',data:{type:'FeatureCollection',features:[-.003,.003].map((lng,id)=>({type:'Feature',id,properties:{},geometry:{type:'Point',coordinates:[lng,0]}}))}});await map.flush();source=await map.getSource('points');await scene.attachSource(source);
 // Add layers after attaching so the draft never flashes visible data.
 map.addLayer({id:'points',type:'circle',source:'points',paint:{'circle-color':'#ff0000','circle-radius':8}});await map.flush();
 offs.push(map.on('sqs.scenechange',event=>{status.textContent=JSON.stringify({reason:event.reason,scene:event.scene},null,2);}));
 offs.push(map.onStyleLayersChange(()=>{act(show);}));
 offs.push(map.onAttributionsChange(event=>{document.querySelector('#credits').textContent=event.attributions.text;}));
 document.querySelector('#credits').textContent=(await map.getAttributions()).text;await show();
 for(const method of ['start','pause','resume','stop'])document.querySelector('#'+method).onclick=()=>act(()=>requireScene()[method]());
 document.querySelector('#detach').onclick=()=>act(()=>requireScene().detachSource(source));
 document.querySelector('#remove').onclick=()=>act(async()=>{await map.sqs.scenes.remove(requireScene());scene=null;});
}catch(error){status.textContent=String(error);}
window.addEventListener('pagehide',()=>{for(const off of offs)off();map?.destroy();},{once:true});
</script></html>