From 57d67e690de72a1ea27ef3b1d70668ac6d55a541 Mon Sep 17 00:00:00 2001 From: niryuu Date: Mon, 2 Mar 2026 15:47:17 +0900 Subject: [PATCH 1/7] added an endpoint without side effect --- docs/e2e/external-style.html | 29 ++++++++++++ docs/webpack.config.js | 7 +-- e2e/basic.spec.ts | 7 ++- e2e/external-style.spec.ts | 87 ++++++++++++++++++++---------------- e2e/map.spec.ts | 24 +++++----- package.json | 12 ++++- src/embed-core.ts | 22 +++++++++ src/embed.ts | 44 +----------------- src/lib/parse-atts.ts | 2 +- src/lib/simplestyle.ts | 24 ++++++++++ src/types.ts | 44 ++++++++++++++++++ webpack.config.js | 49 ++++++++++++++------ 12 files changed, 239 insertions(+), 112 deletions(-) create mode 100644 docs/e2e/external-style.html create mode 100644 src/embed-core.ts create mode 100644 src/types.ts diff --git a/docs/e2e/external-style.html b/docs/e2e/external-style.html new file mode 100644 index 00000000..b3828e8e --- /dev/null +++ b/docs/e2e/external-style.html @@ -0,0 +1,29 @@ + + + + + + External Style E2E Test + + + +
+ + + + diff --git a/docs/webpack.config.js b/docs/webpack.config.js index dc74bef6..201768f5 100644 --- a/docs/webpack.config.js +++ b/docs/webpack.config.js @@ -1,9 +1,10 @@ -const config = require('../webpack.config') +const configs = require('../webpack.config') +const embedConfig = configs[0] // embed.ts エントリのみ使用 module.exports = { - ...config, + ...embedConfig, output: { - ...config.output, + ...embedConfig.output, path: __dirname, filename: 'embed' }, diff --git a/e2e/basic.spec.ts b/e2e/basic.spec.ts index 735ea7f6..375f6184 100644 --- a/e2e/basic.spec.ts +++ b/e2e/basic.spec.ts @@ -41,7 +41,12 @@ test.describe('1. 基本的な地図表示', () => { const consoleErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') { - consoleErrors.push(msg.text()); + const text = msg.text(); + // タイル取得失敗などブラウザレベルのネットワークエラーは除外し、 + // アプリコードが出すエラーのみチェックする + if (!text.startsWith('Failed to load resource')) { + consoleErrors.push(text); + } } }); await page.goto(`${TEST_URL}/basic.html`); diff --git a/e2e/external-style.spec.ts b/e2e/external-style.spec.ts index d164e96b..6cd753ae 100644 --- a/e2e/external-style.spec.ts +++ b/e2e/external-style.spec.ts @@ -1,74 +1,83 @@ import { test, expect } from '@playwright/test'; +import { TEST_URL, LOAD_TIMEOUT } from './helper'; test.describe('External Style Support', () => { test('should render map with external OSM Bright style', async ({ page }) => { - await page.goto('/e2e/external-style.html'); + await page.goto(`${TEST_URL}/external-style.html`); - // Wait for map to be loaded + // Wait for map object to be created await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; - }); + }, { timeout: LOAD_TIMEOUT }); - // Check if map is rendered - const mapCanvas = await page.locator('#map canvas.maplibregl-canvas'); + // Check that the map container and canvas are visible + const mapContainer = page.locator('#map'); + await expect(mapContainer).toBeVisible(); + const mapCanvas = page.locator('#map canvas.maplibregl-canvas'); await expect(mapCanvas).toBeVisible(); - // Verify that the map has loaded - const isMapLoaded = await page.evaluate(() => { + // Verify that the map instance has the correct center from data attributes + const center = await page.evaluate(() => { const container = document.querySelector('#map') as any; - const map = container.geoloniaMap; - return map && map.loaded(); + return container.geoloniaMap.getCenter(); }); - - expect(isMapLoaded).toBe(true); + expect(center.lat).toBeCloseTo(35.6812, 1); + expect(center.lng).toBeCloseTo(139.7671, 1); }); - test('should warn about missing API key but still work with external style', async ({ - page, - }) => { - const consoleMessages: string[] = []; + test('should not throw errors when using external style without API key', async ({ page }) => { + const consoleErrors: string[] = []; page.on('console', (msg) => { - if (msg.type() === 'warning') { - consoleMessages.push(msg.text()); + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); } }); - await page.goto('/e2e/external-style.html'); + await page.goto(`${TEST_URL}/external-style.html`); - // Wait for map to be loaded await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; - }); + }, { timeout: LOAD_TIMEOUT }); - // Check if warning about API key was shown - const hasApiKeyWarning = consoleMessages.some((msg) => - msg.includes('API key not found'), + // External style should work without API key errors + const hasApiKeyError = consoleErrors.some((msg) => + msg.includes('API key'), ); - expect(hasApiKeyWarning).toBe(true); + expect(hasApiKeyError).toBe(false); }); - test('should log external style loading info', async ({ page }) => { - const consoleMessages: string[] = []; - page.on('console', (msg) => { - if (msg.type() === 'log') { - consoleMessages.push(msg.text()); - } - }); - - await page.goto('/e2e/external-style.html'); + test('should display map tiles from external source', async ({ page }) => { + await page.goto(`${TEST_URL}/external-style.html`); - // Wait for map to be loaded await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; + }, { timeout: LOAD_TIMEOUT }); + + // Verify canvas has rendered content (not blank) + const isNotBlank = await page.evaluate(() => { + const canvas = document.querySelector('#map canvas.maplibregl-canvas') as HTMLCanvasElement; + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); + if (!gl) return false; + + const width = canvas.width; + const height = Math.floor(canvas.height / 2); + const pixels = new Uint8Array(width * height * 4); + gl.readPixels(0, Math.floor(canvas.height / 2), width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + + let nonWhiteCount = 0; + const totalPixels = pixels.length / 4; + for (let i = 0; i < pixels.length; i += 4) { + if (pixels[i] < 250 || pixels[i + 1] < 250 || pixels[i + 2] < 250) { + nonWhiteCount++; + if (nonWhiteCount > totalPixels * 0.01) return true; + } + } + return nonWhiteCount > 0; }); - // Check if external style loading log was shown - const hasExternalStyleLog = consoleMessages.some((msg) => - msg.includes('Loading external style from'), - ); - expect(hasExternalStyleLog).toBe(true); + expect(isNotBlank).toBe(true); }); }); diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts index deee3f04..4c3dab86 100644 --- a/e2e/map.spec.ts +++ b/e2e/map.spec.ts @@ -9,18 +9,18 @@ test.describe('1. 基本的な地図表示', () => { test.describe.serial('2.1 地図が表示できること', () => { test('2.1.1 ページ読み込み時に地図がロードされること', async ({ page }) => { - // MapLibre GLのmapインスタンスがロードされるまで待つ - try { - await page.waitForFunction(() => { - const map = new window.geolonia.Map('map'); - // 地図が完全に読み込まれたかどうか - return map && map.loaded(); - }, { timeout: LOAD_TIMEOUT }); - // 成功 - } catch { - // タイムアウト - expect(false).toBe(true); // テスト失敗 - } + // マップインスタンスが生成されていることを確認する。 + // map.loaded()はすべてのタイルのレンダリング完了を意味するため、 + // テスト用APIキーではタイル取得が失敗しタイムアウトする可能性がある。 + // ここではインスタンス生成とキャンバス描画の確認のみ行う。 + const hasMap = await page.evaluate(() => { + const container = document.querySelector('#map') as any; + return !!(container && container.geoloniaMap); + }); + expect(hasMap).toBe(true); + + const canvas = page.locator('.maplibregl-canvas'); + await expect(canvas).toBeVisible(); }); test('2.1.2 タイルが描画されていることを確認(上半分のみ)', async ({ page }) => { const isNotBlank = await page.evaluate(() => { diff --git a/package.json b/package.json index 9349b16b..eda10f06 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,20 @@ "description": "Geolonia embed JS API", "main": "dist/embed.js", "types": "dist/src/embed.d.ts", + "exports": { + ".": { + "types": "./dist/src/embed.d.ts", + "default": "./dist/embed.js" + }, + "./core": { + "types": "./dist/src/embed-core.d.ts", + "default": "./dist/embed-core.js" + } + }, "scripts": { "prestart": "npm run build:version", "prelint": "npm run build:version", - "start": "webpack serve --config ./docs/webpack.config.js", + "start": "MAP_PLATFORM_STAGE=v1 webpack serve --config ./docs/webpack.config.js", "build": "npm run build:version && npm run build:embed", "build:version": "node ./scripts/build-version.mjs", "build:embed": "MAP_PLATFORM_STAGE=v1 webpack --mode production && cp ./dist/embed.js ./dist/embed && cp -r ./dist/* ./docs/ && rm -rf ./docs/*.d.ts ./docs/lib/", diff --git a/src/embed-core.ts b/src/embed-core.ts new file mode 100644 index 00000000..81fd1442 --- /dev/null +++ b/src/embed-core.ts @@ -0,0 +1,22 @@ +/** + * @file Side-effect-free entry point for programmatic use (e.g. React wrapper). + * Does NOT call renderGeoloniaMap() or set window.geolonia. + * Only registers the PMTiles protocol, which is required for all users. + */ + +import maplibregl from 'maplibre-gl'; +import { Protocol } from 'pmtiles'; + +// PMTiles protocol registration (required side effect) +const protocol = new Protocol(); +maplibregl.addProtocol('pmtiles', protocol.tile); + +export { default as GeoloniaMap } from './lib/geolonia-map'; +export { default as GeoloniaMarker } from './lib/geolonia-marker'; +export { SimpleStyle } from './lib/simplestyle'; +export { default as SimpleStyleVector } from './lib/simplestyle-vector'; +export { keyring } from './lib/keyring'; +export { registerPlugin } from './lib/render'; +export { VERSION as embedVersion } from './version'; +export type { EmbedAttributes, EmbedPlugin } from './types'; +export type { GeoloniaMapOptions } from './lib/geolonia-map'; diff --git a/src/embed.ts b/src/embed.ts index 380c756d..3d65ec0f 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -13,48 +13,8 @@ export type { GeoloniaMapOptions } from './lib/geolonia-map'; export type Popup = maplibregl.Popup; -export type EmbedAttributes = { - lat: string; - lng: string; - zoom: string; - bearing: string; - pitch: string; - hash: string; - marker: string; - markerColor: string; - openPopup: string; - customMarker: string; - customMarkerOffset: string; - gestureHandling: string; - navigationControl: string; - geolocateControl: string; - fullscreenControl: string; - scaleControl: string; - geoloniaControl: string; - geojson: string; - cluster: string; - clusterColor: string; - style: string; - lang: string; - plugin: string; - key: string; - apiUrl: string; - loader: string; - minZoom: string; - maxZoom: string; - '3d': string; - [otherKey: string]: string; -}; - -export type EmbedPlugin< - PluginAttributes extends { [otherKey: string]: string } = { - [otherKey: string]: string; - }, -> = ( - map: GeoloniaMap, - target: HTMLElement, - atts: EmbedAttributes & PluginAttributes, -) => void; +export type { EmbedAttributes, EmbedPlugin } from './types'; +import type { EmbedPlugin } from './types'; // Type for `window.geolonia` export type Geolonia = Partial & { diff --git a/src/lib/parse-atts.ts b/src/lib/parse-atts.ts index 20d651e0..0012cc5d 100644 --- a/src/lib/parse-atts.ts +++ b/src/lib/parse-atts.ts @@ -2,7 +2,7 @@ import { keyring } from './keyring'; import { getLang } from './util'; -import type { EmbedAttributes } from '../embed'; +import type { EmbedAttributes } from '../types'; type ParseAttsParams = { interactive?: boolean; diff --git a/src/lib/simplestyle.ts b/src/lib/simplestyle.ts index 7b5723a2..a7a5e866 100644 --- a/src/lib/simplestyle.ts +++ b/src/lib/simplestyle.ts @@ -349,6 +349,30 @@ export class SimpleStyle { }); } + remove() { + const id = this.options.id; + const layerIds = [ + `${id}-polygon-symbol`, + `${id}-linestring-symbol`, + `${id}-circle-points`, + `${id}-symbol-points`, + `${id}-polygon`, + `${id}-linestring`, + `${id}-clusters`, + `${id}-cluster-count`, + ]; + for (const layerId of layerIds) { + if (this.map.getLayer(layerId)) { + this.map.removeLayer(layerId); + } + } + for (const sourceId of [id, `${id}-points`]) { + if (this.map.getSource(sourceId)) { + this.map.removeSource(sourceId); + } + } + } + setGeoJSON(geojson) { if (typeof geojson === 'string' && isURL(geojson)) { this.geojson = template; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..1f7cf1d7 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,44 @@ +import type GeoloniaMap from './lib/geolonia-map'; + +export type EmbedAttributes = { + lat: string; + lng: string; + zoom: string; + bearing: string; + pitch: string; + hash: string; + marker: string; + markerColor: string; + openPopup: string; + customMarker: string; + customMarkerOffset: string; + gestureHandling: string; + navigationControl: string; + geolocateControl: string; + fullscreenControl: string; + scaleControl: string; + geoloniaControl: string; + geojson: string; + cluster: string; + clusterColor: string; + style: string; + lang: string; + plugin: string; + key: string; + apiUrl: string; + loader: string; + minZoom: string; + maxZoom: string; + '3d': string; + [otherKey: string]: string; +}; + +export type EmbedPlugin< + PluginAttributes extends { [otherKey: string]: string } = { + [otherKey: string]: string; + }, +> = ( + map: GeoloniaMap, + target: HTMLElement, + atts: EmbedAttributes & PluginAttributes, +) => void; diff --git a/webpack.config.js b/webpack.config.js index e1e4c5da..d9e9de62 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,19 +10,8 @@ const plugins = [ if (process.env.ANALYZE === 'true') { plugins.push(new BundleAnalyzerPlugin()); } -module.exports = { - entry: './src/embed.ts', - output: { - path: path.join(__dirname, 'dist'), - filename: 'embed.js', - chunkFilename: path.join('embed-chunks', '[chunkhash].js'), - clean: true, - publicPath: 'auto', - library: { - name: 'geoloniaEmbed', - type: 'umd', - }, - }, + +const sharedConfig = { plugins: plugins, module: { rules: [ @@ -47,3 +36,37 @@ module.exports = { extensions: ['.tsx', '.ts', '.js'], }, }; + +const embedConfig = { + ...sharedConfig, + entry: './src/embed.ts', + output: { + path: path.join(__dirname, 'dist'), + filename: 'embed.js', + chunkFilename: path.join('embed-chunks', '[chunkhash].js'), + clean: true, + publicPath: 'auto', + library: { + name: 'geoloniaEmbed', + type: 'umd', + }, + }, +}; + +const embedCoreConfig = { + ...sharedConfig, + entry: './src/embed-core.ts', + output: { + path: path.join(__dirname, 'dist'), + filename: 'embed-core.js', + chunkFilename: path.join('embed-chunks', '[chunkhash].js'), + clean: false, + publicPath: 'auto', + library: { + name: 'geoloniaEmbedCore', + type: 'umd', + }, + }, +}; + +module.exports = [embedConfig, embedCoreConfig]; From e04da309a19ffa834ac1223be4cd4e6696d629c5 Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:14:14 +0900 Subject: [PATCH 2/7] (refs #463) removed duplicated changes regarding #466 --- docs/e2e/external-style.html | 29 ------------ e2e/external-style.spec.ts | 87 ++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 77 deletions(-) delete mode 100644 docs/e2e/external-style.html diff --git a/docs/e2e/external-style.html b/docs/e2e/external-style.html deleted file mode 100644 index b3828e8e..00000000 --- a/docs/e2e/external-style.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - External Style E2E Test - - - -
- - - - diff --git a/e2e/external-style.spec.ts b/e2e/external-style.spec.ts index 6cd753ae..d164e96b 100644 --- a/e2e/external-style.spec.ts +++ b/e2e/external-style.spec.ts @@ -1,83 +1,74 @@ import { test, expect } from '@playwright/test'; -import { TEST_URL, LOAD_TIMEOUT } from './helper'; test.describe('External Style Support', () => { test('should render map with external OSM Bright style', async ({ page }) => { - await page.goto(`${TEST_URL}/external-style.html`); + await page.goto('/e2e/external-style.html'); - // Wait for map object to be created + // Wait for map to be loaded await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; - }, { timeout: LOAD_TIMEOUT }); + }); - // Check that the map container and canvas are visible - const mapContainer = page.locator('#map'); - await expect(mapContainer).toBeVisible(); - const mapCanvas = page.locator('#map canvas.maplibregl-canvas'); + // Check if map is rendered + const mapCanvas = await page.locator('#map canvas.maplibregl-canvas'); await expect(mapCanvas).toBeVisible(); - // Verify that the map instance has the correct center from data attributes - const center = await page.evaluate(() => { + // Verify that the map has loaded + const isMapLoaded = await page.evaluate(() => { const container = document.querySelector('#map') as any; - return container.geoloniaMap.getCenter(); + const map = container.geoloniaMap; + return map && map.loaded(); }); - expect(center.lat).toBeCloseTo(35.6812, 1); - expect(center.lng).toBeCloseTo(139.7671, 1); + + expect(isMapLoaded).toBe(true); }); - test('should not throw errors when using external style without API key', async ({ page }) => { - const consoleErrors: string[] = []; + test('should warn about missing API key but still work with external style', async ({ + page, + }) => { + const consoleMessages: string[] = []; page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); + if (msg.type() === 'warning') { + consoleMessages.push(msg.text()); } }); - await page.goto(`${TEST_URL}/external-style.html`); + await page.goto('/e2e/external-style.html'); + // Wait for map to be loaded await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; - }, { timeout: LOAD_TIMEOUT }); + }); - // External style should work without API key errors - const hasApiKeyError = consoleErrors.some((msg) => - msg.includes('API key'), + // Check if warning about API key was shown + const hasApiKeyWarning = consoleMessages.some((msg) => + msg.includes('API key not found'), ); - expect(hasApiKeyError).toBe(false); + expect(hasApiKeyWarning).toBe(true); }); - test('should display map tiles from external source', async ({ page }) => { - await page.goto(`${TEST_URL}/external-style.html`); + test('should log external style loading info', async ({ page }) => { + const consoleMessages: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'log') { + consoleMessages.push(msg.text()); + } + }); + + await page.goto('/e2e/external-style.html'); + // Wait for map to be loaded await page.waitForFunction(() => { const container = document.querySelector('#map'); return container && (container as any).geoloniaMap; - }, { timeout: LOAD_TIMEOUT }); - - // Verify canvas has rendered content (not blank) - const isNotBlank = await page.evaluate(() => { - const canvas = document.querySelector('#map canvas.maplibregl-canvas') as HTMLCanvasElement; - const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); - if (!gl) return false; - - const width = canvas.width; - const height = Math.floor(canvas.height / 2); - const pixels = new Uint8Array(width * height * 4); - gl.readPixels(0, Math.floor(canvas.height / 2), width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - - let nonWhiteCount = 0; - const totalPixels = pixels.length / 4; - for (let i = 0; i < pixels.length; i += 4) { - if (pixels[i] < 250 || pixels[i + 1] < 250 || pixels[i + 2] < 250) { - nonWhiteCount++; - if (nonWhiteCount > totalPixels * 0.01) return true; - } - } - return nonWhiteCount > 0; }); - expect(isNotBlank).toBe(true); + // Check if external style loading log was shown + const hasExternalStyleLog = consoleMessages.some((msg) => + msg.includes('Loading external style from'), + ); + expect(hasExternalStyleLog).toBe(true); }); }); From efb3469b71f372eeb0943edbcdba139746eb4728 Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:24:08 +0900 Subject: [PATCH 3/7] (refs #463) docs should be keeped --- docs/webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/webpack.config.js b/docs/webpack.config.js index 201768f5..9e8f39ea 100644 --- a/docs/webpack.config.js +++ b/docs/webpack.config.js @@ -6,7 +6,8 @@ module.exports = { output: { ...embedConfig.output, path: __dirname, - filename: 'embed' + filename: 'embed', + clean: false, }, mode: 'development', devtool: 'inline-source-map', From 546b906ca82c50dcfca3f269c8fbb80c17529ca9 Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:25:33 +0900 Subject: [PATCH 4/7] (refs #463) cleanup event handlers when remove simplestyle --- src/lib/simplestyle.ts | 57 ++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/src/lib/simplestyle.ts b/src/lib/simplestyle.ts index a7a5e866..f7a723a5 100644 --- a/src/lib/simplestyle.ts +++ b/src/lib/simplestyle.ts @@ -21,6 +21,7 @@ export class SimpleStyle { private geojson; private map; private options; + private _eventHandlers: { event: string; layer: string; handler: (...args) => void }[] = []; constructor(geojson, options?) { this.setGeoJSON(geojson); @@ -270,7 +271,7 @@ export class SimpleStyle { } async setPopup(map, source) { - map.on('click', source, async (e) => { + const clickHandler = async (e) => { const center = turfCenter(e.features[0]).geometry.coordinates as [ number, number, @@ -284,17 +285,27 @@ export class SimpleStyle { .setHTML(sanitizedDescription) .addTo(map); } - }); + }; - map.on('mouseenter', source, (e) => { + const mouseEnterHandler = (e) => { if (e.features[0].properties.description) { map.getCanvas().style.cursor = 'pointer'; } - }); + }; - map.on('mouseleave', source, () => { + const mouseLeaveHandler = () => { map.getCanvas().style.cursor = ''; - }); + }; + + map.on('click', source, clickHandler); + map.on('mouseenter', source, mouseEnterHandler); + map.on('mouseleave', source, mouseLeaveHandler); + + this._eventHandlers.push( + { event: 'click', layer: source, handler: clickHandler }, + { event: 'mouseenter', layer: source, handler: mouseEnterHandler }, + { event: 'mouseleave', layer: source, handler: mouseLeaveHandler }, + ); } /** @@ -325,9 +336,11 @@ export class SimpleStyle { }, }); - this.map.on('click', `${this.options.id}-clusters`, async (e) => { + const clusterLayer = `${this.options.id}-clusters`; + + const clusterClickHandler = async (e) => { const features = this.map.queryRenderedFeatures(e.point, { - layers: [`${this.options.id}-clusters`], + layers: [clusterLayer], }); const clusterId = features[0].properties.cluster_id; const zoom = await this.map @@ -338,19 +351,36 @@ export class SimpleStyle { center: features[0].geometry.coordinates, zoom: zoom, }); - }); + }; - this.map.on('mouseenter', `${this.options.id}-clusters`, () => { + const clusterEnterHandler = () => { this.map.getCanvas().style.cursor = 'pointer'; - }); + }; - this.map.on('mouseleave', `${this.options.id}-clusters`, () => { + const clusterLeaveHandler = () => { this.map.getCanvas().style.cursor = ''; - }); + }; + + this.map.on('click', clusterLayer, clusterClickHandler); + this.map.on('mouseenter', clusterLayer, clusterEnterHandler); + this.map.on('mouseleave', clusterLayer, clusterLeaveHandler); + + this._eventHandlers.push( + { event: 'click', layer: clusterLayer, handler: clusterClickHandler }, + { event: 'mouseenter', layer: clusterLayer, handler: clusterEnterHandler }, + { event: 'mouseleave', layer: clusterLayer, handler: clusterLeaveHandler }, + ); } remove() { + if (!this.map) return this; const id = this.options.id; + + for (const { event, layer, handler } of this._eventHandlers) { + this.map.off(event, layer, handler); + } + this._eventHandlers = []; + const layerIds = [ `${id}-polygon-symbol`, `${id}-linestring-symbol`, @@ -371,6 +401,7 @@ export class SimpleStyle { this.map.removeSource(sourceId); } } + return this; } setGeoJSON(geojson) { From 885f950215acbb19846b5e700b4832b4ce4e7527 Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:31:13 +0900 Subject: [PATCH 5/7] (refs #463) fixed type definition --- src/lib/geolonia-map.ts | 2 +- src/types.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/geolonia-map.ts b/src/lib/geolonia-map.ts index ab781445..cce28995 100644 --- a/src/lib/geolonia-map.ts +++ b/src/lib/geolonia-map.ts @@ -168,7 +168,7 @@ export default class GeoloniaMap extends maplibregl.Map { ) ) { const pathParts = transformedUrlObj.pathname.split('/'); - pathParts[1] = atts.stage; + pathParts[1] = String(atts.stage); transformedUrlObj.pathname = pathParts.join('/'); transformedUrlObj.searchParams.set('key', atts.key); return { diff --git a/src/types.ts b/src/types.ts index 1f7cf1d7..c50d5fc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,11 @@ import type GeoloniaMap from './lib/geolonia-map'; export type EmbedAttributes = { - lat: string; - lng: string; - zoom: string; - bearing: string; - pitch: string; + lat: string | number; + lng: string | number; + zoom: string | number; + bearing: string | number; + pitch: string | number; hash: string; marker: string; markerColor: string; @@ -27,10 +27,10 @@ export type EmbedAttributes = { key: string; apiUrl: string; loader: string; - minZoom: string; - maxZoom: string; + minZoom: string | number; + maxZoom: string | number; '3d': string; - [otherKey: string]: string; + [otherKey: string]: string | number; }; export type EmbedPlugin< From b25cccd830341fe2b81c21e9f808f0e92f4e1a0f Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:34:22 +0900 Subject: [PATCH 6/7] (refs #463) fixed race condition on webpack --- webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index d9e9de62..e62be9a6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,6 +38,7 @@ const sharedConfig = { }; const embedConfig = { + name: 'embed', ...sharedConfig, entry: './src/embed.ts', output: { @@ -54,6 +55,8 @@ const embedConfig = { }; const embedCoreConfig = { + name: 'embed-core', + dependencies: ['embed'], ...sharedConfig, entry: './src/embed-core.ts', output: { From ebe5fbf410bf3c91bbe0bb284dc74e6bdc65dc59 Mon Sep 17 00:00:00 2001 From: niryuu Date: Tue, 10 Mar 2026 15:45:12 +0900 Subject: [PATCH 7/7] (refs #463) fixed cursor state on remove --- src/lib/simplestyle.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/simplestyle.ts b/src/lib/simplestyle.ts index f7a723a5..0892d67c 100644 --- a/src/lib/simplestyle.ts +++ b/src/lib/simplestyle.ts @@ -401,6 +401,9 @@ export class SimpleStyle { this.map.removeSource(sourceId); } } + + this.map.getCanvas().style.cursor = ''; + return this; }