Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ dist/*
*.map
*.mjs
*.cjs
# The patterns above are unanchored, so they swallow sources as well as build
# output — `!src/tileserver/tile-sw.js` above is the same problem, patched once
# before. The whole test suite was invisible to git because of it.
!test/**
dist/*.d.ts
dist/
dist/

# 適合テストが吐く自分の出力。突き合わせ用の中間物で、正本は projection-expected.txt。
test/resources/projection-actual.*.txt
test/resources/geocell-actual.*.txt
5,590 changes: 3,239 additions & 2,351 deletions api/js-sdk-core.api.d.ts

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions src/controller/BaseMapViewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { OnPolylineEventHandler, PolylineState } from '../polyline/Polyline
import type { OnPolygonEventHandler, PolygonState } from '../polygon/PolygonState';
import type { OnCircleEventHandler, CircleState } from '../circle/CircleState';
import type { OnGroundImageEventHandler, GroundImageState } from '../groundimage/GroundImageState';
import type { RasterLayerMounting } from '../map/MapViewStyle';
import type { RasterLayerState } from '../raster/RasterLayerState';

// 統一ズーム(Google 準拠)での許容誤差。これ未満の差では補正しない。
Expand Down Expand Up @@ -155,6 +156,82 @@ export abstract class BaseMapViewController {
if (index >= 0) this.overlayControllers.splice(index, 1);
}

/**
* ラスターレイヤを **children の外から**載せる/外す。
*
* `compositionRasterLayers` は宣言された集合でまるごと置き換えるので、外から
* 足したものは次の再レンダーで消える。{@link RasterLayerController.upsert} は
* その掃除から id を外すので、外から載せたいものはここを通す。
*
* 呼ぶのは 2 つだけ: マーカータイリングと、ベクタースタイルを描けない
* バックエンドに渡す「スタイルから描いたタイル」。
*
* **プロバイダ非依存**。raster コントローラはどのプロバイダも
* `registerOverlayController` 済みなので、プロバイダごとの配線は要らない。
* android-sdk の `BaseMapViewController.mountRasterLayer` と同じ。
*/
mountRasterLayer(state: RasterLayerState): void {
for (const controller of this.rasterLayerMounts()) void controller.upsert(state);
}

/** {@link mountRasterLayer} で載せたものを外す。無いものを渡しても何も起きない。 */
unmountRasterLayer(id: string): void {
for (const controller of this.rasterLayerMounts()) void controller.removeById(id);
}

private rasterLayerMounts(): RasterLayerMounting[] {
return this.overlayControllers.filter(
(controller): controller is OverlayControllerLike & RasterLayerMounting =>
isSlottedOverlayController(controller) &&
controller.kind === 'rasterLayer' &&
typeof (controller as Partial<RasterLayerMounting>).upsert === 'function' &&
typeof (controller as Partial<RasterLayerMounting>).removeById === 'function',
);
}

// ── スタイル読み込みの通知 ────────────────────────────────────────────

private styleLoadedListeners = new Set<() => void>();
/** スタイルが今載っているか。sticky: 聞かれる前に読み終わることがある。 */
private styleLoaded = false;

/**
* スタイル文書を読み終わるたびに `block` を呼ぶ。既に読み終わっていれば今すぐ 1 回。
*
* {@link MapViewStyle} が生きたレンダラへ入れたもの(paint プロパティ、足した
* レイヤ)は、レンダラが文書を読み直した時点で全部消える——アプリのベースマップ
* 切替、デザイン変更、プロバイダのビュー作り直し、理由は何であれ。ここが戻し場所。
* 1 本のリスナーではなく登録簿なのは、ミューテーションストアと設置済みスタイルの
* **両方**が聞く必要があるため。
*
* そういう概念を持たないプロバイダ(Google Maps、Leaflet…)は
* {@link notifyStyleLoaded} を呼ばない。そこに載ったスタイルはラスター化された
* もので、ベースマップ切替では消えない。
*
* @returns 聞くのをやめる手形。
*/
addStyleLoadedListener(block: () => void): () => void {
this.styleLoadedListeners.add(block);
if (this.styleLoaded) block();
return () => this.styleLoadedListeners.delete(block);
}

/**
* スタイルが載り、**かつオーバーレイが戻った**時点でプロバイダが呼ぶ。
*
* 読み込み開始時ではなく、プロバイダが自分のレイヤを戻す前でもない。作り直しの
* 途中でパッチを当てるリスナーは、これから置き換わるものにパッチを当てることになる。
*
* android / iOS では protected。web だけ public なのは、GL 系 3 プロバイダで
* `styledata` を聞いているのがコントローラではなく
* `useVectorStyleMutationSupport`(共有フック)だから——呼ぶのは依然として
* 「プロバイダ側の 1 か所」で、その 1 か所がフックに寄っているだけ。
*/
notifyStyleLoaded(): void {
this.styleLoaded = true;
for (const listener of [...this.styleLoadedListeners]) listener();
}

// ── タップのカスケード ────────────────────────────────────────────────
//
// marker → circle → groundImage → polyline → polygon → map の一本道。
Expand Down
25 changes: 25 additions & 0 deletions src/map/BlankMapStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* A MapLibre-style map with nothing in it but a background.
*
* What a provider shows for "no basemap" when it is built on MapLibre (or
* takes a MapLibre style): the same colour as the device-side blank styles in
* android-sdk-core / ios-sdk-core. A map that draws its whole viewport itself
* -- vector tiles rendered on the client, say -- fetches and paints the basemap
* under it for nobody; this is the style that fetches nothing.
*/
export const BLANK_MAP_STYLE = {
version: 8 as const,
name: 'blank',
sources: {},
layers: [
{
id: 'background',
type: 'background' as const,
paint: { 'background-color': '#f2efe9' },
},
],
};

/** [BLANK_MAP_STYLE] as a URL, for providers whose design carries a style URL. */
export const BLANK_MAP_STYLE_URL =
'data:application/json,' + encodeURIComponent(JSON.stringify(BLANK_MAP_STYLE));
17 changes: 17 additions & 0 deletions src/map/MapViewBase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CameraRestriction } from './CameraRestriction';
import type { MapViewStyle } from './MapViewStyle';
import type { GeoPoint } from '../features/GeoPoint';
import type { MapCameraPosition } from '../types/MapCamera';
import type { MapViewControllerInterface } from '../controller/MapViewControllerInterface';
Expand Down Expand Up @@ -34,6 +35,22 @@ export interface MapViewBaseProps<TState extends MapViewStateInterface<MapDesign
* そちらはマップ生成時にしか反映されない。実行時に変更したい場合はこちらを使う。
*/
cameraRestriction?: CameraRestriction | null;
/**
* 地図の見た目。デザイン名を選ぶ代わりにアプリが直接述べるときに渡す。
*
* ベクタースタイルは**ベースマップそのもの**なので、重ねるのではなく
* 「これが地図だ」と言う。`@mapconductor/vectorstyle` が作る。下で何が
* 起きるかはバックエンド次第で、アプリは知らなくてよい。
*
* **android-sdk と ios-sdk ではこの引数名は `style`。** JS だけ違うのは
* `style` が React/React Native では要素自身の見た目を指す名前として
* 予約同然だから。RN の `MapViewBaseProps` は実際に `style` を
* コンテナのスタイルとして使っており、そこを奪うと React を書く人全員が
* 踏む。web と RN で名前を割るほうが悪いので、JS 側は両方 `mapStyle`。
*/
mapStyle?: MapViewStyle | null;
/** スタイルが言ってきたこと。渡さなければ console に出る。 */
onStyleDiagnostics?: (diagnostics: readonly string[]) => void;
}

/**
Expand Down
196 changes: 196 additions & 0 deletions src/map/MapViewStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import type { RasterLayerState } from '../raster/RasterLayerState';
import type { LocalTileServer } from '../tileserver/LocalTileServer';
import type { MapServiceRegistry } from './MapServiceRegistry';

/**
* How a map looks, as a property of the map rather than a layer inside it.
*
* A vector style *is* the basemap, so passing one to `<XxxView style={...}>`
* says what it is rather than mounting something over it. What happens
* underneath depends on the backend and the app does not have to know:
* MapLibre, Mapbox and MapTiler are told the style and draw it themselves;
* Google Maps, Leaflet, Cesium, OpenLayers and the rest are handed raster
* tiles rendered from the same style in a worker. Either way the same
* adjustments produce the same map.
*
* An interface rather than a class, for two reasons. Core must not depend on
* the style compiler or the rasteriser — both are optional, and one of them
* is a wasm binary — so what core holds is the seam and the modules bring
* the implementation. And a provider's own styling, which is nothing like a
* MapLibre style, can be added later as another implementation without this
* changing.
*
* The concrete implementations live in the vector style module; an app does
* not implement this.
*
* android-sdk-core and ios-sdk-core carry the same seam.
*/
export interface MapViewStyle {
/**
* What identifies this style's content.
*
* The map re-installs a style whose key changed and leaves alone one
* whose key did not. Two objects describing the same thing must have the
* same key: a style built inline in JSX is a new object on every
* render — which is exactly what a colour picker produces — and keying
* off identity would reinstall the whole style on every unrelated state
* change.
*/
readonly key: string;

/**
* Puts the style on the map. Called once the map has a style loaded, and
* again if it loads another one.
*
* @returns the ticket that takes it off again. Disposing must leave the
* map as it was found: a style that cannot be fully undone has no
* business being installed, because the app will switch away from it.
*/
install(host: MapStyleHost): MapStyleInstallation;
}

/**
* A ticket to undo, and a chance to take a change without tearing down.
*
* Returned by anything that sets something up on a map.
*/
export interface MapStyleInstallation {
dispose(): void;

/**
* Takes `next` in place, when it is near enough to what is installed,
* and answers whether it did.
*
* Without this the whole point is lost. A style whose key changed is
* installed again, and installing a style means handing the map a
* document — which is the reload this design exists to avoid. Moving a
* colour slider changes the adjustments and nothing else, so the
* installed style answers true, applies the difference, and the map
* never reloads anything.
*
* Left out, or false, and the caller disposes and installs `next` from
* scratch. False is always correct, only slower.
*/
update?(next: MapViewStyle): boolean;
}

/**
* What a map offers to the style installed on it.
*
* The provider implements this; a {@link MapViewStyle} uses it and never
* touches a provider type. That is what keeps one style implementation
* working on every backend.
*/
export interface MapStyleHost {
/**
* Where to look for what this map can do: `VectorStyleSupportKey` to
* hand over a style document, `VectorStyleMutationSupportKey` to change
* a loaded one in place, `RasterTilePreferenceKey` for the tile size it
* would rather have.
*/
readonly serviceRegistry: MapServiceRegistry;

/** For serving a style document, or tiles rendered from one, to the map. */
readonly tileServer: LocalTileServer;

/**
* The vector style the map is drawing now, when it is drawing one.
*
* Undefined on every backend that has no such thing, which is most of
* them. It is how a rule set that adjusts "whatever is showing" finds
* something to adjust: the style has to be read before a rule can be
* matched against its layers, and only the map knows where it came from.
*/
readonly vectorStyleUrl?: string;

/**
* Runs `block` whenever the map finishes loading a style, including the
* one it has now if it already has one.
*
* Everything a style installs on a live renderer is lost when that
* renderer loads a document again, for any reason. This is how it gets
* put back.
*/
onStyleLoaded(block: () => void): MapStyleInstallation;

/**
* Adds or updates a raster layer the app did not declare.
*
* The raster path needs somewhere to put its tiles, and that cannot be
* the children of the view: the style is a prop, so it is installed from
* outside the tree. Marker tiling already mounts its layer the same way.
*/
upsertRaster(state: RasterLayerState): void;

removeRaster(id: string): void;

/**
* Says what the app should know: a rule that matched nothing, an
* adjustment this backend cannot make, a style that could not be read.
*
* Routed to whatever the host gave the style, so a module can report
* without knowing how the app logs.
*/
report(diagnostics: readonly string[]): void;
}

/**
* ラスターレイヤを children の外から載せられるコントローラ。
*
* {@link RasterLayerController} が既に両方持っている。これは「どのプロバイダの
* コントローラか」を知らずにそこへ届くための名前。
*/
export interface RasterLayerMounting {
upsert(state: RasterLayerState): Promise<void>;
removeById(id: string): Promise<void>;
}

/**
* A style that can be carried to a native map SDK instead of installed here.
*
* React Native is the one host where {@link MapViewStyle.install} cannot do
* the work. The compiler is a wasm binary and Hermes has no wasm, so the
* style cannot be compiled in JavaScript on a device — and the map is not a
* JavaScript object anyway: it is the Android or iOS SDK, which already has
* the whole of this working natively.
*
* So on React Native the style is not installed; it is **described**, sent
* across the bridge as two strings, and rebuilt on the other side by the
* platform's own vector style module. The map a user sees is produced by
* exactly the code an Android or iOS app would run, which is why there is
* nothing here about how it is drawn.
*
* Core declares the shape; `@mapconductor/react-vectorstyle` fills it in.
*/
export interface NativeStyleDescriptor {
/** The style document itself, when the app had it. */
readonly documentJson?: string;
/** Where to fetch the document, when the app named a URL. */
readonly documentUrl?: string;
/** Headers for that fetch — an API key, usually. */
readonly documentHeaders?: Readonly<Record<string, string>>;
/**
* Neither of the above: adjust whatever the map is already drawing.
* The native side reads the design's own style URL.
*/
readonly fromCurrentDesign?: boolean;
/** The rules document, in the form every platform's compiler reads. */
readonly rulesJson: string;
}

/**
* A {@link MapViewStyle} that knows how to describe itself to a native SDK.
*
* Checked structurally rather than by class, so core stays free of the
* module that implements it.
*/
export interface NativeDescribableStyle extends MapViewStyle {
toNativeDescriptor(): NativeStyleDescriptor;
}

/** Whether `style` can be sent to a native SDK rather than installed here. */
export function isNativeDescribableStyle(
style: MapViewStyle | null | undefined,
): style is NativeDescribableStyle {
return typeof (style as Partial<NativeDescribableStyle> | null)?.toNativeDescriptor === 'function';
}
Loading