点聚合
共同点聚合
匹配的浏览器主线程/Worker SDK、桌面脚本和动态库 QuickJS 使用相同图源选项与方法。原生 C 沿用 sqsm_map_source_json 通道,检查 SQSM_MAP_CLUSTERING(16777216)能力标志。跨端图源查询统一返回 Promise,包括 getClusterOptions;setClusterOptions 返回 Promise
cluster 默认为 false;clusterRadius 默认为50逻辑像素,以512像素世界瓦片定义,范围0..512;clusterMaxZoom 默认17,接受0..23并把小数四舍五入;clusterMinPoints 默认2,范围2..4096。clusterProperties 最多8项标量统计,接受数字 +、-、*、/、min、max,布尔 all/any,字符串 concat 简写,或自定义 reduce 表达式,格式为 归约,映射表达式。映射及归约可以返回有限数字、布尔、字符串;null/数组/对象结果不支持,返回 INVALID_ARGUMENT。共同 case/match/coalesce/to-string 及算术表达式可组合;accumulated 仅在归约中合法,get 读取合入子节点已声明统计字段,可跨统计项读取。初值来自第一个子节点,按稳定身份顺序逐层合并,单子节点复用结果。自定义归约可改变标量类型,但后续算术/布尔运算不隐式转换;concat按共同 to-string 转换。数字转文本去除整数的 .0,负零写为0,使用1e-6和1e21的固定/指数边界。非结合运算和浮点次序不承诺与其他引擎相同,推荐可结合归约。每表达式最多32层/1024 JSON节点/估算64KiB,每源预计800万求值节点。每字符串结果最多4096 UTF-8字节,映射缓冲和每工作层分别最多估算8MiB,持久索引仍32MiB;临时缓冲与索引分开计数。错误或超限不修改旧数据、索引、状态和修订。zoom/feature-state依赖仍返回 CAPABILITY_UNAVAILABLE。SDK能力 clusterScalarProperties;C能力 SQSM_MAP_CLUSTER_SCALARS(67108864),保留数字归约标志。 setClusterOptions 仅接受 cluster/clusterRadius/clusterMaxZoom;规范化后相同的配置不重建索引或改变聚合 ID。
启用聚合后,数据只接受 Feature 或 FeatureCollection 中非空 Point,最多4096点,纬度限制墨卡托可表示的 ±85.0511287798066。建议使用稳定且唯一的显式 ID 或 promoteId;无 ID 使用输入位置。聚合按整数缩放层级选择,不跨日期变更线合并;半径0只合并重合点。超过 clusterMaxZoom 显示原始点。图层面板和按钮由用户编写,注册图源后另添加 circle 图层。
querySourceFeatures 与 queryRenderedFeatures 按当前整数 zoom 返回地图实际显示的聚合点。其 properties 包含 cluster=true、数字 cluster_id、point_count、point_count_abbreviated 和配置的统计值;要素 id 为字符串 'sqs:cluster:<cluster_id>',不会与原始数字 ID 混淆。输入不能使用该 ID 前缀或保留聚合属性。getData 始终返回原始 GeoJSON。getClusterLeaves(id,limit=10,offset=0) 返回按稳定身份排序的原始成员及图源实例/数据修订,分页1..1000,offset为0..4096,输出不超过8MiB(脚本仍受更严格的64KiB传输预算约束)。getClusterChildren 返回首次分裂后的子节点,getClusterExpansionZoom 返回对应整数层级。
聚合 ID 属于当前图源及索引修订,数据或配置更新后旧 ID 返回 STALE_HANDLE。关闭聚合后的成员查询返回 CAPABILITY_UNAVAILABLE。图源句柄可以保留,但写入后需重新查询聚合点。数据/配置和显示共同准备后提交,失败保留原地图与索引。重建会清除旧聚合点状态,保留原始点状态;generateId 整体替换仍清空位置身份的状态。配置改变也推进图源 dataRevision,并使用共同 sourcedata/sqs.sourceschange 事件。
公开语法参考:GeoJSON 选项、GeoJSONSource、GetClusterOptions、SetClusterOptions。以上预算和异步查询是明确的 SqsMap 差异;移动设备集成、容器统计、实时源和完整场景所有权仍待实施。
import {map} from 'sqsmap';
map.addSource('points',{type:'geojson',cluster:true,clusterMaxZoom:8,
clusterProperties:{total:['+',['get','weight']]},data:{type:'FeatureCollection',features:[
{type:'Feature',id:'a',properties:{weight:3},geometry:{type:'Point',coordinates:[0,0]}},
{type:'Feature',id:'b',properties:{weight:4},geometry:{type:'Point',coordinates:[.01,0]}}
]}});
map.addLayer({id:'points',type:'circle',source:'points',paint:{'circle-radius':10}});
map.jumpTo({center:[0,0],zoom:0});await map.sqs.flush();
const source=await map.getSource('points');
const cluster=(await map.querySourceFeatures('points')).find(f=>f.properties.cluster);
if(cluster){
const id=cluster.properties.cluster_id;
const page=await source.getClusterLeaves(id,10,0);
const children=await source.getClusterChildren(id);
const zoom=await source.getClusterExpansionZoom(id);
map.jumpTo({center:cluster.geometry.coordinates.slice(0,2),zoom});await map.sqs.flush();
}
await source.setClusterOptions({clusterRadius:60});
const options=await source.getClusterOptions();
完整 HTML/CSS/脚本
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SqsMap point clustering</title><style>html,body{height:100%;margin:0;font-family:system-ui}#map{height:100%}.tools{position:absolute;top:12px;left:12px;background:white;padding:12px;border-radius:8px;max-width:330px}button{padding:6px;margin:2px}pre{white-space:pre-wrap}#credits{position:absolute;bottom:0;right:0;background:#fffd;padding:4px;font-size:12px}</style></head><body><div id="map"></div><section class="tools"><strong>Point clustering</strong><p>Click a blue cluster to expand. Red points are original features.</p><div><button id="overview" disabled>Overview</button><button id="expand" disabled>Expand</button><button id="members" disabled>Member page</button><button id="toggle" disabled>Toggle clustering</button></div><pre id="log" role="status">Loading</pre></section><div id="credits"></div><script type="module">
// Serve next to a matched standalone SqsMap SDK's dist/ and wasm/ directories.
import {MapView} from './dist/index.js';
const log=document.querySelector('#log'),buttons=[...document.querySelectorAll('button')];let map,source,enabled=true;const subscriptions=[];
async function run(action){buttons.forEach(b=>b.disabled=true);try{await action();}catch(error){log.textContent=String(error);}finally{if(map)buttons.forEach(b=>b.disabled=false);}}
async function group(){return(await map.querySourceFeatures('points')).find(f=>f.properties.cluster===true);}
async function expand(feature){if(!feature){log.textContent='No cluster at this zoom';return;}const zoom=await source.getClusterExpansionZoom(feature.properties.cluster_id);map.jumpTo({center:feature.geometry.coordinates.slice(0,2),zoom});await map.flush();log.textContent='Expanded to zoom '+zoom;}
try{
map=await MapView.create(document.querySelector('#map'),{worker:false});if(!map.capabilities.clusterScalarProperties)throw Error('Matched clustering SDK/WASM required');
subscriptions.push(map.on('sqs.attributionschange',event=>document.querySelector('#credits').textContent=event.attributions.text));
map.jumpTo({center:[.025,0],zoom:0});
map.addSource('points',{type:'geojson',cluster:true,clusterMaxZoom:8,attribution:'Example data © SqsMap',clusterProperties:{total:[['+',['accumulated'],['get','total']],['get','weight']],labels:['concat',['get','name']],allActive:['all',['get','active']],anyActive:['any',['get','active']]},data:{type:'FeatureCollection',features:Array.from({length:6},(_,i)=>({type:'Feature',id:'p'+i,properties:{weight:i+1,name:'p'+i,active:i!==1},geometry:{type:'Point',coordinates:[i*.01,0]}}))}});
map.addLayer({id:'points',type:'circle',source:'points',paint:{'circle-radius':['case',['has','point_count'],12,3],'circle-color':['case',['has','point_count'],'#0000ff','#ff0000']}});await map.flush();source=await map.getSource('points');
document.querySelector('#credits').textContent=(await map.getAttributions()).text;
subscriptions.push(map.on('click','points',event=>run(async()=>{const f=event.features[0];if(f?.properties.cluster)await expand(f);else if(f)log.textContent=JSON.stringify(f.properties);})));
document.querySelector('#overview').onclick=()=>run(async()=>{map.jumpTo({center:[.025,0],zoom:0});await map.flush();log.textContent='Overview';});
document.querySelector('#expand').onclick=()=>run(async()=>expand(await group()));
document.querySelector('#members').onclick=()=>run(async()=>{const f=await group();if(!f){log.textContent='No cluster at this zoom';return;}const members=await source.getClusterLeaves(f.properties.cluster_id,2,0);log.textContent=JSON.stringify({count:f.properties.point_count,total:f.properties.total,labels:f.properties.labels,allActive:f.properties.allActive,anyActive:f.properties.anyActive,firstPage:members.map(m=>({id:m.id,weight:m.properties.weight}))},null,2);});
document.querySelector('#toggle').onclick=()=>run(async()=>{await source.setClusterOptions({cluster:!enabled});enabled=!enabled;log.textContent='Clustering '+(enabled?'enabled':'disabled');});
buttons.forEach(b=>b.disabled=false);log.textContent='Ready';
}catch(error){log.textContent=String(error);}
window.addEventListener('pagehide',()=>{subscriptions.forEach(off=>off());map?.destroy();},{once:true});
</script></body></html>
匹配附件
Complete HTML · Portable script · Source declarations · Native header
自定义统计跨端验证脚本(本阶段5个宿主各27项检查):cluster-reducers.js。
标量统计跨端验证脚本(五宿主各55项):cluster-scalars.js。