From 2fbe997f85c6174934756c0335845b1a66493c59 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 28 Oct 2026 22:18:08 -0700 Subject: [PATCH 1/2] Move react-vectortile out, as a submodule It goes to its own repository (MapConductor/react-vectortile, split from here with its history) and returns as a submodule, like js-vectorstyle. - scripts/fetch-vector-assets.mjs and scripts/vector-assets.json move into the package, which fetches its own renderer from the same pinned release; nothing else here used them - .gitignore: the package ignores its own vendor/ --- .gitignore | 5 - .gitmodules | 3 + react-vectortile | 1 + react-vectortile/LICENSE | 201 -------- react-vectortile/README.md | 79 --- .../api/react-vectortile.api.d.ts | 124 ----- react-vectortile/package.json | 52 -- react-vectortile/src/VectorTileLayer.tsx | 485 ------------------ react-vectortile/src/VectorTileRasteriser.ts | 290 ----------- react-vectortile/src/index.ts | 4 - react-vectortile/tsconfig.json | 16 - react-vectortile/tsup.config.ts | 13 - scripts/fetch-vector-assets.mjs | 90 ---- scripts/vector-assets.json | 7 - 14 files changed, 4 insertions(+), 1366 deletions(-) create mode 160000 react-vectortile delete mode 100644 react-vectortile/LICENSE delete mode 100644 react-vectortile/README.md delete mode 100644 react-vectortile/api/react-vectortile.api.d.ts delete mode 100644 react-vectortile/package.json delete mode 100644 react-vectortile/src/VectorTileLayer.tsx delete mode 100644 react-vectortile/src/VectorTileRasteriser.ts delete mode 100644 react-vectortile/src/index.ts delete mode 100644 react-vectortile/tsconfig.json delete mode 100644 react-vectortile/tsup.config.ts delete mode 100644 scripts/fetch-vector-assets.mjs delete mode 100644 scripts/vector-assets.json diff --git a/.gitignore b/.gitignore index 70bea5a..97d3ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -81,8 +81,3 @@ examples/reactnative-basic/android/.kotlin/ newkey.xcresult ios-rest.xcresult ios-final.xcresult - -# The Rust style compiler and renderer (wasm + their JS bindings), fetched from a -# MapConductor/vectortile release by scripts/fetch-vector-assets.mjs. The tag and -# checksums are pinned in scripts/vector-assets.json; the bytes are not tracked. -react-vectortile/vendor/ diff --git a/.gitmodules b/.gitmodules index d249114..9d9f135 100644 --- a/.gitmodules +++ b/.gitmodules @@ -97,3 +97,6 @@ [submodule "js-vectorstyle"] path = js-vectorstyle url = git@github.com:mapconductor/js-vectorstyle.git +[submodule "react-vectortile"] + path = react-vectortile + url = git@github.com:MapConductor/react-vectortile.git diff --git a/react-vectortile b/react-vectortile new file mode 160000 index 0000000..1ca1346 --- /dev/null +++ b/react-vectortile @@ -0,0 +1 @@ +Subproject commit 1ca1346eb1332f2c65bb715bcc4dc8b15d7e6f80 diff --git a/react-vectortile/LICENSE b/react-vectortile/LICENSE deleted file mode 100644 index 4438287..0000000 --- a/react-vectortile/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2026 Masashi Katsumata - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/react-vectortile/README.md b/react-vectortile/README.md deleted file mode 100644 index 4b58e97..0000000 --- a/react-vectortile/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# @mapconductor/react-vectortile - -Renders a MapLibre vector style as raster tiles, so any MapConductor backend can -display one — including the several that cannot read a vector style at all -(Google Maps, MapKit, HERE, ArcGIS, Cesium). - -This package is a thin React component. The renderer itself is a Rust/wasm -build from [MapConductor/vectortile](https://github.com/MapConductor/vectortile), -shipped inside this package under `vendor/` (fetched from a pinned release by -`scripts/fetch-vector-assets.mjs` when the package is built). - -```tsx -import { VectorTileLayer } from '@mapconductor/react-vectortile'; - - - console.warn(messages)} - /> -; -``` - -## Props - -| prop | default | | -| --- | --- | --- | -| `style` | — | A `style.json` URL, its raw text, or the parsed object. | -| `tileSize` | `512` | Output tile size in pixels. | -| `opacity` | `1` | | -| `visible` | `true` | | -| `maxZoom` | `22` | | -| `headers` | — | Sent with every source tile request — auth tokens, API keys. | -| `onDiagnostics` | — | Reasons the style may not render as intended. | - -## Restyling - -Passing a new `style` recolours in place: the vector tiles already fetched are -reused and only the rasterisation is redone, so a palette change costs no -network traffic. `examples/basic` has a page that does this with colour pickers. - -`style` is compared by content, not object identity, so building it inline — -which is what a colour picker does — is safe. - -## Requirements - -The tile service worker must be reachable at `/tile-sw.js`, the same requirement -`@mapconductor/react-heatmap` has. `VectorTileLayer` registers it and waits for -it to control the page before requesting a tile. Service workers need a secure -context: `localhost` over plain HTTP is fine, but LAN access needs HTTPS. - -## How it draws - -Two raster layers are mounted, not one: - -- the **ground** — fills, lines and circles, the half no font arriving can change -- a transparent **label overlay** above it, replaced on its own when glyph ranges land - -To the map and the user they read as one map. The halves render in parallel, and -a glyph range landing redraws only the transparent one, so a label appearing -never blanks the map beneath it. Replacing a raster layer's URL is -remove-then-add, so the replacement is mounted alongside its predecessor and the -old one dropped a moment later — a handover rather than a gap. - -Attribution is handled for you: the credits the style's sources ask for are -attached to both layers, so they appear in the map's attribution overlay without -the host writing any UI and disappear when the layer unmounts. A basemap drawing -OpenStreetMap requires the credit, so this is not cosmetic. - -## Limits - -`onDiagnostics` reports layer types the renderer will not draw, sources it -cannot fetch, and Mapbox `imports` — anything about the style that will not -render as intended. The failure mode that matters is a blank tile, so it is -worth surfacing. - -## License - -Apache License 2.0. See [LICENSE](./LICENSE). diff --git a/react-vectortile/api/react-vectortile.api.d.ts b/react-vectortile/api/react-vectortile.api.d.ts deleted file mode 100644 index c9583bc..0000000 --- a/react-vectortile/api/react-vectortile.api.d.ts +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import { StyleRules, VectorStyleRasteriser, StyleAffects, VectorStyleRasterisation } from '@mapconductor/js-vectorstyle'; -import { MapStyleHost } from '@mapconductor/js-sdk-core'; - -interface VectorTileLayerProps { - /** A `style.json` URL, its raw text, or the parsed object. */ - style: string | object; - /** - * Adjustments applied to the style before anything is drawn -- the same - * rules `VectorStyle` takes, so "roads are white" or "no buildings" - * reads the same wherever it is written. - * - * A rule that hides a role is the reason to reach for this rather than - * editing the document: the layers are switched off in the document the - * renderer is given, so nothing about them is drawn. - * - * ```tsx - * rules={buildStyleRules((r) => { - * r.role(LayerRole.building, { visible: false }); - * r.role(LayerRole.road, { visible: false }); - * })} - * ``` - * - * Left out, the style is drawn as its author wrote it and the compiler's - * wasm is never fetched. - */ - rules?: StyleRules; - /** - * Output tile size in pixels. Left out, the backend decides: a provider - * that has registered a `RasterTilePreference` is answered, and anything - * else gets 512. - * - * Bigger tiles are cheaper for the same screen -- the per-tile costs scale - * with the count -- but ArcGIS's 3D SceneView picks the level as though - * every tile were 256 pixels, so handing it 512 makes it fetch one level - * deeper and four times as many tiles. - */ - tileSize?: number; - opacity?: number; - visible?: boolean; - maxZoom?: number; - /** Headers sent with every source tile request — auth tokens, API keys. */ - headers?: Record; - /** - * Reasons the style may not render as intended: undrawn layer types, - * sources that cannot be fetched, Mapbox `imports`. Worth surfacing — the - * failure mode that matters is a blank tile. - */ - onDiagnostics?: (messages: string[]) => void; - /** - * The style is the basemap, not a layer over one. - * - * On a map that renders vector styles itself (MapLibre, Mapbox, MapTiler - * -- anything registering `VectorStyleSupportKey`) this hands the style - * over directly and mounts no raster layer at all: the fast path, and the - * one an offline package will take. The style then *replaces* the map's - * design and `opacity` does not apply. Elsewhere it changes nothing here; - * the app blanks the map's own basemap (a `None` design) and the opaque - * raster tiles are the map. - */ - asBasemap?: boolean; -} -/** - * Draws a MapLibre vector style on any map backend, by rendering it to raster - * tiles in the browser and serving them through the SDK's local tile server. - * - * The backend only ever sees ordinary raster layers, which is what makes this - * work on Google Maps, MapKit, Cesium, OpenLayers and the rest — none of which - * can render a vector style themselves. - * - * Rendering runs in a Web Worker, so a viewport's worth of tiles does not stall - * the map. - * - * Two layers are mounted, not one: the ground (fills, lines, circles) and a - * transparent label overlay above it. To the map and the user they read as one - * map. The halves render in parallel, and a glyph range landing redraws only - * the transparent one, so a label appearing never blanks the map beneath it. - * - * Changing `style` recolours in place rather than rebuilding: the vector tiles - * already fetched are reused, so only the rasterisation is redone. - * - * ```tsx - * - * ``` - */ -declare function VectorTileLayer({ style, rules, tileSize: requestedTileSize, opacity, visible, maxZoom, headers, onDiagnostics, asBasemap, }: VectorTileLayerProps): React.ReactElement | null; - -interface VectorTileRasteriserOptions { - /** Edge of a rendered tile in CSS pixels. The map's own preference wins. */ - readonly tileSize?: number; - /** Sent with every source-tile, sprite and glyph request. */ - readonly headers?: Record; - /** Where the renderer's `.wasm` is served from, if not the default. */ - readonly wasmUrl?: string | URL; - /** Path the tile service worker is registered from. */ - readonly serviceWorkerPath?: string; -} -/** - * Draws a vector style for a map that cannot read one. - * - * ```tsx - * - * ``` - * - * Google Maps, MapKit, HERE, ArcGIS, Leaflet, OpenLayers and the rest have - * no vector renderer. This renders the same compiled document to tiles in - * the browser and mounts them as the basemap, so the adjustments an app - * writes once produce the same map on every backend. MapLibre, Mapbox and - * MapTiler never call it — they are handed the document itself. - * - * ios-vectortile の `VectorTileRasteriser`、android-vectortile の同名クラスと - * 対応する。世代の付け方、地と文字の分け方、restyle のまとめ方まで揃えてある。 - */ -declare class VectorTileRasteriser implements VectorStyleRasteriser { - private readonly options; - constructor(options?: VectorTileRasteriserOptions); - install(host: MapStyleHost, styleJson: string, _affects: StyleAffects): VectorStyleRasterisation; -} - -export { VectorTileLayer, VectorTileRasteriser }; -export type { VectorTileLayerProps, VectorTileRasteriserOptions }; diff --git a/react-vectortile/package.json b/react-vectortile/package.json deleted file mode 100644 index 496f98f..0000000 --- a/react-vectortile/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@mapconductor/react-vectortile", - "version": "0.2.1", - "description": "Render MapLibre vector styles as raster tiles on any MapConductor backend", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./worker": "./vendor/vectortile/dist/worker.js" - }, - "files": [ - "dist", - "vendor", - "README.md" - ], - "scripts": { - "prebuild": "node ../scripts/fetch-vector-assets.mjs", - "build": "tsup", - "predev": "node ../scripts/fetch-vector-assets.mjs", - "dev": "tsup --watch", - "clean": "rm -rf dist", - "test": "echo 'covered by the harness in mapconductor-vectortile'" - }, - "keywords": [ - "maps", - "vector-tiles", - "maplibre", - "react", - "typescript" - ], - "author": "MapConductor", - "license": "Apache-2.0", - "dependencies": { - "@mapconductor/js-sdk-core": "^0.2.0", - "@mapconductor/js-sdk-react": "^0.2.1", - "@mapconductor/js-vectorstyle": "^0.1.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - }, - "devDependencies": { - "@types/react": "^19.3.0", - "react": "^19.3.1", - "tsup": "^9.0.0", - "typescript": "^5.9.3" - }, - "type": "module" -} diff --git a/react-vectortile/src/VectorTileLayer.tsx b/react-vectortile/src/VectorTileLayer.tsx deleted file mode 100644 index f89cc6d..0000000 --- a/react-vectortile/src/VectorTileLayer.tsx +++ /dev/null @@ -1,485 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - TileServerRegistry, - createRasterLayerState, - TileScheme, - RasterTilePreferenceKey, - VectorStyleSupportKey, - type AttributionRule, - type RasterLayerState, -} from '@mapconductor/js-sdk-core'; -import { RasterLayer, useMapServiceRegistry } from '@mapconductor/js-sdk-react'; -import { VectorTileProvider } from '../vendor/vectortile/dist/index.js'; -import { NO_STYLE_RULES, type StyleRules } from '@mapconductor/js-vectorstyle'; - -export interface VectorTileLayerProps { - /** A `style.json` URL, its raw text, or the parsed object. */ - style: string | object; - /** - * Adjustments applied to the style before anything is drawn -- the same - * rules `VectorStyle` takes, so "roads are white" or "no buildings" - * reads the same wherever it is written. - * - * A rule that hides a role is the reason to reach for this rather than - * editing the document: the layers are switched off in the document the - * renderer is given, so nothing about them is drawn. - * - * ```tsx - * rules={buildStyleRules((r) => { - * r.role(LayerRole.building, { visible: false }); - * r.role(LayerRole.road, { visible: false }); - * })} - * ``` - * - * Left out, the style is drawn as its author wrote it and the compiler's - * wasm is never fetched. - */ - rules?: StyleRules; - /** - * Output tile size in pixels. Left out, the backend decides: a provider - * that has registered a `RasterTilePreference` is answered, and anything - * else gets 512. - * - * Bigger tiles are cheaper for the same screen -- the per-tile costs scale - * with the count -- but ArcGIS's 3D SceneView picks the level as though - * every tile were 256 pixels, so handing it 512 makes it fetch one level - * deeper and four times as many tiles. - */ - tileSize?: number; - opacity?: number; - visible?: boolean; - maxZoom?: number; - /** Headers sent with every source tile request — auth tokens, API keys. */ - headers?: Record; - /** - * Reasons the style may not render as intended: undrawn layer types, - * sources that cannot be fetched, Mapbox `imports`. Worth surfacing — the - * failure mode that matters is a blank tile. - */ - onDiagnostics?: (messages: string[]) => void; - /** - * The style is the basemap, not a layer over one. - * - * On a map that renders vector styles itself (MapLibre, Mapbox, MapTiler - * -- anything registering `VectorStyleSupportKey`) this hands the style - * over directly and mounts no raster layer at all: the fast path, and the - * one an offline package will take. The style then *replaces* the map's - * design and `opacity` does not apply. Elsewhere it changes nothing here; - * the app blanks the map's own basemap (a `None` design) and the opaque - * raster tiles are the map. - */ - asBasemap?: boolean; -} - -/** - * The `attribution` of every source in a style object, without duplicates. - * A URL string cannot be read here; the map credits those sources itself. - */ -function styleAttributions(style: string | object): AttributionRule[] { - if (typeof style === 'string') return []; - const sources = (style as { sources?: Record }).sources; - if (!sources) return []; - const seen = new Set(); - for (const source of Object.values(sources)) { - const attribution = source?.attribution; - if (typeof attribution === 'string' && attribution.trim() !== '') seen.add(attribution); - } - return [...seen].map((attribution) => ({ attribution })); -} - -/** - * The style as text, fetched when it is a URL. - * - * Only `rules` needs this: without them the document is handed to the - * renderer untouched and it does its own fetching. The rule for telling a - * URL from a document is the renderer's, so both agree. - */ -async function styleText(style: string | object): Promise { - if (typeof style === 'object') return JSON.stringify(style); - if (/^\s*\{/.test(style)) return style; - const response = await fetch(style); - if (!response.ok) throw new Error(`style fetch failed: ${response.status} ${style}`); - return response.text(); -} - -/** - * How long a layer being replaced stays up. - * - * Replacing a raster layer's source URL is implemented as remove-then-add, so - * the layer vanishes for as long as the new source takes to fetch its first - * tiles. Mounting the replacement alongside and dropping the old one a moment - * later hands over instead. No signal says a raster source has drawn, so this - * is a window rather than a wait: too short brings the flash back, too long - * leaves two layers stacked, which costs a moment of overdraw and nothing else. - * - * Matches `HANDOVER_MS` in android-vectortile. - */ -const HANDOVER_MS = 900; - -const clamp = (value: number): number => Math.min(1, Math.max(0, value)); - -/** - * Draws a MapLibre vector style on any map backend, by rendering it to raster - * tiles in the browser and serving them through the SDK's local tile server. - * - * The backend only ever sees ordinary raster layers, which is what makes this - * work on Google Maps, MapKit, Cesium, OpenLayers and the rest — none of which - * can render a vector style themselves. - * - * Rendering runs in a Web Worker, so a viewport's worth of tiles does not stall - * the map. - * - * Two layers are mounted, not one: the ground (fills, lines, circles) and a - * transparent label overlay above it. To the map and the user they read as one - * map. The halves render in parallel, and a glyph range landing redraws only - * the transparent one, so a label appearing never blanks the map beneath it. - * - * Changing `style` recolours in place rather than rebuilding: the vector tiles - * already fetched are reused, so only the rasterisation is redone. - * - * ```tsx - * - * ``` - */ -export function VectorTileLayer({ - style, - rules, - tileSize: requestedTileSize, - opacity = 1, - visible = true, - maxZoom = 22, - headers, - onDiagnostics, - asBasemap = false, -}: VectorTileLayerProps): React.ReactElement | null { - const routeId = useMemo( - () => `vectortile-${Math.random().toString(36).slice(2, 10)}`, - [], - ); - // Two routes from one provider. The user sees one map; the halves are - // served, fetched and cached independently. - const groundRoute = `${routeId}-ground`; - const labelRoute = `${routeId}-labels`; - - // タイルの一辺はアプリの好みで決めてよく、既定は 512(1 枚あたりの固定費が - // 枚数に比例するので、同じ画面なら大きいタイルのほうが安い)。ただし地図 - // SDK 側に都合があることがある: ArcGIS の 3D SceneView は「タイルは 256px」 - // という前提でレベルを選び、512 を渡すと 1 段深いレベルを 4 倍の枚数で引く。 - // 宣言しているプロバイダにはそれに従い、明示された値は常に優先する。 - const registry = useMapServiceRegistry(); - const preference = registry.get(RasterTilePreferenceKey); - /** CSS px one tile covers on screen; what the style is read at. */ - const tileSize = requestedTileSize ?? preference?.preferredTileSize ?? 512; - /** Pixels drawn per tile. */ - const pixelTileSize = Math.round(tileSize * devicePixelRatioClamped()); - /** - * What the map is told. A map that lays tiles out in device px needs the - * pixel count or it asks for a zoom one level too deep; one that lays - * them out in CSS px needs the CSS size or everything doubles. Only the - * provider knows which it is, so it says, through the preference. - */ - const declaredTileSize = preference?.layoutInDevicePixels ? pixelTileSize : tileSize; - - // The direct path: the map draws the style itself and nothing below - // mounts. Read on every render because a provider registers as it - // initialises, and the ready re-render is when it first shows up. - const direct = asBasemap ? registry.get(VectorStyleSupportKey) : undefined; - - const tileServer = useMemo(() => TileServerRegistry.get(), []); - const providerRef = useRef(null); - const [ready, setReady] = useState(false); - - /** - * The credits the style's sources ask for, carried by the layers themselves. - * - * The map's attribution overlay is fed from the rules of every visible - * raster layer, so a credit attached here appears without the host writing - * any UI and disappears when the layer unmounts. Nothing else about this - * layer can be wrong while still looking right: the tiles draw perfectly - * whether or not anyone is credited for them. - * - * No zoom or bounds narrowing on the rules. A source's `maxzoom` is where - * its tiles stop, not where its data stops being on screen — past it the - * renderer magnifies the deepest tile it has, so the data is still shown - * and still has to be credited. Crediting a little too often costs a line - * of text; crediting too rarely breaks a licence. - */ - const [credits, setCredits] = useState([]); - - // Restyling keys off content, not object identity. A caller that builds its - // style inline — which is exactly what a colour picker does — produces a new - // object every render, and keying off identity would rerender the whole map - // on every unrelated state change. - const styleKey = useMemo( - () => (typeof style === 'string' ? style : JSON.stringify(style)), - [style], - ); - const rulesJson = rules?.json ?? NO_STYLE_RULES.json; - /** - * What identifies the document actually drawn. The rules belong in it: - * the same style with the buildings switched off is a different map, and - * everything below that reacts to a restyle keys off this. - */ - const drawnKey = `${styleKey}\u0000${rulesJson}`; - // Read inside effects that must not re-run when the style changes. - const styleRef = useRef(style); - styleRef.current = style; - const rulesRef = useRef(rulesJson); - rulesRef.current = rulesJson; - - /** - * The document as it will be drawn: the author's style with `rules` - * applied to it. - * - * Applied to the document rather than sent as per-layer deltas, because - * both halves of this layer -- the rasteriser and the handover to a map - * that draws styles itself -- take a document. Without rules this - * returns the author's style untouched and the compiler's wasm is never - * loaded. - */ - const drawnStyle = useCallback( - async (report?: (messages: string[]) => void): Promise => { - const document = styleRef.current; - const json = rulesRef.current; - if (json === NO_STYLE_RULES.json) return document; - const { compileStyle } = await import('@mapconductor/js-vectorstyle'); - const compiled = await compileStyle(await styleText(document), json); - if (compiled.diagnostics.length > 0) report?.([...compiled.diagnostics]); - return compiled.style as object; - }, - [], - ); - - // Bumped on every restyle and threaded into the tile URL. Without it the - // new template is byte-identical to the old one, so the map serves the - // raster tiles it already holds and nothing appears to change. - const [styleVersion, setStyleVersion] = useState(0); - - // Bumped when glyphs land. The map only refetches a raster source whose URL - // changed, so the generation has to reach the template. - const [glyphGeneration, setGlyphGeneration] = useState(0); - - // Normally one each, briefly two while a replacement takes over. - const [grounds, setGrounds] = useState([]); - const [labels, setLabels] = useState([]); - - // Applied at creation so a layer never mounts at the wrong opacity, then - // pushed onto the live states by the effect at the bottom. - const opacityRef = useRef(opacity); - opacityRef.current = opacity; - const visibleRef = useRef(visible); - visibleRef.current = visible; - - // Strictly increasing, so a replacement always stacks above what it - // replaces. Restyles and glyph arrivals both draw from it. - const labelDepth = useRef(0); - - // The service worker has to be controlling the page before any tile request - // is made, otherwise the first requests 404 before the route exists. - useEffect(() => { - if (direct) return; - let cancelled = false; - - (async () => { - tileServer.startServiceWorker('/tile-sw.js'); - await tileServer.waitForController(); - - const provider = await VectorTileProvider.create({ - style: await drawnStyle(onDiagnostics), - tileSize: pixelTileSize, - displayTileSize: tileSize, - cacheBytes: 48 * 1024 * 1024, - persistSourceTiles: true, - persistRenderedTiles: true, - headers, - onWarning: (message: string) => onDiagnostics?.([message]), - }); - if (cancelled) { - provider.dispose(); - return; - } - providerRef.current = provider; - - // Glyphs arrive after the tiles that need them: a range is a round - // trip and a tile at low zoom wants dozens, so tiles are drawn with - // whatever is loaded and the overlay is replaced once more arrives. - provider.onGlyphsLoaded = (generation: number) => { - setGlyphGeneration(generation); - }; - - tileServer.register(groundRoute, provider.groundTiles); - tileServer.register(labelRoute, provider.labelTiles); - - if (provider.diagnostics.length > 0) onDiagnostics?.(provider.diagnostics); - setCredits(provider.attributions.map((attribution) => ({ attribution }))); - setReady(true); - })().catch((error: unknown) => { - onDiagnostics?.([`style could not be loaded: ${String(error)}`]); - }); - - return () => { - cancelled = true; - setReady(false); - setGrounds([]); - setLabels([]); - tileServer.unregister(groundRoute); - tileServer.unregister(labelRoute); - providerRef.current?.dispose(); - providerRef.current = null; - }; - // `style` is handled by the restyle effect below, not by rebuilding. - // `headers` and `onDiagnostics` are deliberately not dependencies: a new - // object identity each render would tear down and rebuild the renderer. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [routeId, tileServer, tileSize, pixelTileSize, direct]); - - // Handed over rather than rasterised. Keyed on the style's content, so a - // recolour is a new handoff; the support cancels the clear a remount - // would otherwise cause. - useEffect(() => { - if (!direct) return; - let cancelled = false; - void drawnStyle(onDiagnostics) - .then((document) => { - if (cancelled) return; - direct.showStyle(document, styleAttributions(document)); - onDiagnostics?.(['direct: the map draws the style itself']); - }) - .catch((error: unknown) => { - onDiagnostics?.([`style could not be applied: ${String(error)}`]); - }); - return () => { - cancelled = true; - direct.clearStyle(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [direct, drawnKey]); - - // Restyle in place. The vector tiles are unchanged — only the paint applied - // to them — so this costs a re-rasterise and no network traffic. - const appliedStyleKey = useRef(null); - useEffect(() => { - const provider = providerRef.current; - if (!provider || !ready) return; - // The boot effect already applied whatever style was current then. - if (appliedStyleKey.current === null) { - appliedStyleKey.current = drawnKey; - return; - } - if (appliedStyleKey.current === drawnKey) return; - appliedStyleKey.current = drawnKey; - - let cancelled = false; - drawnStyle(onDiagnostics) - .then((document) => provider.setStyle(document)) - .then((diagnostics) => { - if (cancelled) return; - if (diagnostics.length > 0) onDiagnostics?.(diagnostics); - setCredits(provider.attributions.map((attribution) => ({ attribution }))); - setStyleVersion((version) => version + 1); - }) - .catch((error: unknown) => { - onDiagnostics?.([`style could not be applied: ${String(error)}`]); - }); - - return () => { - cancelled = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [drawnKey, ready]); - - /** - * The ground: geometry only, and nothing a font does can invalidate it. - * - * Keyed by the style alone. A glyph range landing must not reach this - * layer — that is the whole reason the halves are separate. - */ - useEffect(() => { - if (!ready) return; - const next = createRasterLayerState({ - id: `${groundRoute}-s${styleVersion}`, - source: { - type: 'UrlTemplate', - template: tileServer.urlTemplate({ - routeId: groundRoute, - tileSize: declaredTileSize, - cacheKey: `s${styleVersion}`, - }), - tileSize: declaredTileSize, - maxZoom, - attributionRules: credits, - scheme: TileScheme.XYZ, - }, - opacity: clamp(opacityRef.current), - visible: visibleRef.current, - zIndex: 0, - }); - setGrounds((prev) => [...prev, next]); - const timer = setTimeout(() => setGrounds((prev) => prev.slice(-1)), HANDOVER_MS); - return () => clearTimeout(timer); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ready, styleVersion, credits, tileSize, pixelTileSize, declaredTileSize, maxZoom, groundRoute]); - - /** - * The labels: replaced on a restyle and on every glyph arrival, always - * above the ground and above the generation it takes over from. - */ - useEffect(() => { - if (!ready) return; - labelDepth.current += 1; - const next = createRasterLayerState({ - id: `${labelRoute}-s${styleVersion}-g${glyphGeneration}`, - source: { - type: 'UrlTemplate', - template: tileServer.urlTemplate({ - routeId: labelRoute, - tileSize: declaredTileSize, - cacheKey: `s${styleVersion}-g${glyphGeneration}`, - }), - tileSize: declaredTileSize, - maxZoom, - // Both halves carry the credit: the attribution overlay - // deduplicates, so printing it twice costs nothing, and hiding - // either half cannot silence it. - attributionRules: credits, - scheme: TileScheme.XYZ, - }, - opacity: clamp(opacityRef.current), - visible: visibleRef.current, - zIndex: 1000 + labelDepth.current, - }); - setLabels((prev) => [...prev, next]); - const timer = setTimeout(() => setLabels((prev) => prev.slice(-1)), HANDOVER_MS); - return () => clearTimeout(timer); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ready, styleVersion, glyphGeneration, credits, tileSize, pixelTileSize, declaredTileSize, maxZoom, labelRoute]); - - // Pushed onto the existing states rather than rebuilding them: opacity and - // visibility do not change what a tile contains, so nothing has to refetch. - useEffect(() => { - for (const state of [...grounds, ...labels]) { - state.opacity = clamp(opacity); - state.visible = visible; - } - }, [grounds, labels, opacity, visible]); - - if (direct || grounds.length === 0) return null; - return ( - <> - {/* Ground first: the providers that ignore zIndex stack in mount - order, so the labels have to be added after the ground. */} - {grounds.map((state) => ( - - ))} - {labels.map((state) => ( - - ))} - - ); -} - -/** Device pixels per CSS pixel, clamped to something sane. */ -function devicePixelRatioClamped(): number { - if (typeof window === 'undefined') return 1; - return Math.max(1, Math.min(3, Math.round(window.devicePixelRatio || 1))); -} diff --git a/react-vectortile/src/VectorTileRasteriser.ts b/react-vectortile/src/VectorTileRasteriser.ts deleted file mode 100644 index 89a8286..0000000 --- a/react-vectortile/src/VectorTileRasteriser.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - RasterTilePreferenceKey, - TileServerRegistry, - TileScheme, - createRasterLayerState, - type AttributionRule, - type MapStyleHost, - type RasterLayerState, -} from '@mapconductor/js-sdk-core'; -import { VectorTileProvider } from '../vendor/vectortile/dist/index.js'; -import type { StyleAffects } from '@mapconductor/js-vectorstyle'; -import type { VectorStyleRasterisation, VectorStyleRasteriser } from '@mapconductor/js-vectorstyle'; - -/** How long a burst of restyles is gathered into one redraw. */ -const COALESCE_MS = 150; - -/** The deepest zoom the renderer is asked to draw. */ -const MAX_ZOOM = 22; - -export interface VectorTileRasteriserOptions { - /** Edge of a rendered tile in CSS pixels. The map's own preference wins. */ - readonly tileSize?: number; - /** Sent with every source-tile, sprite and glyph request. */ - readonly headers?: Record; - /** Where the renderer's `.wasm` is served from, if not the default. */ - readonly wasmUrl?: string | URL; - /** Path the tile service worker is registered from. */ - readonly serviceWorkerPath?: string; -} - -/** - * Draws a vector style for a map that cannot read one. - * - * ```tsx - * - * ``` - * - * Google Maps, MapKit, HERE, ArcGIS, Leaflet, OpenLayers and the rest have - * no vector renderer. This renders the same compiled document to tiles in - * the browser and mounts them as the basemap, so the adjustments an app - * writes once produce the same map on every backend. MapLibre, Mapbox and - * MapTiler never call it — they are handed the document itself. - * - * ios-vectortile の `VectorTileRasteriser`、android-vectortile の同名クラスと - * 対応する。世代の付け方、地と文字の分け方、restyle のまとめ方まで揃えてある。 - */ -export class VectorTileRasteriser implements VectorStyleRasteriser { - constructor(private readonly options: VectorTileRasteriserOptions = {}) {} - - install(host: MapStyleHost, styleJson: string, _affects: StyleAffects): VectorStyleRasterisation { - return new Rasterisation(host, this.options, styleJson); - } -} - -/** - * One installed rasterisation. - * - * Two layers, not one. A glyph range arriving must redraw the labels and - * leave the ground alone, and a recolour that touches neither must redraw - * nothing — so the halves carry separate generations, and the generation is - * in the tile URL because a map only refetches a raster source whose URL - * changed. - */ -class Rasterisation implements VectorStyleRasterisation { - private readonly groupId = `vectorstyle-raster-${Math.random().toString(36).slice(2, 10)}`; - private readonly tileServer = TileServerRegistry.get(); - private readonly groundRoute: string; - private readonly labelRoute: string; - - private provider: VectorTileProvider | null = null; - private disposed = false; - /** Bumped per restyle request; a build for an older one is dropped. */ - private revision = 0; - private pendingStyleJson: string | null = null; - private pendingAffects: StyleAffects = 'none'; - private redrawTimer: ReturnType | null = null; - private groundGeneration = 0; - private labelGeneration = 0; - private credits: AttributionRule[] = []; - /** CSS px one tile covers on screen. */ - private tileSize: number; - /** Pixels per tile: what the renderer draws. */ - private pixelTileSize: number; - /** - * The size the map is told, in the layer state and in the tile URL - * (Leaflet derives its zoom offset from the URL's size segment; the - * server itself ignores it). The CSS size for a map that lays tiles out - * in CSS px, the pixel size for one that lays them out in device px -- - * which is the provider's to say, through `RasterTilePreference`. - */ - private declaredTileSize: number; - - constructor( - private readonly host: MapStyleHost, - private readonly options: VectorTileRasteriserOptions, - styleJson: string, - ) { - this.groundRoute = `${this.groupId}-geom`; - this.labelRoute = `${this.groupId}-labels`; - this.tileSize = options.tileSize ?? 512; - this.pixelTileSize = this.tileSize; - this.declaredTileSize = this.tileSize; - void this.start(styleJson); - } - - restyle(styleJson: string, affects: StyleAffects): void { - if (this.disposed) return; - // An empty rule set means “as authored”. It has no affected role in - // the style compiler, but it still has to replace the currently - // rendered preset. Treat it as a full redraw so raster backends do - // not keep showing the previous preset forever. - const redraw: StyleAffects = affects === 'none' ? 'both' : affects; - this.revision += 1; - this.pendingStyleJson = styleJson; - this.pendingAffects = widen(this.pendingAffects, redraw); - if (this.redrawTimer !== null) return; - // A slider emits every frame. One document per burst, with the - // periodic previews a continuous drag wants. - this.redrawTimer = setTimeout(() => { - this.redrawTimer = null; - const latest = this.pendingStyleJson; - const affectsNow = this.pendingAffects; - const requested = this.revision; - this.pendingStyleJson = null; - this.pendingAffects = 'none'; - if (latest === null) return; - void this.apply(latest, affectsNow, requested); - }, COALESCE_MS); - } - - dispose(): void { - if (this.disposed) return; - this.disposed = true; - if (this.redrawTimer !== null) clearTimeout(this.redrawTimer); - this.redrawTimer = null; - this.pendingStyleJson = null; - const current = this.provider; - this.provider = null; - this.tileServer.unregister(this.groundRoute); - this.tileServer.unregister(this.labelRoute); - current?.dispose(); - this.host.removeRaster(`${this.groupId}-geom`); - this.host.removeRaster(`${this.groupId}-labels`); - } - - // MARK: - private - - private async start(styleJson: string): Promise { - try { - // The worker has to be controlling the page before any tile is - // asked for, or the first requests 404 before the route exists. - this.tileServer.startServiceWorker(this.options.serviceWorkerPath ?? '/tile-sw.js'); - await this.tileServer.waitForController(); - if (this.disposed) return; - const preference = this.host.serviceRegistry.get(RasterTilePreferenceKey); - this.tileSize = this.options.tileSize ?? preference?.preferredTileSize ?? 512; - this.pixelTileSize = Math.round(this.tileSize * pixelRatio()); - this.declaredTileSize = preference?.layoutInDevicePixels ? this.pixelTileSize : this.tileSize; - await this.apply(styleJson, 'both', this.revision); - } catch (error: unknown) { - this.host.report([`the style could not be rasterised: ${describe(error)}`]); - } - } - - private async apply(styleJson: string, affects: StyleAffects, requested: number): Promise { - let created: VectorTileProvider; - try { - created = await VectorTileProvider.create({ - style: styleJson, - // Two numbers, deliberately: the pixels to fill, and the - // CSS px they are shown at. See `pixelRatio`. - tileSize: this.pixelTileSize, - displayTileSize: this.tileSize, - cacheBytes: 48 * 1024 * 1024, - persistSourceTiles: true, - persistRenderedTiles: true, - headers: this.options.headers, - wasmUrl: this.options.wasmUrl, - }); - } catch (error: unknown) { - this.host.report([`the style could not be rasterised: ${describe(error)}`]); - return; - } - // Built asynchronously, so by now the answer may be stale or the - // whole thing taken off. - if (this.disposed || requested !== this.revision) { - created.dispose(); - return; - } - - const previous = this.provider; - this.provider = created; - this.tileServer.register(this.groundRoute, created.groundTiles); - this.tileServer.register(this.labelRoute, created.labelTiles); - this.credits = created.attributions.map((attribution) => ({ attribution })); - - // Glyphs arrive after the tiles that need them: a range is a round - // trip and a low zoom wants dozens, so tiles are drawn with what is - // loaded and the labels are refetched once more is. - created.onGlyphsLoaded = () => { - if (this.disposed || this.provider !== created) return; - this.labelGeneration += 1; - this.host.upsertRaster(this.labelState()); - }; - - if (affects === 'ground' || affects === 'both') this.groundGeneration += 1; - if (affects === 'labels' || affects === 'both') this.labelGeneration += 1; - previous?.dispose(); - - if (created.diagnostics.length > 0) this.host.report(created.diagnostics); - if (affects === 'ground' || affects === 'both') this.host.upsertRaster(this.groundState()); - if (affects === 'labels' || affects === 'both') this.host.upsertRaster(this.labelState()); - } - - /** Geometry only, drawn below everything, never invalidated by glyphs. */ - private groundState(): RasterLayerState { - return createRasterLayerState({ - id: `${this.groupId}-geom`, - source: { - type: 'UrlTemplate', - template: this.tileServer.urlTemplate({ - routeId: this.groundRoute, - tileSize: this.declaredTileSize, - cacheKey: `g${this.groundGeneration}`, - }), - tileSize: this.declaredTileSize, - maxZoom: MAX_ZOOM, - attributionRules: this.credits, - scheme: TileScheme.XYZ, - }, - // Under everything the app declared, and under the labels. - // A style *is* the basemap here. - zIndex: -2, - }); - } - - /** The transparent overlay the glyphs land on. */ - private labelState(): RasterLayerState { - return createRasterLayerState({ - id: `${this.groupId}-labels`, - source: { - type: 'UrlTemplate', - template: this.tileServer.urlTemplate({ - routeId: this.labelRoute, - tileSize: this.declaredTileSize, - cacheKey: `g${this.labelGeneration}`, - }), - tileSize: this.declaredTileSize, - maxZoom: MAX_ZOOM, - // Both halves carry the credit, so hiding either one cannot - // silence it; the overlay ends in `distinct`. - attributionRules: this.credits, - scheme: TileScheme.XYZ, - }, - zIndex: -1, - }); - } -} - -/** Two changes in one burst affect whatever either of them affected. */ -function widen(current: StyleAffects, next: StyleAffects): StyleAffects { - if (current === 'none') return next; - if (current === next) return current; - return 'both'; -} - -/** - * Device pixels per CSS pixel, clamped to something sane. - * - * A raster basemap is laid out by the map in device pixels, so on a 2x - * screen a tile declared as 512 covers half the ground it does on a 1x one: - * the map asks for a zoom one level deeper and the style is read there, - * which is how every label and road ends up half size. Declaring - * `size * ratio` keeps the zoom, rendering that many pixels keeps it sharp, - * and `displayTileSize` tells the rasteriser the ground has not changed. - * - * ios-vectortile's `VectorTileProvider(tileSize:renderScale:)` splits the - * same two numbers, for the same reason. - */ -function pixelRatio(): number { - if (typeof window === 'undefined') return 1; - return Math.max(1, Math.min(3, Math.round(window.devicePixelRatio || 1))); -} - -function describe(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/react-vectortile/src/index.ts b/react-vectortile/src/index.ts deleted file mode 100644 index bbd48bc..0000000 --- a/react-vectortile/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { VectorTileLayer } from './VectorTileLayer'; -export type { VectorTileLayerProps } from './VectorTileLayer'; -export { VectorTileRasteriser } from './VectorTileRasteriser'; -export type { VectorTileRasteriserOptions } from './VectorTileRasteriser'; diff --git a/react-vectortile/tsconfig.json b/react-vectortile/tsconfig.json deleted file mode 100644 index e95aa4e..0000000 --- a/react-vectortile/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM"], - "jsx": "react-jsx", - "strict": true, - "declaration": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowJs": true, - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/react-vectortile/tsup.config.ts b/react-vectortile/tsup.config.ts deleted file mode 100644 index de32061..0000000 --- a/react-vectortile/tsup.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['esm'], - dts: true, - clean: true, - // vendor/ is the Rust build fetched from MapConductor/vectortile's release. - // It ships next to dist/ as it is: it finds its worker and wasm relative to - // its own files (`new URL('./worker.js', import.meta.url)`), which bundling - // would break. - external: [/\/vendor\//, '@mapconductor/js-sdk-core'], -}); diff --git a/scripts/fetch-vector-assets.mjs b/scripts/fetch-vector-assets.mjs deleted file mode 100644 index a88183f..0000000 --- a/scripts/fetch-vector-assets.mjs +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node -/** - * Puts the Rust renderer (wasm + its JS bindings) where react-vectortile - * imports it from. The style compiler is fetched by js-vectorstyle itself, the - * same way, from its own copy of this script; keep the two tags in step. - * - * node scripts/fetch-vector-assets.mjs fetch what is missing or stale - * node scripts/fetch-vector-assets.mjs --force fetch again regardless - * - * They are built and released by MapConductor/vectortile, not here. The tag and - * the SHA-256 of each asset are pinned in scripts/vector-assets.json, and an - * asset that does not match its checksum is refused. Each package's `prebuild` - * runs this, so a clean checkout builds, tests and publishes the same bytes. - * - * To try a local build of mapconductor-vectortile instead, point - * MAPCONDUCTOR_VECTORTILE_DIR at that checkout (after `npm run build` in its - * bindings/js). Nothing is verified then, and the stamp - * says so, so the next plain run fetches the pinned release again. - */ -import { createHash } from "node:crypto"; -import { execFileSync } from "node:child_process"; -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); -const PIN = JSON.parse(readFileSync(join(ROOT, "scripts", "vector-assets.json"), "utf8")); - -/** Release asset -> where it is unpacked, and where a local build keeps it. */ -const TARGETS = [ - { asset: "vectortile-web.tgz", dest: "react-vectortile/vendor/vectortile", local: "bindings/js" }, -]; - -const force = process.argv.includes("--force"); -const localDir = process.env.MAPCONDUCTOR_VECTORTILE_DIR; - -function stampOf(dest) { - const path = join(ROOT, dest, ".release"); - return existsSync(path) ? readFileSync(path, "utf8").trim() : null; -} - -function fromLocalBuild({ dest, local }) { - const src = join(localDir, local); - for (const part of ["pkg", "dist", "package.json"]) { - if (!existsSync(join(src, part))) throw new Error(`${join(src, part)} is missing; build it first`); - } - const out = join(ROOT, dest); - rmSync(out, { recursive: true, force: true }); - mkdirSync(out, { recursive: true }); - for (const part of ["pkg", "dist", "package.json"]) cpSync(join(src, part), join(out, part), { recursive: true }); - // wasm-pack's own `*` .gitignore; harmless here, but it is not in a release either. - rmSync(join(out, "pkg", ".gitignore"), { force: true }); - writeFileSync(join(out, ".release"), `local ${src}\n`); - console.log(`${dest} <- ${src} (local build, unverified)`); -} - -async function fromRelease({ asset, dest }) { - const expected = PIN.assets[asset]; - if (!expected) throw new Error(`scripts/vector-assets.json pins no checksum for ${asset}`); - const stamp = `${PIN.tag} ${expected}`; - if (!force && stampOf(dest) === stamp) return; - - const url = `https://github.com/${PIN.repository}/releases/download/${PIN.tag}/${asset}`; - const response = await fetch(url); - if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`); - const bytes = Buffer.from(await response.arrayBuffer()); - const actual = createHash("sha256").update(bytes).digest("hex"); - if (actual !== expected) { - throw new Error(`${asset} from ${PIN.tag}: SHA-256 ${actual}, expected ${expected}`); - } - - const archive = join(tmpdir(), `mapconductor-${PIN.tag}-${asset}`); - writeFileSync(archive, bytes); - const out = join(ROOT, dest); - rmSync(out, { recursive: true, force: true }); - mkdirSync(out, { recursive: true }); - try { - execFileSync("tar", ["-xzf", archive, "-C", out], { stdio: "inherit" }); - } finally { - rmSync(archive, { force: true }); - } - writeFileSync(join(out, ".release"), `${stamp}\n`); - console.log(`${dest} <- ${PIN.repository}@${PIN.tag} ${asset}`); -} - -for (const target of TARGETS) { - if (localDir) fromLocalBuild(target); - else await fromRelease(target); -} diff --git a/scripts/vector-assets.json b/scripts/vector-assets.json deleted file mode 100644 index c624af2..0000000 --- a/scripts/vector-assets.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "repository": "MapConductor/vectortile", - "tag": "v0.1.0", - "assets": { - "vectortile-web.tgz": "f87e498e36d892595ba240dbf4cdcb94914bd0088b98d66feff842a0fdc4f490" - } -} From 9f950d9e3e977f92317870e147b7aae7a9675c4c Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 28 Oct 2026 22:23:02 -0700 Subject: [PATCH 2/2] Point js-vectorstyle at develop, now linking react-vectortile's repository --- js-vectorstyle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-vectorstyle b/js-vectorstyle index 18e17e4..10f361b 160000 --- a/js-vectorstyle +++ b/js-vectorstyle @@ -1 +1 @@ -Subproject commit 18e17e477cccce8e1e827e66b22380f82ca01969 +Subproject commit 10f361bd2e461bceab55b90723490ae448910e0e