├── data ├── dbRoot.v5 ├── error.png ├── quadtreeset.cn.proto └── quadtreeset.proto ├── docs ├── compare_1.jpg ├── compare_2.jpg └── screenshot.jpg ├── test ├── test_data │ └── f1-021-i.1016 ├── decode_test.ts ├── quad_test.ts ├── history_test.ts └── key.ts ├── deno.json ├── src ├── version.ts ├── wmts.ts ├── cache.ts ├── ge.ts ├── decode.ts ├── info.ts ├── qtree.ts ├── server.ts ├── quad.ts └── history.ts ├── LICENSE ├── README.md ├── .gitignore └── deno.lock /data/dbRoot.v5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/data/dbRoot.v5 -------------------------------------------------------------------------------- /data/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/data/error.png -------------------------------------------------------------------------------- /docs/compare_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/docs/compare_1.jpg -------------------------------------------------------------------------------- /docs/compare_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/docs/compare_2.jpg -------------------------------------------------------------------------------- /docs/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/docs/screenshot.jpg -------------------------------------------------------------------------------- /test/test_data/f1-021-i.1016: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/GE_WMTS/main/test/test_data/f1-021-i.1016 -------------------------------------------------------------------------------- /test/decode_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { decode_data } from "../src/decode.ts"; 3 | import { key } from "./key.ts"; 4 | 5 | Deno.test(async function test_decode() { 6 | const test_data = await Deno.readFile("./test/test_data/f1-021-i.1016"); 7 | const decoded_data = decode_data(test_data, key); 8 | const jpeg_magic = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); 9 | assertEquals(decoded_data.slice(0, 4), jpeg_magic); 10 | }); 11 | -------------------------------------------------------------------------------- /test/quad_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { QuadKey } from "../src/quad.ts"; 3 | 4 | Deno.test(function test_QuadKey() { 5 | assertEquals(new QuadKey(3, 0, 2).quad_key, "021"); 6 | assertEquals(new QuadKey(0, 0, 0).quad_key, "0"); 7 | assertEquals(new QuadKey("0").parent_quad_key, "0"); 8 | assertEquals(new QuadKey("02").parent_quad_key, "0"); 9 | assertEquals(new QuadKey("0210").parent_quad_key, "0"); 10 | assertEquals(new QuadKey("02101").parent_quad_key, "0210"); 11 | assertEquals(new QuadKey("02103103").parent_quad_key, "0210"); 12 | }); 13 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "dev": "deno run -A --unstable-kv --unstable-cron --watch ./src/server.ts", 4 | "test": "deno test -A --unstable-kv", 5 | "build": "deno compile -A --unstable-kv --include ./data -o ./dist/gewmts.exe ./src/server.ts" 6 | }, 7 | "imports": { 8 | "@liuxspro/capgen": "jsr:@liuxspro/capgen@^0.2.1", 9 | "@std/assert": "jsr:@std/assert@1", 10 | "protobufjs": "npm:protobufjs@^7.4.0" 11 | }, 12 | "deploy": { 13 | "project": "418f4fc8-3ebb-4921-9e72-d70591d973cc", 14 | "exclude": ["**/node_modules", "**/.git", "**/playground", "**/docs"], 15 | "include": [], 16 | "entrypoint": "src/server.ts" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | const kv = await Deno.openKv(); 2 | 3 | export async function get_dbroot() { 4 | const url = "http://kh.google.com/dbRoot.v5?hl=zh-hans&gl=hk"; 5 | const data = await (await fetch(url)).bytes(); 6 | return data; 7 | } 8 | 9 | export async function get_version_and_key() { 10 | const dbroot_data = await get_dbroot(); 11 | const version_byte = dbroot_data.slice(6, 8); 12 | // 组合为 16 位整数 小端序 13 | const uint16Value = (version_byte[1] << 8) | version_byte[0]; 14 | const version = uint16Value ^ 0x4200; 15 | const key = new Uint8Array(1024); 16 | // 将前 8 个字节填充为 0 17 | key.fill(0, 0, 8); 18 | key.set(dbroot_data.slice(8, 1024), 8); 19 | return { version, key }; 20 | } 21 | 22 | export async function _get_earth_version() { 23 | const entry = await kv.get(["Version", "Earth"]); 24 | if (entry.value) { 25 | return entry.value; 26 | } else { 27 | const { version } = await get_version_and_key(); 28 | await kv.set(["Version", "Earth"], version); 29 | return version; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/wmts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Capabilities, 3 | GeoPoint, 4 | MapLayer, 5 | mercator_bbox, 6 | Service, 7 | world_crs84_quad, 8 | } from "@liuxspro/capgen"; 9 | 10 | const service: Service = { 11 | title: "Google Earth", 12 | abstract: "Google Earth Tile As WMTS", 13 | keywords: ["Google Earth"], 14 | }; 15 | 16 | export function create_ge_cap(url: string) { 17 | const ge_layer = new MapLayer( 18 | "Google Earth", 19 | "Google Earth", 20 | "GoogleEarthLatest", 21 | mercator_bbox, 22 | world_crs84_quad.clone().setZoom(2, 20), 23 | url, 24 | "image/jpeg", 25 | ); 26 | return new Capabilities(service, [ge_layer]).xml; 27 | } 28 | 29 | export function create_ge_his_cap(bbox: [GeoPoint, GeoPoint], url: string) { 30 | const layer = new MapLayer( 31 | "Google Earth Historical", 32 | "Google Earth Historical", 33 | "GoogleEarthHistorical", 34 | bbox, 35 | world_crs84_quad.clone().setZoom(2, 20), 36 | url, 37 | "image/jpeg", 38 | ); 39 | return new Capabilities( 40 | service, 41 | [layer], 42 | ).xml; 43 | } 44 | -------------------------------------------------------------------------------- /test/history_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { number_to_date } from "../src/history.ts"; 3 | 4 | Deno.test(function test_number_to_date() { 5 | // 以瓦片 18/214697/40742 为例 6 | // 四叉树编码为 0210230011023132002 7 | // qtree 信息存储在 0210230011023132 中 8 | // 该瓦片的所有历史日期如下 9 | // 与 Google Earth 位置(114.84213007, 34.04870002)下的历史时间轴对比 10 | // 545 不能计算 11 | // assertEquals(number_to_date(545), "2021-09-22"); 12 | assertEquals(number_to_date(1030756), "2013-03-04"); 13 | assertEquals(number_to_date(1032027), "2015-10-27"); 14 | assertEquals(number_to_date(1032779), "2017-02-11"); 15 | assertEquals(number_to_date(1033259), "2018-01-11"); 16 | assertEquals(number_to_date(1033532), "2018-09-28"); 17 | assertEquals(number_to_date(1033830), "2019-03-06"); 18 | assertEquals(number_to_date(1034612), "2020-11-20"); 19 | assertEquals(number_to_date(1035062), "2021-09-22"); 20 | assertEquals(number_to_date(1036471), "2024-05-23"); 21 | assertEquals(number_to_date(1036697), "2024-12-25"); 22 | assertEquals(number_to_date(parseInt("fd199", 16)), "2024-12-25"); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 liuxspro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GE_WMTS 2 | 3 | View & Download Google Earth Historical Imagery in QGIS 4 | 5 | 谷歌地球影像 WMTS 服务 6 | 7 | Google Earth Imagery WMTS Service 8 | 9 | ## 进度 Progress 10 | 11 | - [x] 影像解密 Image decode 12 | - [x] 四叉树编码➡️ XYZ 行列号(经纬度投影) Quadkey ➡️ XYZ tile coordinates 13 | (geodetic coordinate system) 14 | - [x] WMTS 服务 WMTS service implementation 15 | - [x] 最新影像 qtree 解析 Latest imagery qtree parsing 16 | - [ ] 历史影像(初步完成)Historical imagery (partially implemented) 17 | - [ ] 缓存 Cache 18 | 19 | ## 调试运行 Run 20 | 21 | ```bash 22 | deno task dev 23 | ``` 24 | 25 | QGIS 加载效果 26 | 27 | ![](docs/screenshot.jpg) 28 | 29 | 效果对比 30 | 31 | | Google Map Satellite | Google Earth | 32 | | :---------------------: | :---------------------: | 33 | | ![](docs/compare_1.jpg) | ![](docs/compare_2.jpg) | 34 | 35 | ## 部署到 Deno Deploy 36 | 37 | ```bash 38 | deployctl deploy --entrypoint .\src\server.ts 39 | ``` 40 | 41 | ## 参考资料 Reference 42 | 43 | 1. 瓷砖名称的形成原理 --- Принцип формирования имён тайлов[EB/OL]. 44 | https://greverse.bitbucket.io/genames.htm. 45 | 2. 有关 dbRoot.v5 文件的详细信息 --- Подробно о файле dbRoot.v5[EB/OL]. 46 | https://greverse.bitbucket.io/dbroot.htm. 47 | 3. 谷歌地球瓦片下载分析(未完) - 暗鸦 - 博客园[EB/OL]. 48 | https://www.cnblogs.com/utopin/p/14691863.html. 49 | 4. Google Earth影像数据破解之旅 - fu*k - 博客园[EB/OL]. 50 | https://www.cnblogs.com/fuckgiser/p/6424207.html. 51 | 5. qtree文件结构解析(二) - janehlp - 博客园[EB/OL]. 52 | https://www.cnblogs.com/janehlp/p/13695688.html. 53 | 6. [Cesium](https://github.com/CesiumGS/cesium) 54 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "jsr:@std/path"; 2 | 3 | async function _create_cache_dir() { 4 | try { 5 | await Deno.mkdir("Cache", { recursive: true }); 6 | } catch (err) { 7 | if (err instanceof Deno.errors.AlreadyExists) { 8 | console.log("Cache 文件夹已存在"); 9 | } else { 10 | console.error("发生错误:", err); 11 | } 12 | } 13 | } 14 | 15 | async function _create_qtree_dir(version: number) { 16 | try { 17 | await Deno.mkdir(`Cache/Qtrees/Earth/${version}`, { recursive: true }); 18 | } catch (err) { 19 | if (err instanceof Deno.errors.AlreadyExists) { 20 | console.log(`Cache/Qtrees/${version} 文件夹已存在`); 21 | } else { 22 | console.error("发生错误:", err); 23 | } 24 | } 25 | } 26 | 27 | async function _create_his_qtree_dir(version: number) { 28 | try { 29 | await Deno.mkdir(`Cache/Qtrees/History/${version}`, { recursive: true }); 30 | } catch (err) { 31 | if (err instanceof Deno.errors.AlreadyExists) { 32 | console.log(`Cache/Qtrees/History/${version} 文件夹已存在`); 33 | } else { 34 | console.error("发生错误:", err); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * 获取当前执行脚本所在的目录 41 | * @returns {string} 路径 42 | */ 43 | export function get_current_dir(): string { 44 | const current_dir = import.meta.dirname; 45 | if (current_dir) { 46 | return current_dir; 47 | } else { 48 | const dir = dirname(new URL(import.meta.url).pathname); 49 | if (Deno.build.os === "windows") { 50 | return dir.slice(1); 51 | } 52 | return dir; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ge.ts: -------------------------------------------------------------------------------- 1 | import { get_qtree, parse_qtree } from "./qtree.ts"; 2 | import { QuadKey } from "./quad.ts"; 3 | import { decode_tile } from "./decode.ts"; 4 | 5 | /** 6 | * 根据 version 获取瓦片 7 | * @param x X 8 | * @param y Y 9 | * @param z Z 10 | * @param version 当前 Version 值 11 | * @param key 密钥 12 | * @returns 解密后的瓦片数据(JPEG) 13 | */ 14 | export async function get_ge_tile( 15 | x: number, 16 | y: number, 17 | z: number, 18 | version: number, 19 | key: Uint8Array 20 | ) { 21 | const quad = new QuadKey(x, y, z); 22 | const tile_url = `https://kh.google.com/flatfile?f1-${quad.quad_key}-i.${version}`; 23 | const raw_tile_data = await (await fetch(tile_url)).bytes(); 24 | const decrypted_tile_data = decode_tile(raw_tile_data, key); 25 | return decrypted_tile_data; 26 | } 27 | 28 | /** 29 | * 获取瓦片 (自动查找 version) 30 | * @param x X 31 | * @param y Y 32 | * @param z Z 33 | * @param version 当前 qtree version 34 | * @param key 密钥 35 | * @returns 瓦片数据 36 | */ 37 | export async function get_tile( 38 | x: number, 39 | y: number, 40 | z: number, 41 | version: number, 42 | key: Uint8Array 43 | ) { 44 | const quad = new QuadKey(x, y, z); 45 | const qtree_name = quad.parent_quad_key; 46 | const qtree_data = await get_qtree(qtree_name, version, key); 47 | const tiles = parse_qtree(qtree_data, qtree_name); 48 | const tile_info = tiles[quad.quad_key]; 49 | if (tile_info != null) { 50 | const tile_version = tile_info.imagery_version; 51 | const tile_data = await get_ge_tile(x, y, z, tile_version, key); 52 | return tile_data; 53 | } else { 54 | console.error(`${z}/${x}/${y} 未找到 version 信息`); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/decode.ts: -------------------------------------------------------------------------------- 1 | import { inflate } from "jsr:@deno-library/compress"; 2 | import { array_is_equal } from "jsr:@liuxspro/utils"; 3 | 4 | /** 5 | * ## 使用密钥解密数据 6 | * 7 | * @param {Uint8Array} encrypted_data - 待解密的原始数据(Uint8Array类型) 8 | * @param {Uint8Array} key - 解密密钥(从DBroot文件中获取,长度为1024字节) 9 | * @returns {Uint8Array} 解密后的数据,若输入为空则返回空数组 10 | * 11 | * @example 12 | * // 基本用法 13 | * const encrypted = new Uint8Array([0x12, 0x34, 0x56,...]); 14 | * const key = new Uint8Array(1024); // 1024字节密钥 15 | * const decrypted = decode_data(encrypted, key); 16 | * 17 | * @note 18 | * 参考: https://greverse.bitbucket.io/gecrypt.htm 19 | * 优化建议参考: https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Core/decodeGoogleEarthEnterpriseData.js 20 | */ 21 | export function decode_data( 22 | encrypted_data: Uint8Array, 23 | key: Uint8Array 24 | ): Uint8Array { 25 | // 创建一个新的 Uint8Array 来存储解密后的数据 26 | const decrypted = new Uint8Array(encrypted_data.length); 27 | // 初始化密钥索引 28 | let key_index = 16; // 从密钥的第 16 个字节开始 29 | // 对每个字节进行异或解密 30 | for (let i = 0; i < encrypted_data.length; i++) { 31 | // 使用密钥的当前字节进行异或操作 32 | decrypted[i] = encrypted_data[i] ^ key[key_index + 8]; 33 | // 更新密钥索引 34 | key_index++; 35 | // 如果 keyIndex 是 8 的倍数,则 keyIndex 增加 16 36 | if (key_index % 8 === 0) { 37 | key_index += 16; 38 | } 39 | // 如果 keyIndex 超过了密钥的长度 keylen,则重新调整 keyIndex 的值。 40 | // keyIndex 更新为 (keyIndex + 8) % 24,这意味着 keyIndex 会在 0 到 23 之间循环。 41 | if (key_index >= 1016) { 42 | key_index = (key_index + 8) % 24; 43 | } 44 | } 45 | return decrypted; 46 | } 47 | 48 | /** 49 | * ## 解码 Qtree 数据 50 | * 先用密钥解密,解密后是 zlib 压缩后的数据 51 | * 再解压数据得到原始数据 52 | * @param encrypted_data 请求得到的原始 Qtree数据 53 | * @param key 密钥 54 | * @returns 解码后的数据包 55 | */ 56 | export function decode_qtree_data( 57 | encrypted_data: Uint8Array, 58 | key: Uint8Array 59 | ): Uint8Array { 60 | const zlib_data = decode_data(encrypted_data, key); 61 | const decompressed = inflate(zlib_data.slice(8)); 62 | return decompressed; 63 | } 64 | 65 | /** 66 | * 解密瓦片数据 67 | * @param tile_data 请求得到的原始瓦片数据 68 | * @param key 密钥 69 | * @returns 返回解密后的瓦片数据 70 | */ 71 | export function decode_tile( 72 | tile_data: Uint8Array, 73 | key: Uint8Array 74 | ): Uint8Array | null { 75 | const header = new Uint8Array([0x07, 0x91, 0xef, 0xa6]); 76 | // 判断一下文件头 77 | if (array_is_equal(tile_data.slice(0, 4), header)) { 78 | return decode_data(tile_data, key); 79 | } 80 | return null; 81 | } 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | Cache 133 | playground -------------------------------------------------------------------------------- /src/info.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `{ "date": 545, "datedTileEpoch": 271, "provider": 0 }` 3 | */ 4 | interface DatedTile { 5 | date: number; // Tile 日期 6 | datedTileEpoch: number; // 版本 7 | provider: number; // 提供商 8 | date_1: string; 9 | } 10 | 11 | export class DatedTile2 { 12 | date: number; 13 | epoch: number; 14 | provider: number; 15 | 16 | constructor(date: number, epoch: number, provider: number) { 17 | this.date = date; 18 | this.epoch = epoch; 19 | this.provider = provider; 20 | } 21 | } 22 | 23 | /** 24 | * 25 | * ``` 26 | * "datesLayer": { 27 | * "datedTile": [ 28 | * { "date": 545, "datedTileEpoch": 271, "provider": 0 }, 29 | * ... 30 | * ], 31 | * "sharedTileDate": 1036610, // 可选属性 32 | * "coarseTileDates": [1016735] // 可选属性 33 | *} 34 | * ``` 35 | */ 36 | export interface DatesLayer { 37 | datedTile: DatedTile2[]; 38 | sharedTileDate?: number; 39 | coarseTileDates?: number[]; 40 | } 41 | 42 | interface ImageryLayer { 43 | type: "LAYER_TYPE_IMAGERY"; 44 | layerEpoch: number; 45 | } 46 | 47 | interface HistoryLayer { 48 | type: "LAYER_TYPE_IMAGERY_HISTORY"; 49 | layerEpoch: number; 50 | datesLayer: DatesLayer; 51 | } 52 | 53 | export type Layer = ImageryLayer | HistoryLayer; 54 | 55 | export interface Node { 56 | flags: number; 57 | layer?: Layer[]; 58 | } 59 | 60 | export interface SparseQuadtreeNode { 61 | index: number; 62 | Node: Node; 63 | } 64 | 65 | // enum LayerType { 66 | // LAYER_TYPE_IMAGERY = 0, 67 | // LAYER_TYPE_TERRAIN = 1, 68 | // LAYER_TYPE_VECTOR = 2, 69 | // LAYER_TYPE_IMAGERY_HISTORY = 3, 70 | // } 71 | 72 | export type HistoryTilesInfo = { 73 | [key: string]: GEHistoryTileInfo | null; 74 | }; 75 | 76 | /** 77 | * 检查 bits 中是否有任何与 mask 对应的位被设置(即是否为 1)。 78 | * 如果 bits 和 mask 的按位与结果不为 0,说明至少有一个对应的位被设置。 79 | * 80 | * @param bit - 一个整数,表示要检查的二进制位。 81 | * @param mask - 一个掩码,用于指定要检查的位。 82 | * @returns 如果 bits 中与 mask 对应的位有任何一个被设置,返回 true;否则返回 false。 83 | * 84 | * @example 85 | * // bits: 0101 0000 mask: 0b01000000(0x40) 86 | * // 检查二进制数 第 7 位是是否为 1 87 | * console.log(isBitSet(0b01010000, 0b01000000)); // true 88 | * console.log(isBitSet(0x50, 0x40)); // true 89 | * console.log(isBitSet(80, 64)); // true 90 | * console.log(isBitSet(0b00010000, 0b01000000)); // false 91 | * 92 | */ 93 | export function isBitSet(bits: number, mask: number) { 94 | return (bits & mask) !== 0; 95 | } 96 | 97 | // See: https://github.com/google/earthenterprise/blob/master/earth_enterprise/src/keyhole/earth_client_protobuf/quadtreeset.protodevel 98 | // See: https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Workers/decodeGoogleEarthEnterprisePacket.js 99 | // Bitmask for checking tile properties 100 | const childrenBitmasks = [0x01, 0x02, 0x04, 0x08]; 101 | const anyChildBitmask = 0x0f; 102 | const cacheFlagBitmask = 0x10; // True if there is a child subtree 103 | const imageBitmask = 0x40; 104 | const terrainBitmask = 0x80; 105 | 106 | export class TileInfo { 107 | bitfield: number; 108 | 109 | constructor(bitfield: number) { 110 | this.bitfield = bitfield; 111 | } 112 | 113 | // 是否含有子节点 114 | has_subtree(): boolean { 115 | return isBitSet(this.bitfield, cacheFlagBitmask); 116 | } 117 | // 是否含有图像 118 | has_imagery(): boolean { 119 | return isBitSet(this.bitfield, imageBitmask); 120 | } 121 | // 是否含有地形数据 122 | has_terrain(): boolean { 123 | return isBitSet(this.bitfield, terrainBitmask); 124 | } 125 | // 是否有任意子节点 126 | has_children(): boolean { 127 | return isBitSet(this.bitfield, anyChildBitmask); 128 | } 129 | // 是否有指定的子节点 130 | has_child(index: number) { 131 | return isBitSet(this.bitfield, childrenBitmasks[index]); 132 | } 133 | } 134 | 135 | /** 136 | * Qtree 数据中的 Tile 节点数据 137 | * 138 | * 每个节点占用 32 字节 139 | * 第 1 个字节为 Bitfield 用于识别节点图像类型 140 | * 采用的是位掩码设计 141 | */ 142 | export class GETileInfo extends TileInfo { 143 | cnode_version: number; 144 | imagery_version: number; 145 | terrain_version: number; 146 | 147 | constructor( 148 | bitfield: number, 149 | cnode_version: number, 150 | imagery_version: number, 151 | terrain_version: number 152 | ) { 153 | super(bitfield); 154 | this.cnode_version = cnode_version; 155 | this.imagery_version = imagery_version; 156 | this.terrain_version = terrain_version; 157 | } 158 | } 159 | 160 | export class GEHistoryTileInfo extends TileInfo { 161 | layer: Layer[]; 162 | 163 | constructor(bitfield: number, layer: Layer[]) { 164 | super(bitfield); 165 | this.layer = layer; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/qtree.ts: -------------------------------------------------------------------------------- 1 | import { array_is_equal } from "jsr:@liuxspro/utils"; 2 | import { decode_qtree_data } from "./decode.ts"; 3 | import { GETileInfo } from "./info.ts"; 4 | 5 | const kv = await Deno.openKv(); 6 | 7 | /** 8 | * 请求原始的 qtree 数据 9 | * @param {string} quad_key 完整的四叉树编码 10 | * @param {number} version 当前版本 11 | * @returns {Promise} 原始 qtree packet 数据 12 | */ 13 | export async function fetch_qtree_rawdata( 14 | quad_key: string, 15 | version: number 16 | ): Promise { 17 | const qtree_url = `https://kh.google.com/flatfile?q2-${quad_key}-q.${version}`; 18 | const data = await (await fetch(qtree_url)).bytes(); 19 | return data; 20 | } 21 | 22 | /** 23 | * 获取 qtree 信息(有缓存) 24 | * @param quad_key quad key 25 | * @param version qtree version 26 | * @param key 密钥 27 | * @returns 28 | */ 29 | export async function get_qtree( 30 | quad_key: string, 31 | version: number, 32 | key: Uint8Array 33 | ) { 34 | const entry = await kv.get(["Earth", version, quad_key]); 35 | // 如果 KV 中有,直接解密一下返回 36 | // 没有就抓取并缓存到 KV 中 37 | if (entry.value) { 38 | return decode_qtree_data(entry.value as Uint8Array, key); 39 | } else { 40 | const qtree_rawdata = await fetch_qtree_rawdata(quad_key, version); 41 | await kv.set(["Earth", version, quad_key], qtree_rawdata); 42 | return decode_qtree_data(qtree_rawdata, key); 43 | } 44 | } 45 | 46 | function to_number(a: Uint8Array): number { 47 | // 使用 DataView 解析为小端序的 32 位无符号整数 48 | // 补零到 4 位 49 | const paddedArray = new Uint8Array(4); // 创建一个长度为 4 的 Uint8Array 50 | paddedArray.set(a, 0); // 将原始数据复制到新数组的开头 51 | const dataView = new DataView(paddedArray.buffer); 52 | const decimalNumber = dataView.getUint32(0, true); // true 表示小端序 53 | 54 | return decimalNumber; 55 | } 56 | 57 | export function parse_qtree_node(node_data: Uint8Array): GETileInfo { 58 | // 节点数据长度为 32 59 | if (node_data.length != 32) { 60 | throw "invalid node data"; 61 | } 62 | const bitfield = to_number(node_data.slice(0, 1)); 63 | const cnode_version = to_number(node_data.slice(2, 4)); 64 | const imagery_version = to_number(node_data.slice(4, 6)); 65 | const terrain_version = to_number(node_data.slice(6, 8)); 66 | return new GETileInfo( 67 | bitfield, 68 | cnode_version, 69 | imagery_version, 70 | terrain_version 71 | ); 72 | } 73 | 74 | /** 75 | * 解析 Qtree 数据包 76 | * @param qtree_data 77 | * @returns {GETileInfo[]} GETileInfo 数组 78 | */ 79 | export function get_nodes_from_qtree(qtree_data: Uint8Array): GETileInfo[] { 80 | const magic = new Uint8Array([0x2d, 0x7e, 0x00, 0x00]); 81 | 82 | if (!array_is_equal(qtree_data.slice(0, 4), magic)) { 83 | throw "not a qtree data"; 84 | } 85 | const num_instances = to_number(qtree_data.slice(12, 16)); 86 | const nodes = []; 87 | for (let i = 1; i <= num_instances; i++) { 88 | nodes.push(parse_qtree_node(qtree_data.slice(32 * i, 32 * (i + 1)))); 89 | } 90 | return nodes; 91 | } 92 | 93 | type TilesInfo = { 94 | [key: string]: GETileInfo | null; 95 | }; 96 | 97 | export function parse_qtree( 98 | qtree_data: Uint8Array, 99 | quad_key: string 100 | ): TilesInfo { 101 | const nodes = get_nodes_from_qtree(qtree_data); 102 | const tiles_info: TilesInfo = {}; 103 | let index = 0; 104 | 105 | // 递归函数:填充 tiles_info 106 | function populate_tiles( 107 | parentKey: string, 108 | parent: GETileInfo, 109 | level: number 110 | ) { 111 | // 如果是叶子节点(level === 4),设置所有子节点为 null 112 | const isLeaf = level === 4 && !parent.has_subtree(); 113 | 114 | for (let i = 0; i < 4; i++) { 115 | const childKey = parentKey + i; // 构建子节点的 quadkey 116 | 117 | if (isLeaf) { 118 | tiles_info[childKey] = null; // 子节点的子节点为 null 119 | } else if (level < 4) { 120 | if (!parent.has_child(i)) { 121 | tiles_info[childKey] = null; // 如果父节点没有该子节点,设置为 null 122 | } else { 123 | if (index >= nodes.length) { 124 | console.error("Incorrect number of instances"); 125 | return; 126 | } 127 | const instance = nodes[index++]; 128 | tiles_info[childKey] = instance; // 设置子节点 129 | populate_tiles(childKey, instance, level + 1); // 递归处理子节点 130 | } 131 | } 132 | } 133 | } 134 | 135 | // 处理根节点 136 | const root = nodes[index++]; 137 | if (quad_key === "") { 138 | populate_tiles("", root, 1); // 根节点从 level 1 开始 139 | } else { 140 | tiles_info[quad_key] = root; // 非根节点直接设置 141 | populate_tiles(quad_key, root, 0); // 从 level 0 开始 142 | } 143 | 144 | return tiles_info; 145 | } 146 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "jsr:@oak/oak/application"; 2 | import { Router } from "jsr:@oak/oak/router"; 3 | import { get_tile } from "./ge.ts"; 4 | import { get_version_and_key } from "./version.ts"; 5 | import { get_current_dir } from "./cache.ts"; 6 | import { get_history_tile, get_hisversion, query_point } from "./history.ts"; 7 | import { dirname, join } from "jsr:@std/path"; 8 | import { create_ge_cap, create_ge_his_cap } from "./wmts.ts"; 9 | import { GeoPoint } from "@liuxspro/capgen"; 10 | 11 | console.log("初始化..."); 12 | 13 | // 获取当前脚本的绝对路径 14 | const current_dir = get_current_dir(); 15 | const root_dir = dirname(current_dir); 16 | const data_dir = join(root_dir, "data"); 17 | // static file path 18 | const error_png_path = join(data_dir, "error.png"); 19 | const error_png = Deno.readFileSync(error_png_path); 20 | 21 | // 获取当前版本和密钥 22 | let { version, key } = await get_version_and_key(); 23 | let his_version = await get_hisversion(); 24 | 25 | console.log(`[Init] [Get Version]: Earth: ${version} History: ${his_version}`); 26 | console.log("初始化完成!\n"); 27 | 28 | // 每小时更新版本 29 | Deno.cron("get version", "0 * * * *", async () => { 30 | ({ version, key } = await get_version_and_key()); 31 | his_version = await get_hisversion(); 32 | console.log( 33 | `[Cron Job] [Get Version]: Earth:${version} History: ${his_version}`, 34 | ); 35 | }); 36 | 37 | function isDenoDeploy(): boolean { 38 | return Deno.env.has("DENO_DEPLOYMENT_ID"); 39 | } 40 | 41 | function get_host(): string { 42 | let host = "http://localhost:8080"; 43 | if (isDenoDeploy()) { 44 | host = "https://gewmts.deno.dev"; 45 | } 46 | return host; 47 | } 48 | 49 | const router = new Router(); 50 | router.get("/:z/:x/:y", async (ctx) => { 51 | const { z, x, y } = ctx.params; 52 | const nz = parseInt(z); 53 | const nx = parseInt(x); 54 | const ny = parseInt(y); 55 | 56 | const tile_data = await get_tile(nx, ny, nz, version, key); 57 | if (tile_data) { 58 | ctx.response.type = "image/jpg"; 59 | ctx.response.body = tile_data; 60 | } else { 61 | ctx.response.type = "image/png"; 62 | ctx.response.body = error_png; 63 | } 64 | }); 65 | 66 | router.get("/history/:z/:x/:y", async (ctx) => { 67 | const { z, x, y } = ctx.params; 68 | const nz = parseInt(z); 69 | const nx = parseInt(x); 70 | const ny = parseInt(y); 71 | const date = ctx.request.url.searchParams.get("d"); 72 | const version = ctx.request.url.searchParams.get("v"); 73 | 74 | if (!date) { 75 | ctx.response.status = 400; // Bad Request 76 | ctx.response.body = { error: "Missing 'date' query parameter" }; 77 | return; 78 | } 79 | 80 | const version_n = parseInt(version || "0", 10); 81 | 82 | const tile_data = await get_history_tile(nx, ny, nz, version_n, date, key); 83 | 84 | if (tile_data) { 85 | ctx.response.type = "image/jpg"; 86 | ctx.response.body = tile_data; 87 | } else { 88 | ctx.response.type = "image/png"; 89 | ctx.response.body = error_png; 90 | } 91 | }); 92 | 93 | /** 94 | * 根据经纬度和层级查询历史影像列表 95 | * http://localhost:8080/ge/his/query?lon=117.11919576379941&lat=34.25658580862091&level=18 96 | */ 97 | router.get("/his/query", async (ctx) => { 98 | const lon = ctx.request.url.searchParams.get("lon") || ""; 99 | const lat = ctx.request.url.searchParams.get("lat") || ""; 100 | const level = ctx.request.url.searchParams.get("level") || ""; 101 | const lon_number = parseFloat(lon); 102 | const lat_number = parseFloat(lat); 103 | const level_number = parseFloat(level); 104 | const layers = await query_point( 105 | lat_number, 106 | lon_number, 107 | level_number, 108 | his_version, 109 | key, 110 | ); 111 | ctx.response.type = "text/json"; 112 | ctx.response.body = layers; 113 | }); 114 | 115 | router.get("/wmts", (ctx) => { 116 | ctx.response.type = "text/xml;charset=UTF-8"; 117 | const xml = create_ge_cap(`${get_host()}/{z}/{x}/{y}`); 118 | ctx.response.body = xml; 119 | }); 120 | 121 | router.get("/his/wmts", (ctx) => { 122 | const d = ctx.request.url.searchParams.get("d") || ""; 123 | const v = ctx.request.url.searchParams.get("v") || ""; 124 | const lower = ctx.request.url.searchParams.get("l") || "-180.0 -85.051129"; 125 | let [lon, lat] = lower.split(" ").map(Number); 126 | const lower_point: GeoPoint = { lon, lat }; 127 | const upper = ctx.request.url.searchParams.get("u") || "180.0 85.051129"; 128 | [lon, lat] = upper.split(" ").map(Number); 129 | const upper_point = { lon, lat }; 130 | const bbox: [GeoPoint, GeoPoint] = [lower_point, upper_point]; 131 | const url = `${get_host()}/history/{z}/{x}/{y}?d=${d}&v=${v}`; 132 | const xml = create_ge_his_cap(bbox, url); 133 | ctx.response.type = "text/xml;charset=UTF-8"; 134 | ctx.response.body = xml; 135 | }); 136 | 137 | router.get("/", (ctx) => { 138 | ctx.response.body = "On Deno Deploy 💖"; 139 | }); 140 | 141 | const app = new Application(); 142 | app.use(router.routes()); 143 | app.use(router.allowedMethods()); 144 | 145 | console.log("Server is runing..."); 146 | console.log("访问 http://localhost:8080/ge/wmts"); 147 | app.listen({ port: 8080 }); 148 | -------------------------------------------------------------------------------- /data/quadtreeset.cn.proto: -------------------------------------------------------------------------------- 1 | // 版权所有 2017 Google Inc. 2 | // 3 | // 根据 Apache License, Version 2.0 (以下简称“许可证”) 许可,您只能在遵守许可证的前提下使用本文件。 4 | // 您可以在以下网址获得许可证副本: 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // 除非适用法律要求或书面同意,否则本软件按“原样”提供,没有任何明示或暗示的保证或条件。 9 | // 请参阅许可证以了解特定的许可权和限制。 10 | 11 | // 12 | // 下一代四叉树数据包的协议缓冲区定义。 13 | // 14 | // 我们将地球划分为四叉树。每个四叉树的节点包含该节点上可用的图层信息。 15 | // 整个四叉树被划分为 QuadtreePacket,每个 QuadtreePacket 包含了整个四叉树的一个 n 层子树。 16 | 17 | syntax = "proto2"; 18 | 19 | package keyhole; 20 | 21 | // 渠道信息来自 Fusion,并使用 Fusion 的编号方案。 22 | message QuadtreeChannel { 23 | required int32 type = 1; // 渠道类型 24 | required int32 channel_epoch = 2; // 渠道的时间戳 25 | }; 26 | 27 | // 用于特定日期的“定时”图块: 28 | // 每个图块在该日期和版本的图块信息。 29 | message QuadtreeImageryTimedTile { 30 | // 从午夜起的毫秒数,必须是“required”以使旧版客户端兼容 31 | required int32 milliseconds = 1; 32 | 33 | // 定时图块的版本,如果与 “QuadtreeImageryDatedTile” 中的 "dated_tile_epoch" 不同,则存在 34 | // 必须是“required”以使旧版客户端兼容 35 | required int32 timed_tile_epoch = 2; 36 | 37 | // 定时图块的提供者,如果与 “QuadtreeImageryDatedTile” 中的 "provider" 不同,则存在 38 | optional int32 provider = 3; 39 | }; 40 | 41 | // 时间机器的图层特定信息。 42 | // 每个可用的日期图块有一个条目。 43 | // 日期使用 JpegCommentDate::YearMonthDayKey 格式。 44 | // dated_tile_epoch 是时间机器图块的版本。 45 | // 如果图块可以与 keyhole-hires 共享,则给出其共享图块的日期。 46 | // timed_tiles 字段包含具有比一天更精细时间分辨率的额外图块。 47 | // timed_tiles 将按升序排序。 48 | // 请注意,旧客户端仍然需要“全天”图块,无法看到 timed_tiles 字段。 49 | message QuadtreeImageryDatedTile { 50 | required int32 date = 1; // 图块日期 51 | required int32 dated_tile_epoch = 2; // 时间机器图块的版本 52 | required int32 provider = 3; // 图块提供者 53 | repeated QuadtreeImageryTimedTile timed_tiles = 4; // 定时图块列表 54 | }; 55 | 56 | message QuadtreeImageryDates { 57 | repeated QuadtreeImageryDatedTile dated_tile = 1; // 可用的日期图块 58 | optional int32 shared_tile_date = 2; // 从 keyhole 共享的图块的日期 59 | // 从粗略层级中可见的图层的日期。这些日期按升序排序。 60 | // coarse_tile_dates 中的日期与 dated_tile 中的图块日期不交集,而是补充的。 61 | // coarse_tile_dates 属于上层分辨率较低的图块,当前层级不可见。它们存在于这里,以便让用户知道上层不同的图层。 62 | repeated int32 coarse_tile_dates = 3; 63 | // 从 keyhole 共享的图块的时间(从午夜起的毫秒数)。 64 | optional int32 shared_tile_milliseconds = 4; 65 | }; 66 | 67 | // 四叉树图层定义 68 | message QuadtreeLayer { 69 | enum LayerType { 70 | LAYER_TYPE_IMAGERY = 0; // 图像图层 71 | LAYER_TYPE_TERRAIN = 1; // 地形图层 72 | LAYER_TYPE_VECTOR = 2; // 向量图层 73 | LAYER_TYPE_IMAGERY_HISTORY = 3; // 图像历史图层 74 | } 75 | 76 | required LayerType type = 1; // 图层类型 77 | required int32 layer_epoch = 2; // 图层的版本时间戳 78 | optional int32 provider = 3; // 图层的提供者 ID 79 | 80 | // 如果图层类型有额外信息,则在这里添加: 81 | optional QuadtreeImageryDates dates_layer = 4; // 图层日期信息 82 | }; 83 | 84 | message QuadtreeNode { 85 | // 此节点的标志位的位域。 86 | // 0-3: 表示每个内部子节点的存在位 87 | // 4: 对于叶节点,表示此节点下方是否有其他四叉树集 88 | // 5: 表示此节点中是否有向量数据 89 | // 6: 表示此节点中是否有图像数据 90 | // 7: 表示此节点中是否有地形数据 91 | enum NodeFlags { 92 | option allow_alias = true; // 允许多个枚举值共享相同的数值,不然会报错 93 | NODE_FLAGS_CHILD_COUNT = 4; // 内部节点指示子节点存在 94 | NODE_FLAGS_CACHE_BIT = 4; // 叶节点下有数据 95 | NODE_FLAGS_DRAWABLE_BIT = 5; // 此节点有向量数据 96 | NODE_FLAGS_IMAGE_BIT = 6; // 此节点有图像数据 97 | NODE_FLAGS_TERRAIN_BIT = 7; // 此节点有地形数据 98 | } 99 | 100 | optional int32 flags = 1; // 节点标志 101 | 102 | // 此节点生成的版本时间戳。 103 | // 目前,数据包中的所有节点同时生成,因此每个节点的版本相同。 104 | // 如果我们只生成已更改的节点,未来可能会有所不同。 105 | // 客户端使用该信息保持其节点缓存更新。 106 | optional int32 cache_node_epoch = 2; 107 | 108 | // 该节点的图层数据。 109 | // 图层:图像、向量、地形等。 110 | repeated QuadtreeLayer layer = 3; 111 | 112 | // 该节点的渠道信息。 113 | // 渠道是向量图层的组成部分,来自 Fusion 流程。 114 | repeated QuadtreeChannel channel = 4; 115 | }; 116 | 117 | // 完整的四叉树数据包。包含整个全球四叉树的一个子树。 118 | message QuadtreePacket { 119 | // 此四叉树数据包生成的版本时间戳。 120 | required int32 packet_epoch = 1; 121 | 122 | // 节点有两种编号方案: 123 | // 124 | // 1) “子索引”。这种编号从树的顶部开始,按每一层的左到右的顺序排列,如下所示: 125 | // 126 | // 0 127 | // / \ 128 | // 1 86 171 256 129 | // / \ 130 | // 2 3 4 5 ... 131 | // / \ 132 | // 6 7 8 9 ... 133 | // 134 | // 注意,第二行是特殊的,它不是左到右顺序。但是,Keyhole 的根节点是特殊的,它的排列不按这个顺序: 135 | // 136 | // 0 137 | // / \ 138 | // 1 2 3 4 139 | // / \ 140 | // 5 6 7 8 ... 141 | // / \ 142 | // 21 22 23 24 ... 143 | // 144 | // 第二行的混乱通过 TreeNumbering 构造函数的参数进行控制。 145 | // 146 | // 2) “中序遍历”。节点在中序遍历中的顺序。 147 | // 148 | // 客户端使用子索引顺序请求节点。 149 | // 有关详细信息,请参见 googleclient/geo/earth_enterprise/src/common/qtpacket/tree_utils.cpp。 150 | 151 | // 此数据包中的所有四叉树节点,按子索引编号。 152 | repeated group SparseQuadtreeNode = 2 { 153 | // 此节点在四叉树数据包中的索引。四叉树数据包中的节点按子索引顺序编号且稀疏。 154 | // 注意:节点当前不保证按任何特定顺序排序。 155 | required int32 index = 3; // 子索引索引 156 | required QuadtreeNode Node = 4; // 节点本身 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /src/quad.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将地理坐标(经纬度)转换为四叉树键(QuadKey)。 3 | * 4 | * 该函数通过递归细分地理区域(-180°~180°纬度,-180°~180°经度),生成表示坐标位置的字符串编码。 5 | * 编码规则: 6 | * - 第一位固定为 '0' 7 | * - 从第二位开始,每个字符表示当前区域的象限划分: 8 | * - '0':左下象限 9 | * - '1':右下象限 10 | * - '2':右上象限 11 | * - '3':左上象限 12 | * 13 | * @param {number} lat - 纬度(范围:-180° ~ 180°) 14 | * @param {number} lon - 经度(范围:-180° ~ 180°) 15 | * @param {number} depth - 四叉树的深度(决定编码长度,例如 depth=3 → 编码长度为 4) 16 | * @returns {string} 四叉树键字符串(例如 "0321") 17 | * @throws {Error} 如果纬度或经度超出有效范围 18 | * 19 | * @example 20 | * // 基本用法 21 | * const quad_key = gcs_to_quad(45.0, -90.0, 4); 22 | * console.log(quad_key); // 例如 "0321" 23 | */ 24 | export function gcs_to_quad(lat: number, lon: number, depth: number): string { 25 | let code = "0"; // 第一位固定为 '0' 26 | let lat1 = 180; 27 | let lon1 = -180; // 初始区域左上角坐标 28 | let lat2 = -180; 29 | let lon2 = 180; // 初始区域右下角坐标 30 | 31 | for (let i = 1; i <= depth; i++) { 32 | // 从第二位开始划分 33 | const midLat = (lat1 + lat2) / 2; // 中间纬度 34 | const midLon = (lon1 + lon2) / 2; // 中间经度 35 | 36 | if (lat >= midLat && lon < midLon) { 37 | // 左上象限('3') 38 | code += "3"; 39 | lat2 = midLat; 40 | lon2 = midLon; 41 | } else if (lat >= midLat && lon >= midLon) { 42 | // 右上象限('2') 43 | code += "2"; 44 | lat2 = midLat; 45 | lon1 = midLon; 46 | } else if (lat < midLat && lon >= midLon) { 47 | // 右下象限('1') 48 | code += "1"; 49 | lat1 = midLat; 50 | lon1 = midLon; 51 | } else if (lat < midLat && lon < midLon) { 52 | // 左下象限('0') 53 | code += "0"; 54 | lat1 = midLat; 55 | lon2 = midLon; 56 | } else { 57 | throw new Error("Invalid lat/lon values"); 58 | } 59 | } 60 | return code; 61 | } 62 | 63 | /** 64 | * 将 XYZ 瓦片坐标转换为对应瓦片*中心*的地理坐标(经纬度)。 65 | * 66 | * 根据 EPSG:4326 瓦片坐标系规则,计算指定瓦片(x, y, z)中心点的经纬度。 67 | * 68 | * @param {number} x - 瓦片的 X 坐标(范围:0 ≤ x < 2^z) 69 | * @param {number} y - 瓦片的 Y 坐标(范围:0 ≤ y < 2^z) 70 | * @param {number} z - 缩放级别(z ≥ 0) 71 | * @returns {{ lon: number, lat: number }} 瓦片中心点的经纬度(单位:度) 72 | * 73 | * @example 74 | * // 获取瓦片 (x=3, y=5, z=3) 的中心坐标 75 | * const coord = xyz_to_gcs(3, 5, 3); 76 | * console.log(coord); // 输出示例: { lon: 45, lat: -33.75 } 77 | */ 78 | function xyz_to_gcs( 79 | x: number, 80 | y: number, 81 | z: number 82 | ): { lon: number; lat: number } { 83 | const lon = (x * 360) / Math.pow(2, z) - 180; 84 | // 计算瓦片中心点坐标 85 | const d_lon = 360 / Math.pow(2, z); 86 | const lat = 90 - (y * 180) / Math.pow(2, z - 1); 87 | const d_lat = 180 / Math.pow(2, z - 1); 88 | return { lon: lon + d_lon / 2, lat: lat - d_lat / 2 }; 89 | } 90 | 91 | /** 92 | * 将XYZ行列号转为 Quad 四叉树编码 93 | * @param {number} x - X 行列号 94 | * @param {number} y - Y 行列号 95 | * @param {number} z - Z 缩放级别 96 | * @returns {string} 97 | */ 98 | function xyz_to_quad(x: number, y: number, z: number): string { 99 | const coord = xyz_to_gcs(x, y, z); 100 | return gcs_to_quad(coord.lat, coord.lon, z); 101 | } 102 | 103 | /** 104 | * 表示一个四叉树键 `QuadKey`, 用于空间索引或地图瓦片坐标系统。 105 | * 106 | * 支持通过以下方式初始化: 107 | * - 直接传入 `quad_key` 字符串 108 | * - 通过 `x, y, z` 行列号生成对应的 `quad_key` 109 | * 110 | * @class 111 | * @example 112 | * // 通过字符串初始化 113 | * const key1 = new QuadKey("0210230110210"); 114 | * // 通过行列号初始化 115 | * const key2 = new QuadKey(2, 1, 3); 116 | */ 117 | export class QuadKey { 118 | /** 119 | * 四叉树键 Quad 的完整字符串表示。 120 | * @type {string} 121 | * @readonly 122 | */ 123 | quad_key: string; 124 | 125 | /** 126 | * 创建一个 QuadKey 实例。 127 | * @constructor 128 | * @overload 129 | * @param {string} quad_key - 四叉树键的字符串表示 130 | * 131 | * @overload 132 | * @param {number} x - X 行列号 133 | * @param {number} y - Y 行列号 134 | * @param {number} z - Z 缩放级别 135 | * 136 | * @throws {Error} 如果参数类型不合法 137 | */ 138 | constructor(quad_key: string); 139 | constructor(x: number, y: number, z: number); 140 | constructor(...args: [string] | [number, number, number]) { 141 | if (typeof args[0] === "string") { 142 | // 通过字符串初始化 143 | this.quad_key = args[0]; 144 | } else if ( 145 | typeof args[0] === "number" && 146 | typeof args[1] === "number" && 147 | typeof args[2] === "number" 148 | ) { 149 | // 通过 x, y, z 初始化 150 | this.quad_key = xyz_to_quad(args[0], args[1], args[2]); 151 | } else { 152 | throw new Error("Invalid initialization parameters"); 153 | } 154 | } 155 | 156 | /** 157 | * 获取去掉第一位的短 quad_key(例如 "021023" → "21023")。 158 | * @type {string} 159 | * @readonly 160 | */ 161 | get short_quad_key(): string { 162 | return this.quad_key.slice(1); 163 | } 164 | 165 | // Z=13 X=6764 Y=1267 对应的quad为 02102301102200 166 | // qtree只有在1,4,8,12,16,20级别(位数) 167 | // 02102301102200 位数为14 ,qtree信息应该到上一级的12 168 | // 也就是去掉两位数后 021023011022 中去寻找 169 | 170 | /** 171 | * 获取父qtree文件信息 172 | * 即当前quad等信息应该去请求哪个qtree 173 | * 174 | * qtree只在第0、3、7、11、15、19级即0、0000、00000000等 175 | * (级别 = quad 位数 - 1) 176 | * 177 | * 找到比当前 quad 级别小的最大级别 178 | * 如寻找 02103103(7,100,22) 在哪个 qtree 中 179 | * 级别为7,应该在第3级中去寻找即 0210 180 | * (虽然7也是qtree级别,但是qtree的第一项数据是未定义的,要在再上一级中找) 181 | * @type {string} 182 | * @readonly 183 | */ 184 | get parent_quad_key(): string { 185 | const level = this.quad_key.length - 1; 186 | const parent_level = [0, 3, 7, 11, 15, 19]; 187 | let target_level = 0; 188 | if (level > 0) { 189 | target_level = Math.max(...parent_level.filter((num) => num < level)); 190 | } 191 | const qtree_name = this.quad_key.slice(0, target_level + 1); 192 | return qtree_name; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /test/key.ts: -------------------------------------------------------------------------------- 1 | export const key = new Uint8Array([ 2 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0xf4, 0xbd, 0x0b, 0x79, 3 | 0xe2, 0x6a, 0x45, 0x22, 0x05, 0x92, 0x2c, 0x17, 0xcd, 0x06, 0x71, 0xf8, 0x49, 4 | 0x10, 0x46, 0x67, 0x51, 0x00, 0x42, 0x25, 0xc6, 0xe8, 0x61, 0x2c, 0x66, 0x29, 5 | 0x08, 0xc6, 0x34, 0xdc, 0x6a, 0x62, 0x25, 0x79, 0x0a, 0x77, 0x1d, 0x6d, 0x69, 6 | 0xd6, 0xf0, 0x9c, 0x6b, 0x93, 0xa1, 0xbd, 0x4e, 0x75, 0xe0, 0x41, 0x04, 0x5b, 7 | 0xdf, 0x40, 0x56, 0x0c, 0xd9, 0xbb, 0x72, 0x9b, 0x81, 0x7c, 0x10, 0x33, 0x53, 8 | 0xee, 0x4f, 0x6c, 0xd4, 0x71, 0x05, 0xb0, 0x7b, 0xc0, 0x7f, 0x45, 0x03, 0x56, 9 | 0x5a, 0xad, 0x77, 0x55, 0x65, 0x0b, 0x33, 0x92, 0x2a, 0xac, 0x19, 0x6c, 0x35, 10 | 0x14, 0xc5, 0x1d, 0x30, 0x73, 0xf8, 0x33, 0x3e, 0x6d, 0x46, 0x38, 0x4a, 0xb4, 11 | 0xdd, 0xf0, 0x2e, 0xdd, 0x17, 0x75, 0x16, 0xda, 0x8c, 0x44, 0x74, 0x22, 0x06, 12 | 0xfa, 0x61, 0x22, 0x0c, 0x33, 0x22, 0x53, 0x6f, 0xaf, 0x39, 0x44, 0x0b, 0x8c, 13 | 0x0e, 0x39, 0xd9, 0x39, 0x13, 0x4c, 0xb9, 0xbf, 0x7f, 0xab, 0x5c, 0x8c, 0x50, 14 | 0x5f, 0x9f, 0x22, 0x75, 0x78, 0x1f, 0xe9, 0x07, 0x71, 0x91, 0x68, 0x3b, 0xc1, 15 | 0xc4, 0x9b, 0x7f, 0xf0, 0x3c, 0x56, 0x71, 0x48, 0x82, 0x05, 0x27, 0x55, 0x66, 16 | 0x59, 0x4e, 0x65, 0x1d, 0x98, 0x75, 0xa3, 0x61, 0x46, 0x7d, 0x61, 0x3f, 0x15, 17 | 0x41, 0x00, 0x9f, 0x14, 0x06, 0xd7, 0xb4, 0x34, 0x4d, 0xce, 0x13, 0x87, 0x46, 18 | 0xb0, 0x1a, 0xd5, 0x05, 0x1c, 0xb8, 0x8a, 0x27, 0x7b, 0x8b, 0xdc, 0x2b, 0xbb, 19 | 0x4d, 0x67, 0x30, 0xc8, 0xd1, 0xf6, 0x5c, 0x8f, 0x50, 0xfa, 0x5b, 0x2f, 0x46, 20 | 0x9b, 0x6e, 0x35, 0x18, 0x2f, 0x27, 0x43, 0x2e, 0xeb, 0x0a, 0x0c, 0x5e, 0x10, 21 | 0x05, 0x10, 0xa5, 0x73, 0x1b, 0x65, 0x34, 0xe5, 0x6c, 0x2e, 0x6a, 0x43, 0x27, 22 | 0x63, 0x14, 0x23, 0x55, 0xa9, 0x3f, 0x71, 0x7b, 0x67, 0x43, 0x7d, 0x3a, 0xaf, 23 | 0xcd, 0xe2, 0x54, 0x55, 0x9c, 0xfd, 0x4b, 0xc6, 0xe2, 0x9f, 0x2f, 0x28, 0xed, 24 | 0xcb, 0x5c, 0xc6, 0x2d, 0x66, 0x07, 0x88, 0xa7, 0x3b, 0x2f, 0x18, 0x2a, 0x22, 25 | 0x4e, 0x0e, 0xb0, 0x6b, 0x2e, 0xdd, 0x0d, 0x95, 0x7d, 0x7d, 0x47, 0xba, 0x43, 26 | 0xb2, 0x11, 0xb2, 0x2b, 0x3e, 0x4d, 0xaa, 0x3e, 0x7d, 0xe6, 0xce, 0x49, 0x89, 27 | 0xc6, 0xe6, 0x78, 0x0c, 0x61, 0x31, 0x05, 0x2d, 0x01, 0xa4, 0x4f, 0xa5, 0x7e, 28 | 0x71, 0x20, 0x88, 0xec, 0x0d, 0x31, 0xe8, 0x4e, 0x0b, 0x00, 0x6e, 0x50, 0x68, 29 | 0x7d, 0x17, 0x3d, 0x08, 0x0d, 0x17, 0x95, 0xa6, 0x6e, 0xa3, 0x68, 0x97, 0x24, 30 | 0x5b, 0x6b, 0xf3, 0x17, 0x23, 0xf3, 0xb6, 0x73, 0xb3, 0x0d, 0x0b, 0x40, 0xc0, 31 | 0x9f, 0xd8, 0x04, 0x51, 0x5d, 0xfa, 0x1a, 0x17, 0x22, 0x2e, 0x15, 0x6a, 0xdf, 32 | 0x49, 0x00, 0xb9, 0xa0, 0x77, 0x55, 0xc6, 0xef, 0x10, 0x6a, 0xbf, 0x7b, 0x47, 33 | 0x4c, 0x7f, 0x83, 0x17, 0x05, 0xee, 0xdc, 0xdc, 0x46, 0x85, 0xa9, 0xad, 0x53, 34 | 0x07, 0x2b, 0x53, 0x34, 0x06, 0x07, 0xff, 0x14, 0x94, 0x59, 0x19, 0x02, 0xe4, 35 | 0x38, 0xe8, 0x31, 0x83, 0x4e, 0xb9, 0x58, 0x46, 0x6b, 0xcb, 0x2d, 0x23, 0x86, 36 | 0x92, 0x70, 0x00, 0x35, 0x88, 0x22, 0xcf, 0x31, 0xb2, 0x26, 0x2f, 0xe7, 0xc3, 37 | 0x75, 0x2d, 0x36, 0x2c, 0x72, 0x74, 0xb0, 0x23, 0x47, 0xb7, 0xd3, 0xd1, 0x26, 38 | 0x16, 0x85, 0x37, 0x72, 0xe2, 0x00, 0x8c, 0x44, 0xcf, 0x10, 0xda, 0x33, 0x2d, 39 | 0x1a, 0xde, 0x60, 0x86, 0x69, 0x23, 0x69, 0x2a, 0x7c, 0xcd, 0x4b, 0x51, 0x0d, 40 | 0x95, 0x54, 0x39, 0x77, 0x2e, 0x29, 0xea, 0x1b, 0xa6, 0x50, 0xa2, 0x6a, 0x8f, 41 | 0x6f, 0x50, 0x99, 0x5c, 0x3e, 0x54, 0xfb, 0xef, 0x50, 0x5b, 0x0b, 0x07, 0x45, 42 | 0x17, 0x89, 0x6d, 0x28, 0x13, 0x77, 0x37, 0x1d, 0xdb, 0x8e, 0x1e, 0x4a, 0x05, 43 | 0x66, 0x4a, 0x6f, 0x99, 0x20, 0xe5, 0x70, 0xe2, 0xb9, 0x71, 0x7e, 0x0c, 0x6d, 44 | 0x49, 0x04, 0x2d, 0x7a, 0xfe, 0x72, 0xc7, 0xf2, 0x59, 0x30, 0x8f, 0xbb, 0x02, 45 | 0x5d, 0x73, 0xe5, 0xc9, 0x20, 0xea, 0x78, 0xec, 0x20, 0x90, 0xf0, 0x8a, 0x7f, 46 | 0x42, 0x17, 0x7c, 0x47, 0x19, 0x60, 0xb0, 0x16, 0xbd, 0x26, 0xb7, 0x71, 0xb6, 47 | 0xc7, 0x9f, 0x0e, 0xd1, 0x33, 0x82, 0x3d, 0xd3, 0xab, 0xee, 0x63, 0x99, 0xc8, 48 | 0x2b, 0x53, 0xa0, 0x44, 0x5c, 0x71, 0x01, 0xc6, 0xcc, 0x44, 0x1f, 0x32, 0x4f, 49 | 0x3c, 0xca, 0xc0, 0x29, 0x3d, 0x52, 0xd3, 0x61, 0x19, 0x58, 0xa9, 0x7d, 0x65, 50 | 0xb4, 0xdc, 0xcf, 0x0d, 0xf4, 0x3d, 0xf1, 0x08, 0xa9, 0x42, 0xda, 0x23, 0x09, 51 | 0xd8, 0xbf, 0x5e, 0x50, 0x49, 0xf8, 0x4d, 0xc0, 0xcb, 0x47, 0x4c, 0x1c, 0x4f, 52 | 0xf7, 0x7b, 0x2b, 0xd8, 0x16, 0x18, 0xc5, 0x31, 0x92, 0x3b, 0xb5, 0x6f, 0xdc, 53 | 0x6c, 0x0d, 0x92, 0x88, 0x16, 0xd1, 0x9e, 0xdb, 0x3f, 0xe2, 0xe9, 0xda, 0x5f, 54 | 0xd4, 0x84, 0xe2, 0x46, 0x61, 0x5a, 0xde, 0x1c, 0x55, 0xcf, 0xa4, 0x00, 0xbe, 55 | 0xfd, 0xce, 0x67, 0xf1, 0x4a, 0x69, 0x1c, 0x97, 0xe6, 0x20, 0x48, 0xd8, 0x5d, 56 | 0x7f, 0x7e, 0xae, 0x71, 0x20, 0x0e, 0x4e, 0xae, 0xc0, 0x56, 0xa9, 0x91, 0x01, 57 | 0x3c, 0x82, 0x1d, 0x0f, 0x72, 0xe7, 0x76, 0xec, 0x29, 0x49, 0xd6, 0x5d, 0x2d, 58 | 0x83, 0xe3, 0xdb, 0x36, 0x06, 0xa9, 0x3b, 0x66, 0x13, 0x97, 0x87, 0x6a, 0xd5, 59 | 0xb6, 0x3d, 0x50, 0x5e, 0x52, 0xb9, 0x4b, 0xc7, 0x73, 0x57, 0x78, 0xc9, 0xf4, 60 | 0x2e, 0x59, 0x07, 0x95, 0x93, 0x6f, 0xd0, 0x4b, 0x17, 0x57, 0x19, 0x3e, 0x27, 61 | 0x27, 0xc7, 0x60, 0xdb, 0x3b, 0xed, 0x9a, 0x0e, 0x53, 0x44, 0x16, 0x3e, 0x3f, 62 | 0x8d, 0x92, 0x6d, 0x77, 0xa2, 0x0a, 0xeb, 0x3f, 0x52, 0xa8, 0xc6, 0x55, 0x5e, 63 | 0x31, 0x49, 0x37, 0x85, 0xf4, 0xc5, 0x1f, 0x26, 0x2d, 0xa9, 0x1c, 0xbf, 0x8b, 64 | 0x27, 0x54, 0xda, 0xc3, 0x6a, 0x20, 0xe5, 0x2a, 0x78, 0x04, 0xb0, 0xd6, 0x90, 65 | 0x70, 0x72, 0xaa, 0x8b, 0x68, 0xbd, 0x88, 0xf7, 0x02, 0x5f, 0x48, 0xb1, 0x7e, 66 | 0xc0, 0x58, 0x4c, 0x3f, 0x66, 0x1a, 0xf9, 0x3e, 0xe1, 0x65, 0xc0, 0x70, 0xa7, 67 | 0xcf, 0x38, 0x69, 0xaf, 0xf0, 0x56, 0x6c, 0x64, 0x49, 0x9c, 0x27, 0xad, 0x78, 68 | 0x74, 0x4f, 0xc2, 0x87, 0xde, 0x56, 0x39, 0x00, 0xda, 0x77, 0x0b, 0xcb, 0x2d, 69 | 0x1b, 0x89, 0xfb, 0x35, 0x4f, 0x02, 0xf5, 0x08, 0x51, 0x13, 0x60, 0xc1, 0x0a, 70 | 0x5a, 0x47, 0x4d, 0x26, 0x1c, 0x33, 0x30, 0x78, 0xda, 0xc0, 0x9c, 0x46, 0x47, 71 | 0xe2, 0x5b, 0x79, 0x60, 0x49, 0x6e, 0x37, 0x67, 0x53, 0x0a, 0x3e, 0xe9, 0xec, 72 | 0x46, 0x39, 0xb2, 0xf1, 0x34, 0x0d, 0xc6, 0x84, 0x53, 0x75, 0x6e, 0xe1, 0x0c, 73 | 0x59, 0xd9, 0x1e, 0xde, 0x29, 0x85, 0x10, 0x7b, 0x49, 0x49, 0xa5, 0x77, 0x79, 74 | 0xbe, 0x49, 0x56, 0x2e, 0x36, 0xe7, 0x0b, 0x3a, 0xbb, 0x4f, 0x03, 0x62, 0x7b, 75 | 0xd2, 0x4d, 0x31, 0x95, 0x2f, 0xbd, 0x38, 0x7b, 0xa8, 0x4f, 0x21, 0xe1, 0xec, 76 | 0x46, 0x70, 0x76, 0x95, 0x7d, 0x29, 0x22, 0x78, 0x88, 0x0a, 0x90, 0xdd, 0x9d, 77 | 0x5c, 0xda, 0xde, 0x19, 0x51, 0xcf, 0xf0, 0xfc, 0x59, 0x52, 0x65, 0x7c, 0x33, 78 | 0x13, 0xdf, 0xf3, 0x48, 0xda, 0xbb, 0x2a, 0x75, 0xdb, 0x60, 0xb2, 0x02, 0x15, 79 | 0xd4, 0xfc, 0x19, 0xed, 0x1b, 0xec, 0x7f, 0x35, 0xa8, 0xff, 0x28, 0x31, 0x07, 80 | 0x2d, 0x12, 0xc8, 0xdc, 0x88, 0x46, 0x7c, 0x8a, 0x5b, 0x22, 81 | ]); 82 | -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import protobuf from "protobufjs"; 2 | import { decode_qtree_data } from "./decode.ts"; 3 | import { 4 | GEHistoryTileInfo, 5 | HistoryTilesInfo, 6 | SparseQuadtreeNode, 7 | Layer, 8 | } from "./info.ts"; 9 | import { QuadKey, gcs_to_quad } from "./quad.ts"; 10 | import { decode_tile } from "./decode.ts"; 11 | 12 | const kv = await Deno.openKv(); 13 | 14 | export async function get_his_dbroot() { 15 | const url = "http://khmdb.google.com/dbRoot.v5?db=tm&hl=zh-hans&gl=hk"; 16 | const data = await (await fetch(url)).bytes(); 17 | return data; 18 | } 19 | 20 | export async function get_hisversion() { 21 | const dbroot_data = await get_his_dbroot(); 22 | const version_byte = dbroot_data.slice(6, 8); 23 | // 组合为 16 位整数 小端序 24 | const uint16Value = (version_byte[1] << 8) | version_byte[0]; 25 | const version = uint16Value ^ 0x4200; 26 | return version; 27 | } 28 | 29 | /** 30 | * 请求 qtree packet 数据 31 | * @param {string} quad_key 完整的四叉树编码 32 | * @param {number} version 当前版本 33 | * @returns {Promise} 原始 qtree packet 数据 34 | */ 35 | export async function fetch_his_qtree_rawdata( 36 | quad_key: string, 37 | version: number 38 | ): Promise { 39 | const his_qtree_url = `https://khmdb.google.com/flatfile?db=tm&qp-${quad_key}-q.${version}`; 40 | const data = await (await fetch(his_qtree_url)).bytes(); 41 | return data; 42 | } 43 | 44 | /** 45 | * 获取历史模式 Qtree 数据 46 | * 从缓存中读取 47 | * @param quad_key 四叉树编码 48 | * @param version 版本 49 | * @param key 密钥 50 | * @returns 历史模式 Qtree 数据 51 | */ 52 | export async function get_his_qtree( 53 | quad_key: string, 54 | version: number, 55 | key: Uint8Array 56 | ) { 57 | // 检查该 qtree 文件是否已经被缓存 58 | const entry = await kv.get(["History", version, quad_key]); 59 | if (entry.value) { 60 | return decode_qtree_data(entry.value as Uint8Array, key); 61 | } else { 62 | const qtree_rawdata = await fetch_his_qtree_rawdata(quad_key, version); 63 | await kv.set(["History", version, quad_key], qtree_rawdata); 64 | return decode_qtree_data(qtree_rawdata, key); 65 | } 66 | } 67 | 68 | /** 69 | * 反序列化 Qtree 数据包 70 | * 这是一个 protobuf,proto 文件可在 https://github.com/google/earthenterprise/blob/master/earth_enterprise/src/keyhole/earth_client_protobuf/quadtreeset.protodevel 找到 71 | * @param { Uint8Array }qtree_data Qtree数据包 72 | * @returns 反序列化后的对象 73 | */ 74 | export async function deserialize_qtreepacket(qtree_data: Uint8Array) { 75 | const root = await protobuf.load("./data/quadtreeset.proto"); 76 | // 获取 QuadtreePacket 类型 77 | const QuadtreePacket = root.lookupType("keyhole.QuadtreePacket"); 78 | const message = QuadtreePacket.decode(qtree_data); 79 | return message.toJSON(); 80 | } 81 | 82 | /** 83 | * 从 qtree 数据中读取 tiles 84 | * @param qtree_data qtree 数据 85 | * @returns {Promise} Tile 列表 86 | */ 87 | export async function get_nodes_from_qtree( 88 | qtree_data: Uint8Array 89 | ): Promise { 90 | const qtree = await deserialize_qtreepacket(qtree_data); 91 | const sparse_qtree_nodes: SparseQuadtreeNode[] = qtree["sparseQuadtreeNode"]; 92 | const nodes: GEHistoryTileInfo[] = []; 93 | for (let i = 0; i < sparse_qtree_nodes.length; i++) { 94 | const flags = sparse_qtree_nodes[i].Node.flags; 95 | const node = sparse_qtree_nodes[i].Node; 96 | if (node.layer) { 97 | const item = new GEHistoryTileInfo(flags, node.layer); 98 | nodes.push(item); 99 | } 100 | } 101 | return nodes; 102 | } 103 | 104 | export async function parse_history_qtree( 105 | qtree_data: Uint8Array, 106 | quad_key: string 107 | ): Promise { 108 | const nodes = await get_nodes_from_qtree(qtree_data); 109 | const tiles_info: HistoryTilesInfo = {}; 110 | let index = 0; 111 | 112 | // 递归函数:填充 history_tiles_info 113 | function populate_tiles( 114 | parentKey: string, 115 | parent: GEHistoryTileInfo, 116 | level: number 117 | ) { 118 | // 如果是叶子节点(level === 4),设置所有子节点为 null 119 | const isLeaf = level === 4 && !parent.has_subtree(); 120 | 121 | for (let i = 0; i < 4; i++) { 122 | const childKey = parentKey + i; // 构建子节点的 quadkey 123 | 124 | if (isLeaf) { 125 | tiles_info[childKey] = null; // 子节点的子节点为 null 126 | } else if (level < 4) { 127 | if (!parent.has_child(i)) { 128 | tiles_info[childKey] = null; // 如果父节点没有该子节点,设置为 null 129 | } else { 130 | if (index >= nodes.length) { 131 | console.error("Incorrect number of instances"); 132 | return; 133 | } 134 | const instance = nodes[index++]; 135 | tiles_info[childKey] = instance; // 设置子节点 136 | populate_tiles(childKey, instance, level + 1); // 递归处理子节点 137 | } 138 | } 139 | } 140 | } 141 | 142 | // 处理根节点 143 | const root = nodes[index++]; 144 | if (quad_key === "") { 145 | populate_tiles("", root, 1); // 根节点从 level 1 开始 146 | } else { 147 | tiles_info[quad_key] = root; // 非根节点直接设置 148 | populate_tiles(quad_key, root, 0); // 从 level 0 开始 149 | } 150 | 151 | return tiles_info; 152 | } 153 | 154 | export async function get_history_tile( 155 | x: number, 156 | y: number, 157 | z: number, 158 | version: number, 159 | date: string, 160 | key: Uint8Array 161 | ) { 162 | const quad = new QuadKey(x, y, z); 163 | const tile_url = `https://khmdb.google.com/flatfile?db=tm&f1-${quad.quad_key}-i.${version}-${date}`; 164 | const raw_tile_data = await (await fetch(tile_url)).bytes(); 165 | const decrypted_tile_data = decode_tile(raw_tile_data, key); 166 | return decrypted_tile_data; 167 | } 168 | 169 | export function number_to_date(date_number: number) { 170 | // 1. 将十六进制转换为二进制 171 | // const binary = parseInt(hex_str, 16).toString(2); 172 | const binary = date_number.toString(2); 173 | 174 | // 2. 分割二进制为年、月、日 175 | const yearBinary = binary.slice(0, 11); // 前 11 位 176 | const monthBinary = binary.slice(11, 15); // 中间 4 位 177 | const dayBinary = binary.slice(15); // 最后 5 位 178 | 179 | // 3. 将二进制转换为十进制 180 | const year = parseInt(yearBinary, 2); 181 | const month = parseInt(monthBinary, 2); 182 | const day = parseInt(dayBinary, 2); 183 | 184 | // 4. 返回日期字符串 185 | return `${year}-${month.toString().padStart(2, "0")}-${day 186 | .toString() 187 | .padStart(2, "0")}`; 188 | } 189 | 190 | /** 191 | * 获取指定 quad 的历史影像信息 192 | * 193 | * @param {QuadKey} quad QuadKey 194 | * @param version 版本 195 | * @param key 密钥 196 | * @returns {Promise} 瓦片的 Node 信息 197 | */ 198 | export async function get_history_layer( 199 | quad: QuadKey, 200 | version: number, 201 | key: Uint8Array 202 | ): Promise { 203 | const qtree_data = await get_his_qtree(quad.parent_quad_key, version, key); 204 | const tiles = await parse_history_qtree(qtree_data, quad.parent_quad_key); 205 | const current_tile = tiles[quad.quad_key]; 206 | if (current_tile != null) { 207 | return current_tile.layer; 208 | } else { 209 | throw Error("Filed to get history layer"); 210 | } 211 | } 212 | 213 | // export function get_history_layer_dates(layer: DatesLayer) { 214 | // return layer 215 | // .filter((item) => item.date >= 10000) 216 | // .map((item) => number_to_date(item.date)); 217 | // } 218 | 219 | export async function query_point( 220 | lat: number, 221 | lon: number, 222 | level: number, 223 | version: number, 224 | key: Uint8Array 225 | ) { 226 | const quad = new QuadKey(gcs_to_quad(lat, lon, level)); 227 | const layers = await get_history_layer(quad, version, key); 228 | return layers; 229 | } 230 | -------------------------------------------------------------------------------- /data/quadtreeset.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // 16 | // Protocol buffers for next-generation quadtree packets. 17 | // 18 | // We divide the globe up into a quadtree. Each node of the quadtree contains 19 | // information about what layers are available at that node. The entire 20 | // quadtree is divided up into QuadtreePacket's, each of which contains an 21 | // n-level subtree of the full quadtree. 22 | 23 | syntax = "proto2"; 24 | 25 | package keyhole; 26 | 27 | 28 | 29 | // Channels come from Fusion and use Fusion's numbering scheme. 30 | message QuadtreeChannel { 31 | required int32 type = 1; 32 | required int32 channel_epoch = 2; 33 | }; 34 | 35 | // "Timed" tile for a specific day: 36 | // Each tile's tile within the day and version are given in this message. 37 | message QuadtreeImageryTimedTile { 38 | // Milliseconds since midnight, have to be "required" to make legacy client 39 | // work. 40 | required int32 milliseconds = 1; 41 | 42 | // The version of the timed tile, exists if different from "dated_tile_epoch" 43 | // in "QuadtreeImageryDatedTile", have to be "required" to make legacy client 44 | // work. 45 | required int32 timed_tile_epoch = 2; 46 | 47 | // The provider of the timed tile, exists if different from "provider" in 48 | // "QuadtreeImageryDatedTile". 49 | optional int32 provider = 3; 50 | }; 51 | 52 | // Layer-specific info for Time Machine dates. 53 | // One entry for each dated tile available. 54 | // Date in JpegCommentDate::YearMonthDayKey format. 55 | // dated_tile_epoch is the version of the Time Machine tile. 56 | // If a tile can be shared with keyhole-hires, its date is given in 57 | // shared_tile_date. 58 | // The timed_tiles field contains additional tiles with finer time resolution 59 | // than one day. The timed_tiles will be sorted in ascending order. 60 | // Note that a "one day" tile is still required for older 61 | // clients which won't see the timed_tiles field. 62 | message QuadtreeImageryDatedTile { 63 | required int32 date = 1; 64 | required int32 dated_tile_epoch = 2; 65 | required int32 provider = 3; 66 | repeated QuadtreeImageryTimedTile timed_tiles = 4; 67 | }; 68 | 69 | message QuadtreeImageryDates { 70 | repeated QuadtreeImageryDatedTile dated_tile = 1; 71 | optional int32 shared_tile_date = 2; // date of tile shared from keyhole 72 | // Dates of tiles from coarser levels with assets that are visible in this 73 | // tile. Date is in JpegCommentDate::YearMonthDayKey format as an int. 74 | // These will be sorted in ascending order. 75 | // The dates here do not intersect the dates of tiles in dated_tile, but are 76 | // complement to them. "coarse_tile_dates" belong to tiles from upper levels 77 | // with coarser resolution and are not visible at current level. They exist 78 | // here to make user aware of different layers at upper level. 79 | repeated int32 coarse_tile_dates = 3; 80 | // Time (milliseconds from midnight) of tile shared from keyhole. 81 | optional int32 shared_tile_milliseconds = 4; 82 | }; 83 | 84 | message QuadtreeLayer { 85 | enum LayerType { 86 | LAYER_TYPE_IMAGERY = 0; 87 | LAYER_TYPE_TERRAIN = 1; 88 | LAYER_TYPE_VECTOR = 2; 89 | LAYER_TYPE_IMAGERY_HISTORY = 3; 90 | } 91 | 92 | required LayerType type = 1; // type of this layer 93 | required int32 layer_epoch = 2; // epoch of this layer 94 | optional int32 provider = 3; // provider id for this layer 95 | 96 | // If there is additional information for a layer type, add it here: 97 | optional QuadtreeImageryDates dates_layer = 4; 98 | }; 99 | 100 | message QuadtreeNode { 101 | // A bitfield of flags for this node. 102 | // 0-3: presence bits for each interior child node. 103 | // 4: for leaf nodes, this indicates other quadtree sets are below this node 104 | // 5: vector data present in this node. 105 | // 6: imagery data present in this node. 106 | // 7: terrain data present in this node. 107 | enum NodeFlags { 108 | option allow_alias = true; // 允许多个枚举值共享相同的数值, 不然会报错 109 | NODE_FLAGS_CHILD_COUNT = 4; // interior node indication of child presence 110 | NODE_FLAGS_CACHE_BIT = 4; // there's data below leaf nodes 111 | NODE_FLAGS_DRAWABLE_BIT = 5; // there's vector data in this node 112 | NODE_FLAGS_IMAGE_BIT = 6; // there's image data in this node 113 | NODE_FLAGS_TERRAIN_BIT = 7; // there's terrain data in this node 114 | } 115 | 116 | optional int32 flags = 1; 117 | 118 | // The epoch when this node was generated. 119 | // Currently, all the nodes in a packet are generated at the same time, 120 | // so each node has the same version. This may change in the future if 121 | // we only generate the changed nodes. The client uses this to keep its 122 | // node cache fresh. 123 | optional int32 cache_node_epoch = 2; 124 | 125 | // The layer data for this node. 126 | // Layers: image, vector, terrain, etc. 127 | repeated QuadtreeLayer layer = 3; 128 | 129 | // The channel info for this node. 130 | // Channels are components of the vector layer and come 131 | // from the Fusion pipeline. 132 | repeated QuadtreeChannel channel = 4; 133 | }; 134 | 135 | // A full quadtree packet. This contains a subtree of the full global 136 | // quadtree. 137 | message QuadtreePacket { 138 | // Epoch of when this quadtree packet was generated. 139 | required int32 packet_epoch = 1; 140 | 141 | // Nodes have two numbering schemes: 142 | // 143 | // 1) "Subindex". This numbering starts at the top of the tree 144 | // and goes left-to-right across each level, like this: 145 | // 146 | // 0 147 | // / \ . 148 | // 1 86 171 256 149 | // / \ . 150 | // 2 3 4 5 ... 151 | // / \ . 152 | // 6 7 8 9 ... 153 | // 154 | // Notice that the second row is weird in that it's not left-to-right 155 | // order. HOWEVER, the root node in Keyhole is special in that it 156 | // doesn't have this weird ordering. It looks like this: 157 | // 158 | // 0 159 | // / \ . 160 | // 1 2 3 4 161 | // / \ . 162 | // 5 6 7 8 ... 163 | // / \ . 164 | // 21 22 23 24 ... 165 | // 166 | // The mangling of the second row is controlled by a parameter to the 167 | // constructor for TreeNumbering. 168 | // 169 | // 2) "Inorder". The order of this node in an inorder traversal. 170 | // 171 | // Clients make requests for nodes using subindex order. 172 | // See googleclient/geo/earth_enterprise/src/common/qtpacket/tree_utils.cpp 173 | // for more details. 174 | 175 | // All the quadtree nodes in this packet, with subindex indices. 176 | repeated group SparseQuadtreeNode = 2 { 177 | // Index of this node within the quadtree packet. The nodes within 178 | // a quadtree packet are numbered in subindex order and are sparse. 179 | // Note: the nodes are not currently guaranteed to be sorted 180 | // in any particular order. 181 | required int32 index = 3; // the subindex index 182 | required QuadtreeNode Node = 4; // the node itself 183 | } 184 | }; 185 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@deno-library/compress@*": "0.5.5", 5 | "jsr:@deno-library/crc32@1.0.2": "1.0.2", 6 | "jsr:@es-toolkit/es-toolkit@^1.38.0": "1.38.0", 7 | "jsr:@liuxspro/capgen@~0.2.1": "0.2.1", 8 | "jsr:@liuxspro/utils@*": "0.1.8", 9 | "jsr:@oak/commons@1": "1.0.0", 10 | "jsr:@oak/oak@*": "17.1.3", 11 | "jsr:@std/assert@1": "1.0.9", 12 | "jsr:@std/bytes@1": "1.0.4", 13 | "jsr:@std/bytes@^1.0.2": "1.0.4", 14 | "jsr:@std/crypto@1": "1.0.3", 15 | "jsr:@std/encoding@1": "1.0.5", 16 | "jsr:@std/encoding@^1.0.5": "1.0.5", 17 | "jsr:@std/fs@1.0.5": "1.0.5", 18 | "jsr:@std/http@1": "1.0.12", 19 | "jsr:@std/internal@^1.0.5": "1.0.5", 20 | "jsr:@std/io@0.224": "0.224.9", 21 | "jsr:@std/io@0.225.0": "0.225.0", 22 | "jsr:@std/media-types@1": "1.1.0", 23 | "jsr:@std/path@*": "1.0.8", 24 | "jsr:@std/path@1": "1.0.8", 25 | "jsr:@std/path@1.0.8": "1.0.8", 26 | "jsr:@std/path@^1.0.7": "1.0.8", 27 | "jsr:@std/streams@^1.0.7": "1.0.8", 28 | "jsr:@std/tar@0.1.3": "0.1.3", 29 | "jsr:@zip-js/zip-js@2.7.53": "2.7.53", 30 | "npm:@types/node@*": "22.10.1", 31 | "npm:minijinja-js@^2.9.0": "2.9.0", 32 | "npm:path-to-regexp@6.2.1": "6.2.1", 33 | "npm:protobufjs@*": "7.4.0", 34 | "npm:protobufjs@^7.4.0": "7.4.0" 35 | }, 36 | "jsr": { 37 | "@deno-library/compress@0.5.5": { 38 | "integrity": "18b651a33eac87d96ae8c941487045724a665d654e9d94120da43777393655d9", 39 | "dependencies": [ 40 | "jsr:@deno-library/crc32", 41 | "jsr:@std/fs", 42 | "jsr:@std/io@0.225.0", 43 | "jsr:@std/path@1.0.8", 44 | "jsr:@std/tar", 45 | "jsr:@zip-js/zip-js" 46 | ] 47 | }, 48 | "@deno-library/crc32@1.0.2": { 49 | "integrity": "d2061bfee30c87c97f285dfca0fdc4458e632dc072a33ecfc73ca5177a5a39a0" 50 | }, 51 | "@es-toolkit/es-toolkit@1.38.0": { 52 | "integrity": "7a3fa3bbe873116ec37ed68ef1df6b8ec3d89bac56edd354b7d12ff8cc0d5a0a" 53 | }, 54 | "@liuxspro/capgen@0.2.1": { 55 | "integrity": "d01bb326c682a73e3aaec0fb76259015d5d6e7783e2cb02673fafaf002deeda6", 56 | "dependencies": [ 57 | "jsr:@es-toolkit/es-toolkit", 58 | "npm:minijinja-js" 59 | ] 60 | }, 61 | "@liuxspro/utils@0.1.8": { 62 | "integrity": "36ce86f95bca1e1f75346f4ddf31ecb48d5b05408df7d7c6b92a40451d062830" 63 | }, 64 | "@oak/commons@1.0.0": { 65 | "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", 66 | "dependencies": [ 67 | "jsr:@std/assert", 68 | "jsr:@std/bytes@1", 69 | "jsr:@std/crypto", 70 | "jsr:@std/encoding@1", 71 | "jsr:@std/http", 72 | "jsr:@std/media-types" 73 | ] 74 | }, 75 | "@oak/oak@17.1.3": { 76 | "integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33", 77 | "dependencies": [ 78 | "jsr:@oak/commons", 79 | "jsr:@std/assert", 80 | "jsr:@std/bytes@1", 81 | "jsr:@std/crypto", 82 | "jsr:@std/http", 83 | "jsr:@std/io@0.224", 84 | "jsr:@std/media-types", 85 | "jsr:@std/path@1", 86 | "npm:path-to-regexp" 87 | ] 88 | }, 89 | "@std/assert@1.0.9": { 90 | "integrity": "a9f0c611a869cc791b26f523eec54c7e187aab7932c2c8e8bea0622d13680dcd", 91 | "dependencies": [ 92 | "jsr:@std/internal" 93 | ] 94 | }, 95 | "@std/bytes@1.0.4": { 96 | "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" 97 | }, 98 | "@std/crypto@1.0.3": { 99 | "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" 100 | }, 101 | "@std/encoding@1.0.5": { 102 | "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" 103 | }, 104 | "@std/fs@1.0.5": { 105 | "integrity": "41806ad6823d0b5f275f9849a2640d87e4ef67c51ee1b8fb02426f55e02fd44e", 106 | "dependencies": [ 107 | "jsr:@std/path@^1.0.7" 108 | ] 109 | }, 110 | "@std/http@1.0.12": { 111 | "integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa", 112 | "dependencies": [ 113 | "jsr:@std/encoding@^1.0.5" 114 | ] 115 | }, 116 | "@std/internal@1.0.5": { 117 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 118 | }, 119 | "@std/io@0.224.9": { 120 | "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3", 121 | "dependencies": [ 122 | "jsr:@std/bytes@^1.0.2" 123 | ] 124 | }, 125 | "@std/io@0.225.0": { 126 | "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3", 127 | "dependencies": [ 128 | "jsr:@std/bytes@^1.0.2" 129 | ] 130 | }, 131 | "@std/media-types@1.1.0": { 132 | "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" 133 | }, 134 | "@std/path@1.0.8": { 135 | "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" 136 | }, 137 | "@std/streams@1.0.8": { 138 | "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3" 139 | }, 140 | "@std/tar@0.1.3": { 141 | "integrity": "531270fc707b37ab9b5f051aa4943e7b16b86905e0398a4ebe062983b0c93115", 142 | "dependencies": [ 143 | "jsr:@std/streams" 144 | ] 145 | }, 146 | "@zip-js/zip-js@2.7.53": { 147 | "integrity": "acea5bd8e01feb3fe4c242cfbde7d33dd5e006549a4eb1d15283bc0c778ed672" 148 | } 149 | }, 150 | "npm": { 151 | "@protobufjs/aspromise@1.1.2": { 152 | "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" 153 | }, 154 | "@protobufjs/base64@1.1.2": { 155 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 156 | }, 157 | "@protobufjs/codegen@2.0.4": { 158 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 159 | }, 160 | "@protobufjs/eventemitter@1.1.0": { 161 | "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" 162 | }, 163 | "@protobufjs/fetch@1.1.0": { 164 | "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 165 | "dependencies": [ 166 | "@protobufjs/aspromise", 167 | "@protobufjs/inquire" 168 | ] 169 | }, 170 | "@protobufjs/float@1.0.2": { 171 | "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" 172 | }, 173 | "@protobufjs/inquire@1.1.0": { 174 | "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" 175 | }, 176 | "@protobufjs/path@1.1.2": { 177 | "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" 178 | }, 179 | "@protobufjs/pool@1.1.0": { 180 | "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" 181 | }, 182 | "@protobufjs/utf8@1.1.0": { 183 | "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" 184 | }, 185 | "@types/node@22.10.1": { 186 | "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", 187 | "dependencies": [ 188 | "undici-types" 189 | ] 190 | }, 191 | "long@5.2.4": { 192 | "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==" 193 | }, 194 | "minijinja-js@2.9.0": { 195 | "integrity": "sha512-NDrwVXH5c+VfA41LrmINjlKUY+udSUAVSRjafyu0SiRkKJbczceR7DJK17UToZ59zc61dEroH78tesEYDLmegQ==" 196 | }, 197 | "path-to-regexp@6.2.1": { 198 | "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" 199 | }, 200 | "protobufjs@7.4.0": { 201 | "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", 202 | "dependencies": [ 203 | "@protobufjs/aspromise", 204 | "@protobufjs/base64", 205 | "@protobufjs/codegen", 206 | "@protobufjs/eventemitter", 207 | "@protobufjs/fetch", 208 | "@protobufjs/float", 209 | "@protobufjs/inquire", 210 | "@protobufjs/path", 211 | "@protobufjs/pool", 212 | "@protobufjs/utf8", 213 | "@types/node", 214 | "long" 215 | ] 216 | }, 217 | "undici-types@6.20.0": { 218 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" 219 | } 220 | }, 221 | "workspace": { 222 | "dependencies": [ 223 | "jsr:@liuxspro/capgen@~0.2.1", 224 | "jsr:@std/assert@1", 225 | "npm:protobufjs@^7.4.0" 226 | ] 227 | } 228 | } 229 | --------------------------------------------------------------------------------