@movk/maplibre 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/components/extensions/ZztsLayer.d.vue.ts +60 -0
- package/dist/runtime/components/extensions/ZztsLayer.vue +78 -0
- package/dist/runtime/components/extensions/ZztsLayer.vue.d.ts +60 -0
- package/dist/runtime/utils/zzts-cache.d.ts +24 -0
- package/dist/runtime/utils/zzts-cache.js +76 -0
- package/dist/runtime/utils/zzts-clip.d.ts +14 -0
- package/dist/runtime/utils/zzts-clip.js +27 -0
- package/dist/runtime/utils/zzts-tile.d.ts +56 -11
- package/dist/runtime/utils/zzts-tile.js +101 -37
- package/dist/runtime/utils/zzts.d.ts +18 -3
- package/dist/runtime/utils/zzts.js +111 -48
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addP
|
|
|
3
3
|
import { defu } from 'defu';
|
|
4
4
|
|
|
5
5
|
const name = "@movk/maplibre";
|
|
6
|
-
const version = "1.2.
|
|
6
|
+
const version = "1.2.1";
|
|
7
7
|
|
|
8
8
|
const OPTIONAL_DEPS = ["lottie-web", "pmtiles", "@geomatico/maplibre-cog-protocol", "maplibre-contour"];
|
|
9
9
|
const module$1 = defineNuxtModule({
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FitBoundsOptions } from 'maplibre-gl';
|
|
2
|
+
import type { RasterLayerSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
3
|
+
import type { MaskInput } from '../../utils/mask';
|
|
4
|
+
/** ZZTS 二维动态切片影像:经 zzts:// 协议拼合为 512 墨卡托瓦片,支持多边形裁剪、定位与高分屏出图,可用于 globe 与地形场景。 */
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
/** media 接口地址(不含协议前缀),查询参数(`layerName`、`dictCode`、`token` 等)原样带给服务端 */
|
|
7
|
+
url: string;
|
|
8
|
+
/**
|
|
9
|
+
* 协议名;需自定义请求时先以该名称调用 `registerZztsProtocol`,否则按缺省配置自动注册
|
|
10
|
+
* @defaultValue 'zzts'
|
|
11
|
+
*/
|
|
12
|
+
scheme?: string;
|
|
13
|
+
/** source/layer id;省略时自动生成,变更需配合 `:key` 重建 */
|
|
14
|
+
layerId?: string;
|
|
15
|
+
/** 显示范围:仅保留区域内的影像像素,区域外露出下层底图;孔洞(飞地)同样挖空 */
|
|
16
|
+
clip?: MaskInput;
|
|
17
|
+
/**
|
|
18
|
+
* 拼合画布倍率(1-4),设为 `devicePixelRatio` 可在高分屏下取更细一级网格
|
|
19
|
+
* @defaultValue 1
|
|
20
|
+
*/
|
|
21
|
+
pixelRatio?: number;
|
|
22
|
+
/**
|
|
23
|
+
* 元数据加载后自动定位到图层范围(与 `clip` 求交)
|
|
24
|
+
* @defaultValue false
|
|
25
|
+
*/
|
|
26
|
+
autoFit?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 定位动画选项,`autoFit` 与暴露的 `fit()` 共用
|
|
29
|
+
* @see https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FitBoundsOptions/
|
|
30
|
+
*/
|
|
31
|
+
fitOptions?: FitBoundsOptions;
|
|
32
|
+
/**
|
|
33
|
+
* 不透明度
|
|
34
|
+
* @defaultValue 1
|
|
35
|
+
*/
|
|
36
|
+
opacity?: number;
|
|
37
|
+
/**
|
|
38
|
+
* raster 图层 paint(色相、饱和度、对比度、亮度等),与 `opacity` 合并
|
|
39
|
+
* @see https://maplibre.org/maplibre-style-spec/layers/#raster
|
|
40
|
+
*/
|
|
41
|
+
paint?: RasterLayerSpecification['paint'];
|
|
42
|
+
/** 数据源最小缩放级别,缺省 0 */
|
|
43
|
+
minzoom?: number;
|
|
44
|
+
/** 数据源最大缩放级别,缺省按影像原始分辨率推算,更高级别自动超采样 */
|
|
45
|
+
maxzoom?: number;
|
|
46
|
+
/** 插入到该图层之前 */
|
|
47
|
+
beforeId?: string;
|
|
48
|
+
};
|
|
49
|
+
/** 定位到图层范围;元数据尚未加载时返回 false */
|
|
50
|
+
declare function fit(options?: FitBoundsOptions): boolean;
|
|
51
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
52
|
+
fit: typeof fit;
|
|
53
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
54
|
+
pixelRatio: number;
|
|
55
|
+
opacity: number;
|
|
56
|
+
scheme: string;
|
|
57
|
+
autoFit: boolean;
|
|
58
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
59
|
+
declare const _default: typeof __VLS_export;
|
|
60
|
+
export default _default;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onUnmounted, ref, useId, watch } from "vue";
|
|
3
|
+
import { omitUndefined } from "@movk/core";
|
|
4
|
+
import MaplibreSource from "../Source.vue";
|
|
5
|
+
import MaplibreLayer from "../Layer.vue";
|
|
6
|
+
import { useMap } from "../../composables/useMap";
|
|
7
|
+
import { registerZztsProtocol } from "../../utils/zzts";
|
|
8
|
+
import { deleteZztsClip, setZztsClip } from "../../utils/zzts-clip";
|
|
9
|
+
import { ZZTS_TILE_SIZE, zztsSourceUrl } from "../../utils/zzts-tile";
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
url: { type: String, required: true },
|
|
12
|
+
scheme: { type: String, required: false, default: "zzts" },
|
|
13
|
+
layerId: { type: String, required: false },
|
|
14
|
+
clip: { type: Object, required: false },
|
|
15
|
+
pixelRatio: { type: Number, required: false, default: 1 },
|
|
16
|
+
autoFit: { type: Boolean, required: false, default: false },
|
|
17
|
+
fitOptions: { type: Object, required: false },
|
|
18
|
+
opacity: { type: Number, required: false, default: 1 },
|
|
19
|
+
paint: { type: Object, required: false },
|
|
20
|
+
minzoom: { type: Number, required: false },
|
|
21
|
+
maxzoom: { type: Number, required: false },
|
|
22
|
+
beforeId: { type: String, required: false }
|
|
23
|
+
});
|
|
24
|
+
const id = props.layerId ?? `movk-zzts-${useId()}`;
|
|
25
|
+
const ctx = useMap();
|
|
26
|
+
let clipVersion = 0;
|
|
27
|
+
const clipKey = ref(props.clip ? `${id}:0` : void 0);
|
|
28
|
+
function registerClip() {
|
|
29
|
+
if (props.clip && clipKey.value) setZztsClip(clipKey.value, props.clip);
|
|
30
|
+
}
|
|
31
|
+
watch(() => props.clip, (clip) => {
|
|
32
|
+
if (clipKey.value) deleteZztsClip(clipKey.value);
|
|
33
|
+
clipKey.value = clip ? `${id}:${++clipVersion}` : void 0;
|
|
34
|
+
registerClip();
|
|
35
|
+
}, { deep: true });
|
|
36
|
+
const stopReady = ctx.onReady(() => {
|
|
37
|
+
registerZztsProtocol({ scheme: props.scheme });
|
|
38
|
+
registerClip();
|
|
39
|
+
});
|
|
40
|
+
const sourceUrl = computed(() => zztsSourceUrl(props.scheme, props.url, { pixelRatio: props.pixelRatio, clip: clipKey.value }));
|
|
41
|
+
const source = computed(() => omitUndefined({
|
|
42
|
+
type: "raster",
|
|
43
|
+
url: sourceUrl.value,
|
|
44
|
+
tileSize: ZZTS_TILE_SIZE,
|
|
45
|
+
minzoom: props.minzoom,
|
|
46
|
+
maxzoom: props.maxzoom
|
|
47
|
+
}));
|
|
48
|
+
const layerPaint = computed(() => ({ ...props.paint, "raster-opacity": props.opacity }));
|
|
49
|
+
function fit(options) {
|
|
50
|
+
const map = ctx.map.value;
|
|
51
|
+
const bounds = map?.getSource(id)?.bounds;
|
|
52
|
+
if (!map || !bounds) return false;
|
|
53
|
+
map.fitBounds(bounds, { ...props.fitOptions, ...options });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
let fittedUrl;
|
|
57
|
+
watch(ctx.map, (map, _, onCleanup) => {
|
|
58
|
+
if (!map) return;
|
|
59
|
+
const onSourceData = (event) => {
|
|
60
|
+
if (!props.autoFit || event.sourceId !== id || event.sourceDataType !== "metadata") return;
|
|
61
|
+
if (fittedUrl === sourceUrl.value) return;
|
|
62
|
+
if (fit()) fittedUrl = sourceUrl.value;
|
|
63
|
+
};
|
|
64
|
+
map.on("sourcedata", onSourceData);
|
|
65
|
+
onCleanup(() => map.off("sourcedata", onSourceData));
|
|
66
|
+
}, { immediate: true });
|
|
67
|
+
onUnmounted(() => {
|
|
68
|
+
stopReady();
|
|
69
|
+
if (clipKey.value) deleteZztsClip(clipKey.value);
|
|
70
|
+
});
|
|
71
|
+
defineExpose({ fit });
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<MaplibreSource :source-id="id" :source="source">
|
|
76
|
+
<MaplibreLayer :layer-id="id" type="raster" :source="id" :paint="layerPaint" :before-id="beforeId" />
|
|
77
|
+
</MaplibreSource>
|
|
78
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FitBoundsOptions } from 'maplibre-gl';
|
|
2
|
+
import type { RasterLayerSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
3
|
+
import type { MaskInput } from '../../utils/mask';
|
|
4
|
+
/** ZZTS 二维动态切片影像:经 zzts:// 协议拼合为 512 墨卡托瓦片,支持多边形裁剪、定位与高分屏出图,可用于 globe 与地形场景。 */
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
/** media 接口地址(不含协议前缀),查询参数(`layerName`、`dictCode`、`token` 等)原样带给服务端 */
|
|
7
|
+
url: string;
|
|
8
|
+
/**
|
|
9
|
+
* 协议名;需自定义请求时先以该名称调用 `registerZztsProtocol`,否则按缺省配置自动注册
|
|
10
|
+
* @defaultValue 'zzts'
|
|
11
|
+
*/
|
|
12
|
+
scheme?: string;
|
|
13
|
+
/** source/layer id;省略时自动生成,变更需配合 `:key` 重建 */
|
|
14
|
+
layerId?: string;
|
|
15
|
+
/** 显示范围:仅保留区域内的影像像素,区域外露出下层底图;孔洞(飞地)同样挖空 */
|
|
16
|
+
clip?: MaskInput;
|
|
17
|
+
/**
|
|
18
|
+
* 拼合画布倍率(1-4),设为 `devicePixelRatio` 可在高分屏下取更细一级网格
|
|
19
|
+
* @defaultValue 1
|
|
20
|
+
*/
|
|
21
|
+
pixelRatio?: number;
|
|
22
|
+
/**
|
|
23
|
+
* 元数据加载后自动定位到图层范围(与 `clip` 求交)
|
|
24
|
+
* @defaultValue false
|
|
25
|
+
*/
|
|
26
|
+
autoFit?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 定位动画选项,`autoFit` 与暴露的 `fit()` 共用
|
|
29
|
+
* @see https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FitBoundsOptions/
|
|
30
|
+
*/
|
|
31
|
+
fitOptions?: FitBoundsOptions;
|
|
32
|
+
/**
|
|
33
|
+
* 不透明度
|
|
34
|
+
* @defaultValue 1
|
|
35
|
+
*/
|
|
36
|
+
opacity?: number;
|
|
37
|
+
/**
|
|
38
|
+
* raster 图层 paint(色相、饱和度、对比度、亮度等),与 `opacity` 合并
|
|
39
|
+
* @see https://maplibre.org/maplibre-style-spec/layers/#raster
|
|
40
|
+
*/
|
|
41
|
+
paint?: RasterLayerSpecification['paint'];
|
|
42
|
+
/** 数据源最小缩放级别,缺省 0 */
|
|
43
|
+
minzoom?: number;
|
|
44
|
+
/** 数据源最大缩放级别,缺省按影像原始分辨率推算,更高级别自动超采样 */
|
|
45
|
+
maxzoom?: number;
|
|
46
|
+
/** 插入到该图层之前 */
|
|
47
|
+
beforeId?: string;
|
|
48
|
+
};
|
|
49
|
+
/** 定位到图层范围;元数据尚未加载时返回 false */
|
|
50
|
+
declare function fit(options?: FitBoundsOptions): boolean;
|
|
51
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
52
|
+
fit: typeof fit;
|
|
53
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
54
|
+
pixelRatio: number;
|
|
55
|
+
opacity: number;
|
|
56
|
+
scheme: string;
|
|
57
|
+
autoFit: boolean;
|
|
58
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
59
|
+
declare const _default: typeof __VLS_export;
|
|
60
|
+
export default _default;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Request = typeof globalThis.fetch;
|
|
2
|
+
export interface Lru<K, V> {
|
|
3
|
+
get: (key: K) => V | undefined;
|
|
4
|
+
set: (key: K, value: V) => void;
|
|
5
|
+
delete: (key: K) => void;
|
|
6
|
+
}
|
|
7
|
+
/** 基于 Map 插入顺序的 LRU:读取即刷新,超出容量时淘汰最久未用的条目 */
|
|
8
|
+
export declare function createLru<K, V>(size: number): Lru<K, V>;
|
|
9
|
+
/** 网络错误与 5xx 视为临时故障,按指数退避重试;4xx(含 404 未生成)直接返回 */
|
|
10
|
+
export declare function fetchWithRetry(request: Request, url: string, retry: number, signal?: AbortSignal): Promise<Response>;
|
|
11
|
+
export interface ZztsImageStore {
|
|
12
|
+
/** 取元素图片位图;缺图或加载失败时为 undefined。每次调用都须配对一次 release */
|
|
13
|
+
acquire: (url: string) => Promise<ImageBitmap | undefined>;
|
|
14
|
+
release: (url: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 元素图片仓库:压缩数据(Blob)按 url 做 LRU 缓存,404 结果同样缓存以免重复请求,其余失败不缓存;
|
|
18
|
+
* 解码后的位图按引用计数在并发拼合的瓦片间共享,最后一次释放时 close,避免位图常驻内存。
|
|
19
|
+
*/
|
|
20
|
+
export declare function createImageStore(request: Request, { size, retry }: {
|
|
21
|
+
size: number;
|
|
22
|
+
retry: number;
|
|
23
|
+
}): ZztsImageStore;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const RETRY_DELAY = 500;
|
|
2
|
+
export function createLru(size) {
|
|
3
|
+
const map = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
get(key) {
|
|
6
|
+
if (!map.has(key)) return void 0;
|
|
7
|
+
const value = map.get(key);
|
|
8
|
+
map.delete(key);
|
|
9
|
+
map.set(key, value);
|
|
10
|
+
return value;
|
|
11
|
+
},
|
|
12
|
+
set(key, value) {
|
|
13
|
+
map.delete(key);
|
|
14
|
+
map.set(key, value);
|
|
15
|
+
if (map.size > size) map.delete(map.keys().next().value);
|
|
16
|
+
},
|
|
17
|
+
delete(key) {
|
|
18
|
+
map.delete(key);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function wait(ms, signal) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
25
|
+
const timer = setTimeout(resolve, ms);
|
|
26
|
+
signal?.addEventListener("abort", () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(signal.reason);
|
|
29
|
+
}, { once: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export async function fetchWithRetry(request, url, retry, signal) {
|
|
33
|
+
for (let attempt = 0; ; attempt++) {
|
|
34
|
+
try {
|
|
35
|
+
const res = await request(url, signal ? { signal } : void 0);
|
|
36
|
+
if (res.status < 500 || attempt >= retry) return res;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (signal?.aborted || attempt >= retry) throw error;
|
|
39
|
+
}
|
|
40
|
+
await wait(RETRY_DELAY * 2 ** attempt, signal);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function createImageStore(request, { size, retry }) {
|
|
44
|
+
const blobs = createLru(size);
|
|
45
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
46
|
+
function loadBlob(url) {
|
|
47
|
+
const hit = blobs.get(url);
|
|
48
|
+
if (hit) return hit;
|
|
49
|
+
const pending = fetchWithRetry(request, url, retry).then(async (res) => {
|
|
50
|
+
if (res.status === 404) return void 0;
|
|
51
|
+
if (!res.ok) throw new Error(`ZZTS \u56FE\u7247\u52A0\u8F7D\u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
52
|
+
return await res.blob();
|
|
53
|
+
});
|
|
54
|
+
pending.catch(() => blobs.delete(url));
|
|
55
|
+
blobs.set(url, pending);
|
|
56
|
+
return pending;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
acquire(url) {
|
|
60
|
+
const entry = decoded.get(url);
|
|
61
|
+
if (entry) {
|
|
62
|
+
entry.refs++;
|
|
63
|
+
return entry.image;
|
|
64
|
+
}
|
|
65
|
+
const image = loadBlob(url).then((blob) => blob && createImageBitmap(blob)).catch(() => void 0);
|
|
66
|
+
decoded.set(url, { image, refs: 1 });
|
|
67
|
+
return image;
|
|
68
|
+
},
|
|
69
|
+
release(url) {
|
|
70
|
+
const entry = decoded.get(url);
|
|
71
|
+
if (!entry || --entry.refs > 0) return;
|
|
72
|
+
decoded.delete(url);
|
|
73
|
+
void entry.image.then((image) => image?.close());
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Position } from 'geojson';
|
|
2
|
+
import type { MaskInput } from './mask';
|
|
3
|
+
import type { LngLatBBox } from './zzts-tile';
|
|
4
|
+
/** 裁剪区域:全部环(外环与孔洞,按 evenodd 规则填充)及其包围盒 */
|
|
5
|
+
export interface ZztsClip {
|
|
6
|
+
rings: Position[][];
|
|
7
|
+
bbox: LngLatBBox;
|
|
8
|
+
}
|
|
9
|
+
/** 收集面状几何的全部环;非面状要素被忽略 */
|
|
10
|
+
export declare function clipRings(input: MaskInput): Position[][];
|
|
11
|
+
/** 登记裁剪区域;无面状要素时登记为空区域,图层整体不可见 */
|
|
12
|
+
export declare function setZztsClip(key: string, input: MaskInput): void;
|
|
13
|
+
export declare function getZztsClip(key: string): ZztsClip | undefined;
|
|
14
|
+
export declare function deleteZztsClip(key: string): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineGlobalSingleton } from "@movk/core";
|
|
2
|
+
const clips = defineGlobalSingleton("movk-maplibre:zzts-clips", () => /* @__PURE__ */ new Map());
|
|
3
|
+
function polygonRings(geometry) {
|
|
4
|
+
if (geometry?.type === "Polygon") return geometry.coordinates;
|
|
5
|
+
if (geometry?.type === "MultiPolygon") return geometry.coordinates.flat();
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
export function clipRings(input) {
|
|
9
|
+
const rings = input.type === "FeatureCollection" ? input.features.flatMap((feature) => polygonRings(feature.geometry)) : polygonRings(input.type === "Feature" ? input.geometry : input);
|
|
10
|
+
return rings.filter((ring) => ring.length >= 3);
|
|
11
|
+
}
|
|
12
|
+
function ringsBBox(rings) {
|
|
13
|
+
const points = rings.flat();
|
|
14
|
+
const lngs = points.map(([lng = 0]) => lng);
|
|
15
|
+
const lats = points.map(([, lat = 0]) => lat);
|
|
16
|
+
return [Math.min(...lngs), Math.min(...lats), Math.max(...lngs), Math.max(...lats)];
|
|
17
|
+
}
|
|
18
|
+
export function setZztsClip(key, input) {
|
|
19
|
+
const rings = clipRings(input);
|
|
20
|
+
clips.set(key, { rings, bbox: rings.length ? ringsBBox(rings) : [0, 0, 0, 0] });
|
|
21
|
+
}
|
|
22
|
+
export function getZztsClip(key) {
|
|
23
|
+
return clips.get(key);
|
|
24
|
+
}
|
|
25
|
+
export function deleteZztsClip(key) {
|
|
26
|
+
clips.delete(key);
|
|
27
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Position } from 'geojson';
|
|
1
2
|
/** 经纬度包围盒 [west, south, east, north] */
|
|
2
3
|
export type LngLatBBox = [number, number, number, number];
|
|
3
4
|
export interface ZztsExtent {
|
|
@@ -27,20 +28,63 @@ export interface TileCoord {
|
|
|
27
28
|
x: number;
|
|
28
29
|
y: number;
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
+
/** 同一级别的连续瓦片区间,x1 / y1 不含 */
|
|
32
|
+
export interface TileRange {
|
|
33
|
+
z: number;
|
|
34
|
+
x0: number;
|
|
35
|
+
y0: number;
|
|
36
|
+
x1: number;
|
|
37
|
+
y1: number;
|
|
38
|
+
}
|
|
39
|
+
/** 图层级参数,经协议地址的 hash 传递,不发往服务端 */
|
|
40
|
+
export interface ZztsLayerParams {
|
|
41
|
+
/** 拼合画布相对 512 的倍率,用于高分屏 */
|
|
42
|
+
pixelRatio?: number;
|
|
43
|
+
/** 裁剪表中的裁剪区域键 */
|
|
44
|
+
clip?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ZztsRequest {
|
|
47
|
+
mediaUrl: string;
|
|
48
|
+
pixelRatio: number;
|
|
49
|
+
clip?: string;
|
|
50
|
+
tile?: TileCoord;
|
|
51
|
+
}
|
|
52
|
+
/** 一次 elements 请求的参数 */
|
|
53
|
+
export interface ZztsQuery {
|
|
54
|
+
bbox: LngLatBBox;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
scale: number;
|
|
58
|
+
}
|
|
59
|
+
/** 源矩形为元素图片的比例坐标 [x, y, w, h](0-1),目标矩形为画布像素坐标 */
|
|
31
60
|
export interface ZztsDraw {
|
|
32
61
|
source: [number, number, number, number];
|
|
33
62
|
target: [number, number, number, number];
|
|
34
63
|
}
|
|
35
64
|
export declare const ZZTS_TILE_SIZE = 512;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
export declare function
|
|
65
|
+
/** 解析 `<scheme>://<media 地址>#<图层参数>&tile=z/x/y`;scheme 任意,以首个 `://` 为界 */
|
|
66
|
+
export declare function parseZztsUrl(url: string): ZztsRequest;
|
|
67
|
+
/** 拼出 source 的协议地址,缺省参数不写入 hash */
|
|
68
|
+
export declare function zztsSourceUrl(scheme: string, mediaUrl: string, { pixelRatio, clip }?: ZztsLayerParams): string;
|
|
69
|
+
export declare function canvasSize(pixelRatio: number): number;
|
|
70
|
+
export declare function tileRange({ z, x, y }: TileCoord): TileRange;
|
|
71
|
+
export declare function rangeBBox({ z, x0, y0, x1, y1 }: TileRange): LngLatBBox;
|
|
72
|
+
export declare function tileBBox(tile: TileCoord): LngLatBBox;
|
|
73
|
+
export declare function extentBBox({ xmin, ymin, xmax, ymax }: ZztsExtent): LngLatBBox;
|
|
74
|
+
export declare function intersectBBox(a: Readonly<LngLatBBox>, b: Readonly<LngLatBBox>): LngLatBBox | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* 瓦片所在的合并请求区间:k×k 个相邻瓦片共用一次 elements 请求。
|
|
77
|
+
* k 取不超过 size 的 2 的幂,并保证区间经度跨度不超过服务端上限,以便各级别分组对齐。
|
|
78
|
+
*/
|
|
79
|
+
export declare function metatileRange(tile: TileCoord, size: number): TileRange;
|
|
41
80
|
/** 瓦片在给定纬度处的比例尺分母,与公司 Cesium 组件的取值口径一致,保证服务端选出相同的网格级别 */
|
|
42
81
|
export declare function tileScale(z: number, lat: number): number;
|
|
43
|
-
|
|
82
|
+
/**
|
|
83
|
+
* 规划区间的 elements 请求:bbox 裁到图层范围,经度跨度超限时等分成多段;
|
|
84
|
+
* 画布尺寸按裁剪后的像素范围等比缩小。`coarser` 为向上取粗的级数,每级比例尺翻倍,服务端随之降一级网格。
|
|
85
|
+
*/
|
|
86
|
+
export declare function planQueries(range: TileRange, extent: Readonly<LngLatBBox>, pixelRatio: number, coarser?: number): ZztsQuery[];
|
|
87
|
+
export declare function zztsTileJson(url: string, media: ZztsMedia, clip?: Readonly<LngLatBBox>): {
|
|
44
88
|
tiles: string[];
|
|
45
89
|
bounds: LngLatBBox;
|
|
46
90
|
minzoom: number;
|
|
@@ -48,7 +92,8 @@ export declare function zztsTileJson(url: string, media: ZztsMedia): {
|
|
|
48
92
|
tileSize: number;
|
|
49
93
|
name: string;
|
|
50
94
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
95
|
+
export declare function elementsUrl(mediaUrl: string, { bbox, width, height, scale }: ZztsQuery): string;
|
|
96
|
+
/** 规划元素图片在画布上的绘制:经向线性;纬向在低级别按等高条带分段,逐段换算墨卡托以消除拉伸误差 */
|
|
97
|
+
export declare function planDraws(extent: ZztsExtent, tile: TileCoord, size?: number): ZztsDraw[];
|
|
98
|
+
/** 把经纬度环投影为画布像素坐标,供裁剪路径使用 */
|
|
99
|
+
export declare function projectRings(rings: Position[][], { z, x, y }: TileCoord, size: number): [number, number][][];
|
|
@@ -1,60 +1,121 @@
|
|
|
1
1
|
export const ZZTS_TILE_SIZE = 512;
|
|
2
|
-
const
|
|
3
|
-
const TILE_HASH = /#(\d+)\/(\d+)\/(\d+)$/;
|
|
2
|
+
const SCHEME_SEPARATOR = "://";
|
|
4
3
|
const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
|
|
4
|
+
const MAX_LAT = 85.0511287798;
|
|
5
5
|
const METERS_PER_PIXEL_TO_SCALE = 96 / 0.0254;
|
|
6
6
|
const MAX_BBOX_SPAN = 20;
|
|
7
|
+
const MIN_REQUEST_ZOOM = Math.ceil(Math.log2(360 / MAX_BBOX_SPAN));
|
|
8
|
+
const MAX_PIXEL_RATIO = 4;
|
|
7
9
|
const LINEAR_ZOOM = 8;
|
|
8
10
|
const STRIP_HEIGHT = 32;
|
|
11
|
+
const clampLat = (lat) => Math.max(-MAX_LAT, Math.min(MAX_LAT, lat));
|
|
12
|
+
const lngAt = (x, z) => x / 2 ** z * 360 - 180;
|
|
13
|
+
const latAt = (y, z) => Math.atan(Math.sinh(Math.PI * (1 - 2 * y / 2 ** z))) * 180 / Math.PI;
|
|
14
|
+
const colAt = (lng, z) => (lng + 180) / 360 * 2 ** z;
|
|
15
|
+
const rowAt = (lat, z) => (1 - Math.asinh(Math.tan(clampLat(lat) * Math.PI / 180)) / Math.PI) / 2 * 2 ** z;
|
|
9
16
|
export function parseZztsUrl(url) {
|
|
10
|
-
const raw = url.
|
|
11
|
-
const
|
|
12
|
-
|
|
17
|
+
const raw = url.slice(url.indexOf(SCHEME_SEPARATOR) + SCHEME_SEPARATOR.length);
|
|
18
|
+
const hashIndex = raw.indexOf("#");
|
|
19
|
+
const mediaUrl = hashIndex < 0 ? raw : raw.slice(0, hashIndex);
|
|
20
|
+
const params = new URLSearchParams(hashIndex < 0 ? "" : raw.slice(hashIndex + 1));
|
|
21
|
+
const ratio = Number(params.get("pixelRatio"));
|
|
22
|
+
const [z, x, y] = (params.get("tile") ?? "").split("/").map(Number);
|
|
23
|
+
const clip = params.get("clip");
|
|
13
24
|
return {
|
|
14
|
-
mediaUrl
|
|
15
|
-
|
|
25
|
+
mediaUrl,
|
|
26
|
+
pixelRatio: Number.isFinite(ratio) ? Math.min(MAX_PIXEL_RATIO, Math.max(1, ratio)) : 1,
|
|
27
|
+
...clip ? { clip } : {},
|
|
28
|
+
...[z, x, y].every(Number.isInteger) ? { tile: { z, x, y } } : {}
|
|
16
29
|
};
|
|
17
30
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
export function zztsSourceUrl(scheme, mediaUrl, { pixelRatio, clip } = {}) {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
if (pixelRatio && pixelRatio !== 1) params.set("pixelRatio", String(pixelRatio));
|
|
34
|
+
if (clip) params.set("clip", clip);
|
|
35
|
+
const hash = params.toString();
|
|
36
|
+
return `${scheme}${SCHEME_SEPARATOR}${mediaUrl}${hash ? `#${hash}` : ""}`;
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
export function canvasSize(pixelRatio) {
|
|
39
|
+
return Math.round(ZZTS_TILE_SIZE * pixelRatio);
|
|
40
|
+
}
|
|
41
|
+
export function tileRange({ z, x, y }) {
|
|
42
|
+
return { z, x0: x, y0: y, x1: x + 1, y1: y + 1 };
|
|
43
|
+
}
|
|
44
|
+
export function rangeBBox({ z, x0, y0, x1, y1 }) {
|
|
45
|
+
return [lngAt(x0, z), latAt(y1, z), lngAt(x1, z), latAt(y0, z)];
|
|
46
|
+
}
|
|
47
|
+
export function tileBBox(tile) {
|
|
48
|
+
return rangeBBox(tileRange(tile));
|
|
49
|
+
}
|
|
50
|
+
export function extentBBox({ xmin, ymin, xmax, ymax }) {
|
|
51
|
+
return [xmin, ymin, xmax, ymax];
|
|
52
|
+
}
|
|
53
|
+
export function intersectBBox(a, b) {
|
|
54
|
+
const west = Math.max(a[0], b[0]);
|
|
55
|
+
const south = Math.max(a[1], b[1]);
|
|
56
|
+
const east = Math.min(a[2], b[2]);
|
|
57
|
+
const north = Math.min(a[3], b[3]);
|
|
58
|
+
return west < east && south < north ? [west, south, east, north] : void 0;
|
|
59
|
+
}
|
|
60
|
+
export function metatileRange(tile, size) {
|
|
61
|
+
const span = 360 / 2 ** tile.z;
|
|
62
|
+
const limit = Math.min(size, MAX_BBOX_SPAN / span, 2 ** tile.z);
|
|
63
|
+
const k = limit >= 1 ? 2 ** Math.floor(Math.log2(limit)) : 1;
|
|
64
|
+
const x0 = Math.floor(tile.x / k) * k;
|
|
65
|
+
const y0 = Math.floor(tile.y / k) * k;
|
|
66
|
+
return { z: tile.z, x0, y0, x1: x0 + k, y1: y0 + k };
|
|
28
67
|
}
|
|
29
68
|
export function tileScale(z, lat) {
|
|
30
69
|
const metersPerPixel = EARTH_CIRCUMFERENCE * Math.cos(lat * Math.PI / 180) / (ZZTS_TILE_SIZE * 2 ** z);
|
|
31
70
|
return metersPerPixel * METERS_PER_PIXEL_TO_SCALE;
|
|
32
71
|
}
|
|
33
|
-
export function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
72
|
+
export function planQueries(range, extent, pixelRatio, coarser = 0) {
|
|
73
|
+
const clipped = intersectBBox(rangeBBox(range), extent);
|
|
74
|
+
if (!clipped) return [];
|
|
75
|
+
const [west, south, east, north] = clipped;
|
|
76
|
+
const z = Math.max(range.z, MIN_REQUEST_ZOOM);
|
|
77
|
+
const density = ZZTS_TILE_SIZE * pixelRatio;
|
|
78
|
+
const scale = tileScale(z, (south + north) / 2) / pixelRatio * 2 ** coarser;
|
|
79
|
+
const height = Math.max(1, Math.round((rowAt(south, z) - rowAt(north, z)) * density));
|
|
80
|
+
const parts = Math.ceil((east - west) / MAX_BBOX_SPAN);
|
|
81
|
+
const step = (east - west) / parts;
|
|
82
|
+
return Array.from({ length: parts }, (_, i) => {
|
|
83
|
+
const left = west + step * i;
|
|
84
|
+
const right = i === parts - 1 ? east : left + step;
|
|
85
|
+
const width = Math.max(1, Math.round((colAt(right, z) - colAt(left, z)) * density));
|
|
86
|
+
return { bbox: [left, south, right, north], width, height, scale };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function zztsTileJson(url, media, clip) {
|
|
90
|
+
const { pixelRatio } = parseZztsUrl(url);
|
|
91
|
+
const extent = extentBBox(media.extent);
|
|
92
|
+
const centerLat = (extent[1] + extent[3]) / 2;
|
|
93
|
+
const nativeZoom = Math.ceil(Math.log2(EARTH_CIRCUMFERENCE * Math.cos(centerLat * Math.PI / 180) / (canvasSize(pixelRatio) * media.resolution)));
|
|
94
|
+
const [base, hash] = url.split("#");
|
|
38
95
|
return {
|
|
39
|
-
tiles: [`${
|
|
40
|
-
bounds:
|
|
41
|
-
minzoom,
|
|
42
|
-
maxzoom: Math.max(
|
|
96
|
+
tiles: [`${base}#${hash ? `${hash}&` : ""}tile={z}/{x}/{y}`],
|
|
97
|
+
bounds: clip && intersectBBox(extent, clip) || extent,
|
|
98
|
+
minzoom: 0,
|
|
99
|
+
maxzoom: Math.max(0, nativeZoom),
|
|
43
100
|
tileSize: ZZTS_TILE_SIZE,
|
|
44
101
|
name: media.name
|
|
45
102
|
};
|
|
46
103
|
}
|
|
47
|
-
export function elementsUrl(mediaUrl,
|
|
48
|
-
const [west, south, east, north] = tileBBox(tile);
|
|
104
|
+
export function elementsUrl(mediaUrl, { bbox, width, height, scale }) {
|
|
49
105
|
const url = new URL(mediaUrl);
|
|
50
106
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/elements`;
|
|
51
|
-
url.searchParams.set("width", String(
|
|
52
|
-
url.searchParams.set("height", String(
|
|
53
|
-
url.searchParams.set("scale", String(
|
|
54
|
-
url.searchParams.set("bbox",
|
|
107
|
+
url.searchParams.set("width", String(width));
|
|
108
|
+
url.searchParams.set("height", String(height));
|
|
109
|
+
url.searchParams.set("scale", String(scale));
|
|
110
|
+
url.searchParams.set("bbox", bbox.join(","));
|
|
55
111
|
return url.toString();
|
|
56
112
|
}
|
|
57
|
-
|
|
113
|
+
function snapOut(x, y, w, h) {
|
|
114
|
+
const left = Math.floor(x);
|
|
115
|
+
const top = Math.floor(y);
|
|
116
|
+
return [left, top, Math.ceil(x + w) - left, Math.ceil(y + h) - top];
|
|
117
|
+
}
|
|
118
|
+
export function planDraws(extent, tile, size = ZZTS_TILE_SIZE) {
|
|
58
119
|
const [west, south, east, north] = tileBBox(tile);
|
|
59
120
|
const left = Math.max(extent.xmin, west);
|
|
60
121
|
const right = Math.min(extent.xmax, east);
|
|
@@ -66,13 +127,13 @@ export function planDraws(extent, tile) {
|
|
|
66
127
|
const height = extent.ymax - extent.ymin;
|
|
67
128
|
const sx = (left - extent.xmin) / width;
|
|
68
129
|
const sw = (right - left) / width;
|
|
69
|
-
const dx = ((left
|
|
70
|
-
const dw = (right - left) / 360 * 2 ** z *
|
|
71
|
-
const dyTop = (rowAt(top, z) - y) *
|
|
72
|
-
const dyBottom = (rowAt(bottom, z) - y) *
|
|
130
|
+
const dx = (colAt(left, z) - x) * size;
|
|
131
|
+
const dw = (right - left) / 360 * 2 ** z * size;
|
|
132
|
+
const dyTop = (rowAt(top, z) - y) * size;
|
|
133
|
+
const dyBottom = (rowAt(bottom, z) - y) * size;
|
|
73
134
|
const strips = z >= LINEAR_ZOOM ? 1 : Math.max(1, Math.ceil((dyBottom - dyTop) / STRIP_HEIGHT));
|
|
74
135
|
const stripHeight = (dyBottom - dyTop) / strips;
|
|
75
|
-
const latAtPixel = (py) => latAt(y + py /
|
|
136
|
+
const latAtPixel = (py) => latAt(y + py / size, z);
|
|
76
137
|
return Array.from({ length: strips }, (_, i) => {
|
|
77
138
|
const dy = dyTop + i * stripHeight;
|
|
78
139
|
const latTop = i === 0 ? top : latAtPixel(dy);
|
|
@@ -83,3 +144,6 @@ export function planDraws(extent, tile) {
|
|
|
83
144
|
};
|
|
84
145
|
});
|
|
85
146
|
}
|
|
147
|
+
export function projectRings(rings, { z, x, y }, size) {
|
|
148
|
+
return rings.map((ring) => ring.map(([lng = 0, lat = 0]) => [(colAt(lng, z) - x) * size, (rowAt(lat, z) - y) * size]));
|
|
149
|
+
}
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
export interface ZztsProtocolOptions {
|
|
2
|
+
/**
|
|
3
|
+
* 协议名;为不同服务分别注册(如各自的鉴权)时取不同名称
|
|
4
|
+
* @defaultValue 'zzts'
|
|
5
|
+
*/
|
|
6
|
+
scheme?: string;
|
|
2
7
|
/** 自定义请求,元数据、元素列表与元素图片共用;用于附加鉴权头或改走代理 */
|
|
3
8
|
fetch?: typeof globalThis.fetch;
|
|
4
9
|
/**
|
|
5
|
-
*
|
|
10
|
+
* 元素图片缓存数量(压缩数据);相邻瓦片常共用同一网格图片,命中时不再重复下载
|
|
6
11
|
* @defaultValue 256
|
|
7
12
|
*/
|
|
8
13
|
cacheSize?: number;
|
|
14
|
+
/**
|
|
15
|
+
* 网络错误或 5xx 时的重试次数,按 500ms 起指数退避;404 视为未生成,不重试
|
|
16
|
+
* @defaultValue 2
|
|
17
|
+
*/
|
|
18
|
+
retry?: number;
|
|
19
|
+
/**
|
|
20
|
+
* 合并请求的边长(瓦片数,取 2 的幂):相邻 n×n 个瓦片共用一次元素列表请求;设为 1 关闭合并
|
|
21
|
+
* @defaultValue 2
|
|
22
|
+
*/
|
|
23
|
+
metatileSize?: number;
|
|
9
24
|
}
|
|
10
25
|
/**
|
|
11
|
-
* 注册
|
|
12
|
-
* `MaplibreRasterLayer` 的 `url` 写成 `zzts://` + media 接口地址即可。
|
|
26
|
+
* 注册 ZZTS 协议(按协议名全局单例、幂等):把 ZZTS 二维动态切片服务适配为 512 墨卡托栅格瓦片。
|
|
27
|
+
* `MaplibreZztsLayer` 或 `MaplibreRasterLayer` 的 `url` 写成 `zzts://` + media 接口地址即可。
|
|
13
28
|
*/
|
|
14
29
|
export declare function registerZztsProtocol(options?: ZztsProtocolOptions): void;
|
|
@@ -1,72 +1,135 @@
|
|
|
1
1
|
import { addProtocol } from "maplibre-gl";
|
|
2
2
|
import { defineGlobalSingleton } from "@movk/core";
|
|
3
|
-
import {
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { createImageStore, createLru, fetchWithRetry } from "./zzts-cache.js";
|
|
5
|
+
import { getZztsClip } from "./zzts-clip.js";
|
|
6
|
+
import {
|
|
7
|
+
canvasSize,
|
|
8
|
+
elementsUrl,
|
|
9
|
+
extentBBox,
|
|
10
|
+
intersectBBox,
|
|
11
|
+
metatileRange,
|
|
12
|
+
parseZztsUrl,
|
|
13
|
+
planDraws,
|
|
14
|
+
planQueries,
|
|
15
|
+
projectRings,
|
|
16
|
+
tileBBox,
|
|
17
|
+
tileRange,
|
|
18
|
+
zztsTileJson
|
|
19
|
+
} from "./zzts-tile.js";
|
|
4
20
|
const EMPTY_TILE = new ArrayBuffer(0);
|
|
5
21
|
const MAX_FALLBACK_LEVELS = 3;
|
|
6
|
-
|
|
7
|
-
const res = await request(url, { signal });
|
|
8
|
-
if (!res.ok) throw new Error(`ZZTS \u8BF7\u6C42\u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
9
|
-
return await res.json();
|
|
10
|
-
}
|
|
22
|
+
const ELEMENT_LIST_CACHE_SIZE = 512;
|
|
11
23
|
function imageElements(payload) {
|
|
12
24
|
if (!Array.isArray(payload.elements)) throw new Error(payload.msg || "ZZTS \u5143\u7D20\u8BF7\u6C42\u5931\u8D25");
|
|
13
|
-
return payload.elements.filter(
|
|
14
|
-
(element, i, list) => element.type === "image" && list.findIndex((item) => item.id === element.id) === i
|
|
15
|
-
);
|
|
25
|
+
return dedupe(payload.elements.filter((element) => element.type === "image"));
|
|
16
26
|
}
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
return (
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const pending = request(url).then(async (res) => {
|
|
27
|
-
if (res.status === 404) return void 0;
|
|
28
|
-
if (!res.ok) throw new Error(`ZZTS \u56FE\u7247\u52A0\u8F7D\u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
29
|
-
return createImageBitmap(await res.blob());
|
|
30
|
-
});
|
|
31
|
-
pending.catch(() => cache.delete(url));
|
|
32
|
-
cache.set(url, pending);
|
|
33
|
-
if (cache.size > size) cache.delete(cache.keys().next().value);
|
|
34
|
-
return pending;
|
|
35
|
-
};
|
|
27
|
+
function dedupe(elements) {
|
|
28
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29
|
+
return elements.filter(({ id, extent: { xmin, ymin, xmax, ymax } }) => {
|
|
30
|
+
const keys = [`id:${id}`, `extent:${[xmin, ymin, xmax, ymax].map((value) => value.toFixed(8))}`];
|
|
31
|
+
if (keys.some((key) => seen.has(key))) return false;
|
|
32
|
+
keys.forEach((key) => seen.add(key));
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
36
35
|
}
|
|
37
|
-
function compose(tile, items) {
|
|
38
|
-
const canvas = new OffscreenCanvas(
|
|
36
|
+
function compose(tile, items, size, clip) {
|
|
37
|
+
const canvas = new OffscreenCanvas(size, size);
|
|
39
38
|
const context = canvas.getContext("2d");
|
|
40
39
|
for (const { element, image } of items) {
|
|
41
|
-
for (const { source: [sx, sy, sw, sh], target: [dx, dy, dw, dh] } of planDraws(element.extent, tile)) {
|
|
40
|
+
for (const { source: [sx, sy, sw, sh], target: [dx, dy, dw, dh] } of planDraws(element.extent, tile, size)) {
|
|
42
41
|
context.drawImage(image, sx * image.width, sy * image.height, sw * image.width, sh * image.height, dx, dy, dw, dh);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
44
|
+
if (clip) {
|
|
45
|
+
const path = new Path2D();
|
|
46
|
+
for (const [first, ...rest] of projectRings(clip.rings, tile, size)) {
|
|
47
|
+
if (!first) continue;
|
|
48
|
+
path.moveTo(...first);
|
|
49
|
+
rest.forEach((point) => path.lineTo(...point));
|
|
50
|
+
path.closePath();
|
|
51
|
+
}
|
|
52
|
+
context.globalCompositeOperation = "destination-in";
|
|
53
|
+
context.fill(path, "evenodd");
|
|
54
|
+
}
|
|
45
55
|
return canvas.transferToImageBitmap();
|
|
46
56
|
}
|
|
47
|
-
function createZztsProtocol({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
function createZztsProtocol({
|
|
58
|
+
fetch: request = globalThis.fetch.bind(globalThis),
|
|
59
|
+
cacheSize = 256,
|
|
60
|
+
retry = 2,
|
|
61
|
+
metatileSize = 2
|
|
62
|
+
}) {
|
|
63
|
+
const images = createImageStore(request, { size: cacheSize, retry });
|
|
64
|
+
const medias = /* @__PURE__ */ new Map();
|
|
65
|
+
const lists = createLru(ELEMENT_LIST_CACHE_SIZE);
|
|
66
|
+
async function getJson(url) {
|
|
67
|
+
const res = await fetchWithRetry(request, url, retry);
|
|
68
|
+
if (!res.ok) throw new Error(`ZZTS \u8BF7\u6C42\u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
69
|
+
return await res.json();
|
|
70
|
+
}
|
|
71
|
+
function shared(cache, key, load) {
|
|
72
|
+
const hit = cache.get(key);
|
|
73
|
+
if (hit) return hit;
|
|
74
|
+
const pending = load();
|
|
75
|
+
pending.catch(() => cache.delete(key));
|
|
76
|
+
cache.set(key, pending);
|
|
77
|
+
return pending;
|
|
78
|
+
}
|
|
79
|
+
const getMedia = (mediaUrl) => shared(medias, mediaUrl, () => getJson(mediaUrl));
|
|
80
|
+
function rangeElements(mediaUrl, media, range, pixelRatio, coarser) {
|
|
81
|
+
const key = [mediaUrl, pixelRatio, coarser, range.z, range.x0, range.y0, range.x1, range.y1].join("|");
|
|
82
|
+
return shared(lists, key, async () => {
|
|
83
|
+
const queries = planQueries(range, extentBBox(media.extent), pixelRatio, coarser);
|
|
84
|
+
const payloads = await Promise.all(queries.map((query) => getJson(elementsUrl(mediaUrl, query))));
|
|
85
|
+
return dedupe(payloads.flatMap(imageElements));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function tileElements(mediaUrl, media, tile, pixelRatio, coarser) {
|
|
89
|
+
const group = metatileRange(tile, metatileSize);
|
|
90
|
+
const single = tileRange(tile);
|
|
91
|
+
const merged = group.x1 - group.x0 > 1;
|
|
92
|
+
let elements = await rangeElements(mediaUrl, media, group, pixelRatio, coarser);
|
|
93
|
+
if (merged && !elements.length) elements = await rangeElements(mediaUrl, media, single, pixelRatio, coarser);
|
|
94
|
+
const bbox = tileBBox(tile);
|
|
95
|
+
return elements.filter((element) => intersectBBox(extentBBox(element.extent), bbox));
|
|
96
|
+
}
|
|
97
|
+
async function loadLevel(mediaUrl, media, tile, pixelRatio, coarser, acquired) {
|
|
98
|
+
const elements = await tileElements(mediaUrl, media, tile, pixelRatio, coarser);
|
|
99
|
+
const loaded = await Promise.all(elements.map((element) => {
|
|
100
|
+
acquired.push(element.url);
|
|
101
|
+
return images.acquire(element.url);
|
|
102
|
+
}));
|
|
103
|
+
const layers = elements.flatMap((element, i) => loaded[i] ? [{ element, image: loaded[i] }] : []);
|
|
53
104
|
if (layers.length === elements.length || coarser >= MAX_FALLBACK_LEVELS) return layers;
|
|
54
|
-
return [...await loadLevel(mediaUrl, tile, coarser + 1,
|
|
105
|
+
return [...await loadLevel(mediaUrl, media, tile, pixelRatio, coarser + 1, acquired), ...layers];
|
|
55
106
|
}
|
|
56
107
|
return async ({ url }, { signal }) => {
|
|
57
|
-
const { mediaUrl, tile } = parseZztsUrl(url);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { data: zztsTileJson(url, media) };
|
|
61
|
-
}
|
|
62
|
-
const layers = await loadLevel(mediaUrl, tile, 0, signal);
|
|
108
|
+
const { mediaUrl, tile, pixelRatio, clip: clipKey } = parseZztsUrl(url);
|
|
109
|
+
const clip = clipKey ? getZztsClip(clipKey) : void 0;
|
|
110
|
+
const media = await getMedia(mediaUrl);
|
|
63
111
|
signal.throwIfAborted();
|
|
64
|
-
return { data:
|
|
112
|
+
if (!tile) return { data: zztsTileJson(url, media, clip?.bbox) };
|
|
113
|
+
if (clipKey && (!clip || !intersectBBox(tileBBox(tile), clip.bbox))) return { data: EMPTY_TILE };
|
|
114
|
+
const acquired = [];
|
|
115
|
+
try {
|
|
116
|
+
const layers = await loadLevel(mediaUrl, media, tile, pixelRatio, 0, acquired);
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
return { data: layers.length ? compose(tile, layers, canvasSize(pixelRatio), clip) : EMPTY_TILE };
|
|
119
|
+
} finally {
|
|
120
|
+
acquired.forEach(images.release);
|
|
121
|
+
}
|
|
65
122
|
};
|
|
66
123
|
}
|
|
67
|
-
export function registerZztsProtocol(options) {
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
export function registerZztsProtocol(options = {}) {
|
|
125
|
+
const scheme = options.scheme ?? "zzts";
|
|
126
|
+
let created = false;
|
|
127
|
+
defineGlobalSingleton(`movk-maplibre:protocol:${scheme}`, () => {
|
|
128
|
+
created = true;
|
|
129
|
+
addProtocol(scheme, createZztsProtocol(options));
|
|
70
130
|
return true;
|
|
71
131
|
});
|
|
132
|
+
if (!created && Object.keys(options).some((key) => key !== "scheme")) {
|
|
133
|
+
logger.warn(`ZZTS protocol "${scheme}" is already registered; the new options are ignored. Register a different scheme for another configuration.`);
|
|
134
|
+
}
|
|
72
135
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@movk/maplibre",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"packageManager": "pnpm@12.10.1",
|
|
6
6
|
"description": "声明式 MapLibre GL 封装库:MaplibreMap/Source/Layer 等组件与 composables,原生支持 Nuxt 4 模块,并通过 Vite 插件在纯 Vue + Vite 项目中可用。无需 access token,内置 3D 建筑、雷达/扩散/辉光等动态效果,sky/terrain 环境,绘制(terra-draw)、天地图、WMS/WMTS 与多坐标系本土化支持。",
|
|
7
7
|
"author": "YiXuan <mhaibaraai@gmail.com>",
|