下载外部图片
下载外部图片(10.2b)
await map.loadImage(url) 下载并解码 PNG、JPEG 或 WebP,返回 {data:{width,height,data:Uint8Array}}。可以直接将 result.data 传给 map.addImage。加载不会自动注册图片、改变样式或提交地图写入;返回的像素由调用方拥有,修改它不会改变已注册图片。名称和 {data} 使用 MapLibre 风格;这里统一返回 RGBA8 像素,避免三端依赖 DOM 图片对象。
interface MapImageLoadResult {
readonly data: {readonly width:number; readonly height:number; readonly data:Uint8Array}
}
map.loadImage(url:string):Promise<MapImageLoadResult>
地址须为不含用户名密码的绝对HTTP(S)或自定义层次URL(含host),最多4096个UTF-8字节;HTTP图片服务需要允许跨域,自定义处理器由宿主提供。根据实际文件字节识别格式;不支持 SVG/GIF,不隐式转换 EXIF 方向、色彩空间或动画帧。RGBA8 保留直通透明度,不预乘。
每张压缩图片最多4 MiB、边长最多512像素、解码像素最多1 MiB;最多8个并行加载。浏览器和动态库按地图限制,桌面后台下载还有进程级8任务限制。动态库完成但尚未领取的结果仍占名额;调用方领取后保存的像素不计入在途名额。解码器内部另有16 MiB分配限制。
错误包括 INVALID_ARGUMENT(URL)、IO_ERROR(HTTP/空响应/损坏文件)、CAPABILITY_UNAVAILABLE(格式)、RESOURCE_LIMIT(预算)和 CANCELLED(取消)。脚本停止和地图销毁会取消它们拥有的下载;迟到结果不能复活请求。独立网页 SDK 还提供 view.loadImage(url,{signal}),并经 transformRequest(url,ResourceType.Image);函数型请求钩子只能用于主线程宿主。普通跨端脚本只使用一个 URL 参数,不传 DOM AbortSignal。
// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import {map} from "sqsmap";
const result=await map.loadImage("https://www.sqsmap.com/sdk/sqsmap-core/load-image-fixtures/icon.png");
map.addImage("marker",result.data);
await map.sqs.flush();
export default {width:result.data.width,height:result.data.height};
以上公开 URL 需要部署配套附件后才能使用;本机验收使用本地服务。同一份四入口验收脚本使用明确标记的本地测试 URL;完整 HTML/CSS、下载/取消/移除/销毁及署名事件使用同目录附件,可复制整个 SDK 分发目录后运行。
动态库不内置 HTTP 客户端。sqsm_map_image_json 的 loadImage([url]) 返回 requestId;宿主轮询 sqsm_map_resource_requests_json,对 resourceType=Image 下载并调用现有 resource_response;传输失败以 error/errorCode 回传。loadImageResult([requestId]) 返回 pending 或独立 data,成功分配输出后才释放结果。失败输出不消费请求;错误结果需调用 cancelLoadImage([requestId]) 释放。能力位 SQSM_MAP_LOAD_IMAGE=4194304,ABI1及导出数保持不变。原生持续脚本会话等待该资源队列;原生一次性同步运行入口对外部图片加载明确报 CAPABILITY_UNAVAILABLE。桌面 ScriptService 自有后台传输,普通一次性脚本可以 await。
桌面、浏览器主线程/Worker 和 Linux 动态库持续 QuickJS 已接通。移动应用脚本接入、Windows/macOS实测、硬件后端及正式发行仍独立验收;动态图像、伸缩图像和完整字体继续实施。
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SqsMap external image</title>
<style>body{margin:0;font:14px system-ui}#map{height:65vh;min-height:240px}.tools{display:flex;gap:8px;padding:12px;flex-wrap:wrap}input{min-width:260px;flex:1}#status,#attribution{padding:8px 12px;white-space:pre-wrap}</style>
<div id="map"></div><div class="tools"><input id="url" aria-label="Image URL"><button id="load" disabled>Load PNG/JPEG/WebP</button><button id="cancel" disabled>Cancel download</button><button id="remove" disabled>Remove image</button><button id="destroy" disabled>Destroy map</button></div><div id="status" role="status">Starting…</div><div id="attribution"></div>
<script type="module">
import {MapView} from './dist/index.js';
const status=document.querySelector('#status'),url=document.querySelector('#url'),load=document.querySelector('#load'),cancel=document.querySelector('#cancel'),remove=document.querySelector('#remove'),destroy=document.querySelector('#destroy');
url.value=new URL('./load-image-fixtures/icon.png',import.meta.url).href;
const view=await MapView.create(document.querySelector('#map'));let alive=true,pending;
const report=error=>{if(alive)status.textContent=(error.code?error.code+': ':'')+String(error.message??error);};
const offAttribution=view.on('sqs.attributionschange',event=>{document.querySelector('#attribution').textContent=event.attributions.text;});
const offImages=view.on('sqs.imageschange',event=>{if(alive)status.textContent='Image '+event.method+': '+event.id;});
view.jumpTo({center:[0,0],zoom:0});view.setStyle(new URL('./missing-image-style.json',import.meta.url).href);
try {
const deadline=Date.now()+15000;while(!(['ready','degraded'].includes(view.basemapStatus.state)&&view.basemapStatus.readyTiles>0)){if(Date.now()>deadline)throw Error('Style did not become ready');await new Promise(r=>setTimeout(r,30));}
document.querySelector('#attribution').textContent=(await view.getAttributions()).text;load.disabled=remove.disabled=destroy.disabled=false;status.textContent='Ready to load an external image';
}catch(error){report(error);destroy.disabled=false;}
load.onclick=async()=>{
if(!alive||pending)return;const controller=new AbortController();pending=controller;load.disabled=true;cancel.disabled=false;status.textContent='Downloading…';
try {const image=await view.loadImage(url.value,{signal:controller.signal});if(!alive||controller.signal.aborted)return;
if(await view.hasImage('marker'))view.removeImage('marker');view.addImage('marker',image.data);await view.flush({render:true});if(alive)status.textContent='Loaded '+image.data.width+' × '+image.data.height;
}catch(error){report(error);}finally{if(pending===controller)pending=undefined;if(alive){load.disabled=false;cancel.disabled=true;}}
};
cancel.onclick=()=>pending?.abort();
remove.onclick=async()=>{try{if(await view.hasImage('marker')){view.removeImage('marker');await view.flush({render:true});}if(alive)status.textContent='Image removed';}catch(error){report(error);}};
destroy.onclick=()=>{alive=false;pending?.abort();offAttribution();offImages();view.destroy();for(const button of [load,cancel,remove,destroy])button.disabled=true;url.disabled=true;status.textContent='Map destroyed';};
window.addEventListener('pagehide',()=>{if(alive)destroy.onclick();},{once:true});
</script></html>
已交付但尚未完成的下载取消后,动态库会输出 cancel:true 的 Image 描述符。宿主应按生成代中止其拥有的网络请求;取消通知在成功交付前继续占用一个名额,避免通知积压无界。
独立图片的自定义URI和宿主条件见自定义协议图片加载。