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/js-vectorstyle b/js-vectorstyle
index 18e17e4..10f361b 160000
--- a/js-vectorstyle
+++ b/js-vectorstyle
@@ -1 +1 @@
-Subproject commit 18e17e477cccce8e1e827e66b22380f82ca01969
+Subproject commit 10f361bd2e461bceab55b90723490ae448910e0e
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"
- }
-}