From b0e5aa778b1330e4bc50a93e8aca73299984af0b Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Thu, 13 Aug 2026 20:23:42 +1000 Subject: [PATCH] migrate bing streetside to typescript --- .../services/{streetside.js => streetside.ts} | 241 ++++++++++++------ modules/svg/{streetside.js => streetside.ts} | 60 +++-- 2 files changed, 193 insertions(+), 108 deletions(-) rename modules/services/{streetside.js => streetside.ts} (82%) rename modules/svg/{streetside.js => streetside.ts} (82%) diff --git a/modules/services/streetside.js b/modules/services/streetside.ts similarity index 82% rename from modules/services/streetside.js rename to modules/services/streetside.ts index 52c215f6daf..3b6e67f6a14 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { timer as d3_timer } from 'd3-timer'; @@ -16,10 +17,49 @@ import { import { utilAesDecrypt, utilArrayUnion, utilRebind, utilTiler, utilUniqueDomId } from '../util'; import { services } from './'; -import { searchLimited } from '../util/partition'; +import { searchLimited, type WithBbox } from '../util/partition'; import { localeTimestamp } from '../util/date'; import { patchHash } from '../behavior'; +import type { Projection } from '../geo/raw_mercator'; +import type { Tile } from '../util/tiler'; +import type { Vec2 } from '../geo/vector'; +import type { PhotoFramePhoto } from './pannellum_photo'; +import type { Feature, LineString } from 'geojson'; +import type { coreContext } from '../core'; + + +export interface Bubble extends Partial { + service: 'photo'; + key: string; + sequenceKey: string | null; + imageUrl: string; + loc: Vec2; + captured_by: string; + captured_at: Date | undefined; + pano: boolean; + vintageStart?: unknown; +} + +export type BubbleSequence = Feature; + +interface RawBubble { + imageUrl: string; + imageUrlSubdomains: string[]; + lat?: number; + vintageEnd: string; + lon?: number; + he?: number; + latitude: number; + longitude: number; + heading: number; +} + +interface BubbleResult { + resourceSets: { + resources: RawBubble[]; + }[]; +} const streetsideApi = 'https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}&uriScheme=https'; const maxResults = 500; @@ -36,9 +76,32 @@ const defaultHfov = 45; let _hires = false; let _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096 let _currScene = 0; -let _ssCache; -let _pannellumViewer; -let _sceneOptions = { +let _ssCache: { + bubbles: { + rtree: RBush>; + inflight: { + [tileId: string]: AbortController; + }; + loaded: { + [tileId: string]: true; + }; + nextPage: { + [tileId: string]: number; + }; + points: { + [bubbleId: string]: Bubble; + }; + }; + /** @deprecated -- not actually used */ + sequences: { + [sequenceId: string]: { + geojson: BubbleSequence; + bubbles: Bubble[]; + }; + }; +}; +let _pannellumViewer: Pannellum.Viewer; +let _sceneOptions: Pannellum.ConfigOptions = { showFullscreenCtrl: false, autoLoad: true, compass: true, @@ -47,22 +110,22 @@ let _sceneOptions = { maxHfov: maxHfov, hfov: defaultHfov, type: 'cubemap', - cubeMap: [] + cubeMap: [] as any, // defined later }; -let _loadViewerPromise; +let _loadViewerPromise: Promise | null; /** * abortRequest(). */ -function abortRequest(i) { +function abortRequest(i: AbortController) { i.abort(); } /** * loadTiles() wraps the process of generating tiles and then fetching image points for each tile. */ -function loadTiles(which, url, projection, margin) { +function loadTiles(which: 'bubbles', url: string, projection: Projection, margin: number) { const tiles = tiler.margin(margin).getTiles(projection); // abort inflight requests that are no longer needed @@ -82,7 +145,7 @@ function loadTiles(which, url, projection, margin) { /** * loadNextTilePage() load data for the next tile page in line. */ -function loadNextTilePage(which, url, tile) { +function loadNextTilePage(which: 'bubbles', url: string, tile: Omit) { const cache = _ssCache[which]; const nextPage = cache.nextPage[tile.id] || 0; const id = tile.id + ',' + String(nextPage); @@ -107,18 +170,18 @@ function loadNextTilePage(which, url, tile) { if (cache.points[bubbleId]) return null; // skip duplicates // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738 - const loc = [ + const loc: Vec2 = [ bubble.lon || bubble.longitude, bubble.lat || bubble.latitude ]; - const d = { + const d: Bubble = { service: 'photo', loc: loc, key: bubbleId, imageUrl: bubble.imageUrl .replace('{subdomain}', bubble.imageUrlSubdomains[0]), ca: bubble.he || bubble.heading, - captured_at: bubble.vintageEnd, + captured_at: bubble.vintageEnd ? new Date(bubble.vintageEnd) : undefined, captured_by: 'microsoft', pano: true, sequenceKey: null @@ -144,7 +207,7 @@ function loadNextTilePage(which, url, tile) { /** * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations). */ -function getBubbles(url, tile, callback) { +function getBubbles(url: string, tile: Omit, callback: (result: BubbleResult | null) => void) { let rect = tile.extent.rectangle(); const controller = new AbortController(); @@ -152,7 +215,7 @@ function getBubbles(url, tile, callback) { .then(key => url .replace('{key}', key) .replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(',')) - .replace('{count}', maxResults) + .replace('{count}', String(maxResults)) ) .then(url => fetch(url, { signal: controller.signal })) .then(function(response) { @@ -164,7 +227,7 @@ function getBubbles(url, tile, callback) { if (!result) { callback(null); } - return callback(result || []); + return callback(result || null); }).catch(function(err) { if (err.name === 'AbortError') { // ignore aborted requests, e.g. from duplicate requests while zooming/panning the map @@ -179,17 +242,17 @@ function getBubbles(url, tile, callback) { /** * loadImage() */ -function loadImage(imgInfo) { - return new Promise(resolve => { +function loadImage(imgInfo: Face) { + return new Promise<{ imgInfo: Face; status: 'ok' | 'error' }>(resolve => { let img = new Image(); img.onload = () => { - let canvas = document.getElementById('ideditor-canvas' + imgInfo.face); - let ctx = canvas.getContext('2d'); + let canvas = document.getElementById('ideditor-canvas' + imgInfo.face) as HTMLCanvasElement; + let ctx = canvas.getContext('2d')!; ctx.drawImage(img, imgInfo.x, imgInfo.y); resolve({ imgInfo: imgInfo, status: 'ok' }); }; img.onerror = () => { - resolve({ data: imgInfo, status: 'error' }); + resolve({ imgInfo, status: 'error' }); }; img.setAttribute('crossorigin', ''); img.src = imgInfo.url; @@ -200,28 +263,42 @@ function loadImage(imgInfo) { /** * loadCanvas() */ -function loadCanvas(imageGroup) { +function loadCanvas(imageGroup: Face[]) { return Promise.all(imageGroup.map(loadImage)) .then((data) => { - let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face); - const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 }; + let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face) as HTMLCanvasElement; + const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 } as const; let face = data[0].imgInfo.face; - _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0); + _sceneOptions.cubeMap![which[face]] = canvas.toDataURL('image/jpeg', 1.0); return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'}; }); } +enum FaceKey { + Front = '01', + Right = '02', + Back = '03', + Left = '10', + Up = '11', + Down = '12', +}; +interface Face { + face: FaceKey; + url: string; + x: number; + y: number; +} /** * loadFaces() */ -function loadFaces(faceGroup) { +function loadFaces(faceGroup: Face[][]) { return Promise.all(faceGroup.map(loadCanvas)) .then(() => { return { status: 'loadFaces done' }; }); } -function setupCanvas(selection, reset) { +function setupCanvas(selection: d3.Selection, reset?: boolean) { if (reset) { selection.selectAll('#ideditor-stitcher-canvases') .remove(); @@ -245,7 +322,7 @@ function setupCanvas(selection, reset) { } -function qkToXY(qk) { +function qkToXY(qk: string): Vec2 { let x = 0; let y = 0; let scale = 256; @@ -317,23 +394,24 @@ function getQuadKeys() { } +export default new class { + event!: Pick; -export default { /** * init() initialize streetside. */ - init: function() { + init() { if (!_ssCache) { this.reset(); } this.event = utilRebind(this, dispatch, 'on'); - }, + } /** * reset() reset the cache. */ - reset: function() { + reset() { if (_ssCache) { Object.values(_ssCache.bubbles.inflight).forEach(abortRequest); } @@ -342,29 +420,29 @@ export default { bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} }, sequences: {} }; - }, + } /** * bubbles() */ - bubbles: function(projection) { + bubbles(projection: Projection) { const limit = 5; return searchLimited(limit, projection, _ssCache.bubbles.rtree); - }, + } - cachedImage: function(imageKey) { + cachedImage(imageKey: string) { return _ssCache.bubbles.points[imageKey]; - }, + } - sequences: function(projection) { + sequences(projection: Projection) { const viewport = projection.clipExtent(); - const min = [viewport[0][0], viewport[1][1]]; - const max = [viewport[1][0], viewport[0][1]]; + const min: Vec2 = [viewport[0][0], viewport[1][1]]; + const max: Vec2 = [viewport[1][0], viewport[0][1]]; const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); - let seen = {}; - let results = []; + let seen: Record = {}; + let results: BubbleSequence[] = []; // all sequences for bubbles in viewport _ssCache.bubbles.rtree.search(bbox) @@ -377,47 +455,48 @@ export default { }); return results; - }, + } /** * loadBubbles() */ - loadBubbles: function(projection, margin) { + loadBubbles(projection: Projection, margin?: number) { // by default: request 2 nearby tiles so we can connect sequences. if (margin === undefined) margin = 2; loadTiles('bubbles', streetsideApi, projection, margin); - }, + } - viewer: function() { + viewer() { return _pannellumViewer; - }, + } - initViewer: function () { + initViewer() { if (!window.pannellum) return; if (_pannellumViewer) return; _currScene += 1; const sceneID = _currScene.toString(); - const options = { + + const options: Pannellum.TourOptions = { 'default': { firstScene: sceneID }, scenes: {} }; options.scenes[sceneID] = _sceneOptions; _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options); - }, + } - ensureViewerLoaded: function(context) { + ensureViewerLoaded(context: coreContext) { if (_loadViewerPromise) return _loadViewerPromise; // create ms-wrapper, a photo wrapper class - let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper') + let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper') .data([0]); // inject ms-wrapper into the photoviewer div @@ -485,7 +564,7 @@ export default { } }); - _loadViewerPromise = new Promise((resolve, reject) => { + _loadViewerPromise = new Promise((resolve, reject) => { let loadedCount = 0; function loaded() { @@ -529,7 +608,7 @@ export default { return _loadViewerPromise; - function step(stepBy) { + function step(stepBy: number) { return () => { let viewer = context.container().select('.photoviewer'); let selected = viewer.empty() ? undefined : viewer.datum(); @@ -542,19 +621,19 @@ export default { // construct a search trapezoid pointing out from current bubble const meters = 35; - let p1 = [ + let p1: Vec2 = [ origin[0] + geoMetersToLon(meters / 5, origin[1]), origin[1] ]; - let p2 = [ + let p2: Vec2 = [ origin[0] + geoMetersToLon(meters / 2, origin[1]), origin[1] + geoMetersToLat(meters) ]; - let p3 = [ + let p3: Vec2 = [ origin[0] - geoMetersToLon(meters / 2, origin[1]), origin[1] + geoMetersToLat(meters) ]; - let p4 = [ + let p4: Vec2 = [ origin[0] - geoMetersToLon(meters / 5, origin[1]), origin[1] ]; @@ -577,7 +656,7 @@ export default { if (!geoPointInPolygon(d.data.loc, poly)) return; let dist = geoVecLength(d.data.loc, selected.loc); - let theta = selected.ca - d.data.ca; + let theta = selected.ca - d.data.ca!; let minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta)); if (minTheta > 20) { dist += 5; // penalize distance if camera angles don't match @@ -599,26 +678,26 @@ export default { .showViewer(context); }; } - }, + } - yaw: function(yaw) { + yaw(yaw: number) { if (typeof yaw !== 'number') return yaw; _sceneOptions.yaw = yaw; return this; - }, + } /** * showViewer() */ - showViewer: function(context) { + showViewer(context: coreContext) { const wrap = context.container().select('.photoviewer'); const isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size(); if (isHidden) { for (const service of Object.values(services)) { if (service === this) continue; - if (typeof service.hideViewer === 'function') { + if (service && 'hideViewer' in service && typeof service.hideViewer === 'function') { service.hideViewer(context); } } @@ -629,13 +708,13 @@ export default { } return this; - }, + } /** * hideViewer() */ - hideViewer: function (context) { + hideViewer(context: coreContext) { let viewer = context.container().select('.photoviewer'); if (!viewer.empty()) viewer.datum(null); @@ -650,13 +729,13 @@ export default { patchHash({ photo: null }); return this.setStyles(context, null, true); - }, + } /** * selectImage(). */ - selectImage: function (context, key) { + selectImage(context: coreContext, key: string) { let that = this; let d = this.cachedImage(key); @@ -666,7 +745,7 @@ export default { this.setStyles(context, null, true); - let wrap = context.container().select('.photoviewer .ms-wrapper'); + let wrap = context.container().select('.photoviewer .ms-wrapper'); let attribution = wrap.selectAll('.photo-attribution').html(''); wrap.selectAll('.pnlm-load-box') // display "loading.." @@ -759,12 +838,12 @@ export default { .call(t.append('streetside.report')); // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12 - const faceKeys = ['01','02','03','10','11','12']; + const faceKeys = [FaceKey.Front, FaceKey.Right, FaceKey.Back, FaceKey.Left, FaceKey.Up, FaceKey.Down]; // Map images to cube faces let quadKeys = getQuadKeys(); let faces = faceKeys.map((faceKey) => { - return quadKeys.map((quadKey) => { + return quadKeys.map((quadKey): Face => { const xy = qkToXY(quadKey); return { face: faceKey, @@ -800,18 +879,18 @@ export default { }); return this; - }, + } - getSequenceKeyForBubble: function(d) { + getSequenceKeyForBubble(d: Bubble | undefined | null) { return d && d.sequenceKey; - }, + } // Updates the currently highlighted sequence and selected bubble. // Reset is only necessary when interacting with the viewport because // this implicitly changes the currently selected bubble/sequence - setStyles: function (context, hovered, reset) { + setStyles(context: coreContext, hovered?: Bubble | null, reset?: boolean) { if (reset) { // reset all layers context.container().selectAll('.viewfield-group') .classed('highlighted', false) @@ -838,21 +917,21 @@ export default { // highlight sibling viewfields on either the selected or the hovered sequences let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys); - context.container().selectAll('.layer-streetside-images .viewfield-group') + context.container().selectAll('.layer-streetside-images .viewfield-group') .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1) .classed('hovered', d => d.key === hoveredBubbleKey) .classed('currentView', d => d.key === selectedBubbleKey); - context.container().selectAll('.layer-streetside-images .sequence') + context.container().selectAll('.layer-streetside-images .sequence') .classed('highlighted', d => d.properties.key === hoveredSequenceKey) .classed('currentView', d => d.properties.key === selectedSequenceKey); // update viewfields if needed - context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield') + context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield') .attr('d', viewfieldPath); - function viewfieldPath() { - let d = this.parentNode.__data__; + function viewfieldPath(this: SVGPathElement) { + let d = this.parentNode!.__data__; if (d.pano && d.key !== selectedBubbleKey) { return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; } else { @@ -861,13 +940,13 @@ export default { } return this; - }, + } /** * cache(). */ - cache: function () { + cache() { return _ssCache; } }; diff --git a/modules/svg/streetside.js b/modules/svg/streetside.ts similarity index 82% rename from modules/svg/streetside.js rename to modules/svg/streetside.ts index 6730f6e5a59..a39c94e1bc7 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.ts @@ -1,18 +1,22 @@ import { throttle } from 'es-toolkit'; import { select as d3_select } from 'd3-selection'; +import type { Dispatch } from 'd3-dispatch'; import { svgPath, svgPointTransform } from './helpers'; import { services } from '../services'; +import type { Projection } from '../geo/raw_mercator'; +import type { coreContext } from '../core'; +import type { Bubble, BubbleSequence } from '../services/streetside'; -export function svgStreetside(projection, context, dispatch) { +export function svgStreetside(projection: Projection, context: coreContext, dispatch: Dispatch) { var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000); var minZoom = 14; var minMarkerZoom = 16; var minViewfieldZoom = 18; - var layer = d3_select(null); + let layer: d3.Selection = d3_select(null!); var _viewerYaw = 0; - var _selectedSequence = null; - var _streetside; + var _selectedSequence: string | null = null; + var _streetside: typeof services.streetside | null; /** * init(). @@ -87,7 +91,7 @@ export function svgStreetside(projection, context, dispatch) { /** * click() Handles 'bubble' point click event. */ - function click(d3_event, d) { + function click(d3_event: MouseEvent, d: Bubble) { var service = getService(); if (!service) return; @@ -100,7 +104,7 @@ export function svgStreetside(projection, context, dispatch) { service .ensureViewerLoaded(context) .then(function() { - service + service! .selectImage(context, d.key) .yaw(_viewerYaw) .showViewer(context); @@ -112,7 +116,7 @@ export function svgStreetside(projection, context, dispatch) { /** * mouseover(). */ - function mouseover(d3_event, d) { + function mouseover(d3_event: MouseEvent, d: Bubble) { var service = getService(); if (service) service.setStyles(context, d); } @@ -128,9 +132,9 @@ export function svgStreetside(projection, context, dispatch) { /** * transform(). */ - function transform(d) { + function transform(d: Bubble) { var t = svgPointTransform(projection)(d); - var rot = d.ca + _viewerYaw; + var rot = d.ca! + _viewerYaw; if (rot) { t += ' rotate(' + Math.floor(rot) + ',0,0)'; } @@ -152,12 +156,12 @@ export function svgStreetside(projection, context, dispatch) { // e.g. during drags or easing. if (context.map().isTransformed()) return; - layer.selectAll('.viewfield-group.currentView') + layer.selectAll('.viewfield-group.currentView') .attr('transform', transform); } - function filterBubbles(bubbles, skipDateFilter = false) { + function filterBubbles(bubbles: Bubble[], skipDateFilter = false) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); var usernames = context.photos().usernames(); @@ -165,13 +169,13 @@ export function svgStreetside(projection, context, dispatch) { if (fromDate && !skipDateFilter) { var fromTimestamp = new Date(fromDate).getTime(); bubbles = bubbles.filter(function(bubble) { - return new Date(bubble.captured_at).getTime() >= fromTimestamp; + return bubble.captured_at && bubble.captured_at.getTime() >= fromTimestamp; }); } if (toDate && !skipDateFilter) { var toTimestamp = new Date(toDate).getTime(); bubbles = bubbles.filter(function(bubble) { - return new Date(bubble.captured_at).getTime() <= toTimestamp; + return bubble.captured_at && bubble.captured_at.getTime() <= toTimestamp; }); } if (usernames) { @@ -183,7 +187,7 @@ export function svgStreetside(projection, context, dispatch) { return bubbles; } - function filterSequences(sequences, skipDateFilter = false) { + function filterSequences(sequences: BubbleSequence[], skipDateFilter = false) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); var usernames = context.photos().usernames(); @@ -191,13 +195,13 @@ export function svgStreetside(projection, context, dispatch) { if (fromDate && !skipDateFilter) { var fromTimestamp = new Date(fromDate).getTime(); sequences = sequences.filter(function(sequences) { - return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp; + return sequences.properties.captured_at && sequences.properties.captured_at?.getTime() >= fromTimestamp; }); } if (toDate && !skipDateFilter) { var toTimestamp = new Date(toDate).getTime(); sequences = sequences.filter(function(sequences) { - return new Date(sequences.properties.captured_at).getTime() <= toTimestamp; + return sequences.properties.captured_at && sequences.properties.captured_at.getTime() <= toTimestamp; }); } if (usernames) { @@ -212,7 +216,7 @@ export function svgStreetside(projection, context, dispatch) { /** * update(). */ - function update() { + function update(this: any) { var viewer = context.container().select('.photoviewer'); var selected = viewer.empty() ? undefined : viewer.datum(); var z = ~~context.map().zoom(); @@ -220,8 +224,8 @@ export function svgStreetside(projection, context, dispatch) { var showViewfields = (z >= minViewfieldZoom); var service = getService(); - var sequences = []; - var bubbles = []; + var sequences: BubbleSequence[] = []; + var bubbles: Bubble[] = []; if (context.photos().showsPanoramic()) { sequences = (service ? service.sequences(projection) : []); @@ -230,7 +234,7 @@ export function svgStreetside(projection, context, dispatch) { bubbles = filterBubbles(bubbles); } - var traces = layer.selectAll('.sequences').selectAll('.sequence') + var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); dispatch.call('photoDatesChanged', this, 'streetside', [ @@ -249,7 +253,7 @@ export function svgStreetside(projection, context, dispatch) { .attr('d', svgPath(projection).geojson); - var groups = layer.selectAll('.markers').selectAll('.viewfield-group') + var groups = layer.selectAll('.markers').selectAll('.viewfield-group') .data(bubbles, function(d) { // force reenter once bubbles are attached to a sequence return d.key + (d.sequenceKey ? 'v1' : 'v0'); @@ -305,8 +309,8 @@ export function svgStreetside(projection, context, dispatch) { .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') .attr('d', viewfieldPath); - function viewfieldPath() { - var d = this.parentNode.__data__; + function viewfieldPath(this: SVGPathElement) { + var d = this.parentNode!.__data__; if (d.pano) { return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; } else { @@ -321,11 +325,11 @@ export function svgStreetside(projection, context, dispatch) { * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called. * 'svgStreetside()' is called from index.js */ - function drawImages(selection) { + function drawImages(this: any, selection: d3.Selection) { var enabled = svgStreetside.enabled; var service = getService(); - layer = selection.selectAll('.layer-streetside-images') + layer = selection.selectAll('.layer-streetside-images') .data(service ? [0] : []); layer.exit() @@ -365,7 +369,7 @@ export function svgStreetside(projection, context, dispatch) { /** * drawImages.enabled(). */ - drawImages.enabled = function(_) { + drawImages.enabled = function(_: boolean) { if (!arguments.length) return svgStreetside.enabled; svgStreetside.enabled = _; if (svgStreetside.enabled) { @@ -386,7 +390,7 @@ export function svgStreetside(projection, context, dispatch) { return !!getService(); }; - drawImages.rendered = function(zoom) { + drawImages.rendered = function(zoom: number) { return zoom >= minZoom; }; @@ -394,3 +398,5 @@ export function svgStreetside(projection, context, dispatch) { return drawImages; } +svgStreetside.enabled = false; +svgStreetside.initialized = false;