diff --git a/build/Main.d.ts b/build/Main.d.ts index e04c609..fb9d78a 100644 --- a/build/Main.d.ts +++ b/build/Main.d.ts @@ -3,6 +3,7 @@ export { LODControl } from './lod/LODControl'; export { LODRadial } from './lod/LODRadial'; export { LODFrustum } from './lod/LODFrustum'; export { LODRaycast } from './lod/LODRaycast'; +export { LODFrustumOrthographic } from './lod/LODFrustumOrthographic'; export { BingMapsProvider } from './providers/BingMapsProvider'; export { GoogleMapsProvider } from './providers/GoogleMapsProvider'; export { HereMapsProvider } from './providers/HereMapsProvider'; diff --git a/build/MapView.d.ts b/build/MapView.d.ts index af4b132..8c0c02a 100644 --- a/build/MapView.d.ts +++ b/build/MapView.d.ts @@ -14,7 +14,7 @@ export declare class MapView extends Mesh { heightProvider: MapProvider; root: MapNode; cacheTiles: boolean; - constructor(root?: (number | MapNode), provider?: MapProvider, heightProvider?: MapProvider); + constructor(root?: (number | MapNode), provider?: MapProvider, heightProvider?: MapProvider, lod?: LODControl); onBeforeRender: (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void; setRoot(root: (MapNode | number)): void; preSubdivide(): void; diff --git a/build/geo-three.cjs b/build/geo-three.cjs index 298b35e..e2b5dda 100644 --- a/build/geo-three.cjs +++ b/build/geo-three.cjs @@ -2,31 +2,31 @@ var three = require('three'); -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ -/* global Reflect, Promise */ - - -function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise */ + + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); } class MapProvider { @@ -40,6 +40,9 @@ class MapProvider { fetchTile(zoom, x, y) { return null; } + fetchTileBuffer(zoom, x, y) { + return Promise.resolve(null); + } getMetaData() { return __awaiter(this, void 0, void 0, function* () { }); } @@ -592,15 +595,33 @@ class MapHeightNode extends MapNode { return; } try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + let imageData; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } if (this.disposed) { return; } - const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); - const context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); } catch (e) { @@ -852,7 +873,7 @@ class MapHeightNodeShader extends MapHeightNode { #include // Calculate height of the title - vec4 _theight = texture2D(heightMap, vUv); + vec4 _theight = texture2D(heightMap, vMapUv); float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; vec3 _transformed = position + _height * normal; @@ -883,11 +904,25 @@ class MapHeightNodeShader extends MapHeightNode { return; } try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + let texture; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + texture = new three.Texture(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + texture = new three.Texture(image); + } if (this.disposed) { return; } - const texture = new three.Texture(image); texture.generateMipmaps = false; texture.format = three.RGBAFormat; texture.magFilter = three.NearestFilter; @@ -1379,11 +1414,21 @@ class MapMartiniHeightNode extends MapHeightNode { if (this.mapView.heightProvider === null) { throw new Error('GeoThree: MapView.heightProvider provider is null.'); } - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + yield this.processHeight(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + yield this.processHeight(image); } - this.processHeight(image); this.heightLoaded = true; this.nodeReady(); }); @@ -1395,7 +1440,7 @@ MapMartiniHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); MapMartiniHeightNode.tileSize = 256; class MapView extends three.Mesh { - constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null) { + constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null, lod = new LODRaycast()) { super(undefined, new three.MeshBasicMaterial({ transparent: true, opacity: 0.0, depthWrite: false, colorWrite: false })); this.lod = null; this.provider = null; @@ -1405,7 +1450,7 @@ class MapView extends three.Mesh { this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { this.lod.updateLOD(this, camera, renderer, scene); }; - this.lod = new LODRaycast(); + this.lod = lod; this.provider = provider; this.heightProvider = heightProvider; this.setRoot(root); @@ -1502,18 +1547,18 @@ MapView.mapModes = new Map([ [MapView.MARTINI, MapMartiniHeightNode] ]); -const pov$1 = new three.Vector3(); -const position$1 = new three.Vector3(); +const pov$2 = new three.Vector3(); +const position$2 = new three.Vector3(); class LODRadial { constructor(subdivideDistance = 50, simplifyDistance = 300) { this.subdivideDistance = subdivideDistance; this.simplifyDistance = simplifyDistance; } updateLOD(view, camera, renderer, scene) { - camera.getWorldPosition(pov$1); + camera.getWorldPosition(pov$2); view.children[0].traverse((node) => { - node.getWorldPosition(position$1); - let distance = pov$1.distanceTo(position$1); + node.getWorldPosition(position$2); + let distance = pov$2.distanceTo(position$2); distance /= Math.pow(2, view.provider.maxZoom - node.level); if (distance < this.subdivideDistance) { node.subdivide(); @@ -1525,10 +1570,10 @@ class LODRadial { } } -const projection = new three.Matrix4(); -const pov = new three.Vector3(); -const frustum = new three.Frustum(); -const position = new three.Vector3(); +const projection$1 = new three.Matrix4(); +const pov$1 = new three.Vector3(); +const frustum$1 = new three.Frustum(); +const position$1 = new three.Vector3(); class LODFrustum extends LODRadial { constructor(subdivideDistance = 120, simplifyDistance = 400) { super(subdivideDistance, simplifyDistance); @@ -1536,14 +1581,14 @@ class LODFrustum extends LODRadial { this.pointOnly = false; } updateLOD(view, camera, renderer, scene) { - projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); - frustum.setFromProjectionMatrix(projection); - camera.getWorldPosition(pov); + projection$1.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum$1.setFromProjectionMatrix(projection$1); + camera.getWorldPosition(pov$1); view.children[0].traverse((node) => { - node.getWorldPosition(position); - let distance = pov.distanceTo(position); + node.getWorldPosition(position$1); + let distance = pov$1.distanceTo(position$1); distance /= Math.pow(2, view.provider.maxZoom - node.level); - const inFrustum = this.pointOnly ? frustum.containsPoint(position) : frustum.intersectsObject(node); + const inFrustum = this.pointOnly ? frustum$1.containsPoint(position$1) : frustum$1.intersectsObject(node); if (distance < this.subdivideDistance && inFrustum) { node.subdivide(); } @@ -1554,6 +1599,57 @@ class LODFrustum extends LODRadial { } } +const projection = new three.Matrix4(); +const pov = new three.Vector3(); +const frustum = new three.Frustum(); +const position = new three.Vector3(); +const zoomLevelPixelRatios = [ + 78271.484, 39135.742, 19567.871, 9783.936, 4891.968, 2445.984, 1222.992, + 611.496, 305.748, 152.874, 76.437, 38.218, 19.109, 9.555, 4.777, 2.389, 1.194, + 0.597, 0.299, 0.149, 0.075, 0.037, 0.019 +]; +class LODFrustumOrthographic extends LODFrustum { + updateLOD(view, camera, renderer, scene) { + const isOrthographic = camera.isOrthographicCamera; + if (!isOrthographic) { + super.updateLOD(view, camera, renderer, scene); + return; + } + projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum.setFromProjectionMatrix(projection); + camera.getWorldPosition(pov); + view.children[0].traverse((obj) => { + var _a; + const node = obj; + node.getWorldPosition(position); + const nodeBox = new three.Box3().setFromObject(node); + let distance = nodeBox.distanceToPoint(pov); + distance /= Math.pow(2, (view.provider.maxZoom - node.level)); + const inFrustum = frustum.intersectsObject(node); + if (inFrustum) { + const metresPerPixel = 1 / camera.zoom; + let closestZoomLevel = 0; + let minDifference = Number.POSITIVE_INFINITY; + for (let i = 0; i < zoomLevelPixelRatios.length; i++) { + const difference = Math.abs(zoomLevelPixelRatios[i] - metresPerPixel); + if (difference < minDifference) { + minDifference = difference; + closestZoomLevel = i; + } + } + if (node.level < closestZoomLevel) { + if (!(node.children.length > 0)) { + node.subdivide(); + } + } + else if (node.level > closestZoomLevel) { + (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.simplify(); + } + } + }); + } +} + class XHRUtils { static get(url) { return __awaiter(this, void 0, void 0, function* () { @@ -1783,14 +1879,26 @@ class MapBoxProvider extends MapProvider { reject(); }; image.crossOrigin = 'Anonymous'; - if (this.mode === MapBoxProvider.STYLE) { - image.src = MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; - } - else { - image.src = MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; + image.src = this.getTileUrl(zoom, x, y); + }); + } + fetchTileBuffer(zoom, x, y) { + return __awaiter(this, void 0, void 0, function* () { + const response = yield fetch(this.getTileUrl(zoom, x, y)); + if (!response.ok) { + throw new Error('MapBoxProvider: Failed to fetch tile buffer. Status ' + response.status); } + return response.arrayBuffer(); }); } + getTileUrl(zoom, x, y) { + if (this.mode === MapBoxProvider.STYLE) { + return MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; + } + else { + return MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; + } + } } MapBoxProvider.ADDRESS = 'https://api.mapbox.com/'; MapBoxProvider.STYLE = 100; @@ -2011,6 +2119,7 @@ exports.GoogleMapsProvider = GoogleMapsProvider; exports.HeightDebugProvider = HeightDebugProvider; exports.HereMapsProvider = HereMapsProvider; exports.LODFrustum = LODFrustum; +exports.LODFrustumOrthographic = LODFrustumOrthographic; exports.LODRadial = LODRadial; exports.LODRaycast = LODRaycast; exports.MapBoxProvider = MapBoxProvider; diff --git a/build/geo-three.js b/build/geo-three.js index 40b090a..e0166a1 100644 --- a/build/geo-three.js +++ b/build/geo-three.js @@ -31,1976 +31,2084 @@ }); } - class MapProvider { - constructor() { - this.name = ''; - this.minZoom = 0; - this.maxZoom = 20; - this.bounds = []; - this.center = []; - } - fetchTile(zoom, x, y) { - return null; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { }); - } + class MapProvider { + constructor() { + this.name = ''; + this.minZoom = 0; + this.maxZoom = 20; + this.bounds = []; + this.center = []; + } + fetchTile(zoom, x, y) { + return null; + } + fetchTileBuffer(zoom, x, y) { + return Promise.resolve(null); + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { }); + } } - class OpenStreetMapsProvider extends MapProvider { - constructor(address = 'https://a.tile.openstreetmap.org/') { - super(); - this.address = address; - this.format = 'png'; - this.maxZoom = 19; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = this.address + zoom + '/' + x + '/' + y + '.' + this.format; - }); - } + class OpenStreetMapsProvider extends MapProvider { + constructor(address = 'https://a.tile.openstreetmap.org/') { + super(); + this.address = address; + this.format = 'png'; + this.maxZoom = 19; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.address + zoom + '/' + x + '/' + y + '.' + this.format; + }); + } } - class CanvasUtils { - static createOffscreenCanvas(width, height) { - if (typeof OffscreenCanvas !== 'undefined') { - return new OffscreenCanvas(width, height); - } - else { - let canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - } - } + class CanvasUtils { + static createOffscreenCanvas(width, height) { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(width, height); + } + else { + let canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + } } - class TextureUtils { - static createFillTexture(color = '#000000', width = 1, height = 1) { - const canvas = CanvasUtils.createOffscreenCanvas(width, height); - const context = canvas.getContext('2d'); - context.fillStyle = color; - context.fillRect(0, 0, width, height); - const texture = new three.Texture(canvas); - texture.format = three.RGBAFormat; - texture.magFilter = three.LinearFilter; - texture.minFilter = three.LinearFilter; - texture.generateMipmaps = false; - texture.needsUpdate = true; - return texture; - } + class TextureUtils { + static createFillTexture(color = '#000000', width = 1, height = 1) { + const canvas = CanvasUtils.createOffscreenCanvas(width, height); + const context = canvas.getContext('2d'); + context.fillStyle = color; + context.fillRect(0, 0, width, height); + const texture = new three.Texture(canvas); + texture.format = three.RGBAFormat; + texture.magFilter = three.LinearFilter; + texture.minFilter = three.LinearFilter; + texture.generateMipmaps = false; + texture.needsUpdate = true; + return texture; + } } - class QuadTreePosition { - } - QuadTreePosition.root = -1; - QuadTreePosition.topLeft = 0; - QuadTreePosition.topRight = 1; - QuadTreePosition.bottomLeft = 2; - QuadTreePosition.bottomRight = 3; - class MapNode extends three.Mesh { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = null, material = null) { - super(geometry, material); - this.mapView = null; - this.parentNode = null; - this.subdivided = false; - this.disposed = false; - this.nodesLoaded = 0; - this.childrenCache = null; - this.isMesh = true; - this.mapView = mapView; - this.parentNode = parentNode; - this.disposed = false; - this.location = location; - this.level = level; - this.x = x; - this.y = y; - this.initialize(); - } - initialize() { - return __awaiter(this, void 0, void 0, function* () { }); - } - createChildNodes() { } - subdivide() { - const maxZoom = this.mapView.maxZoom(); - if (this.children.length > 0 || this.level + 1 > maxZoom || this.parentNode !== null && this.parentNode.nodesLoaded < MapNode.childrens) { - return; - } - if (this.mapView.cacheTiles && this.childrenCache !== null) { - this.isMesh = false; - this.children = this.childrenCache; - this.nodesLoaded = this.childrenCache.length; - } - else { - this.createChildNodes(); - } - this.subdivided = true; - } - simplify() { - const minZoom = this.mapView.minZoom(); - if (this.level - 1 < minZoom) { - return; - } - if (this.mapView.cacheTiles) { - this.childrenCache = this.children; - } - else { - for (let i = 0; i < this.children.length; i++) { - this.children[i].dispose(); - } - } - this.subdivided = false; - this.isMesh = true; - this.children = []; - this.nodesLoaded = 0; - } - loadData() { - return __awaiter(this, void 0, void 0, function* () { - if (this.level < this.mapView.provider.minZoom || this.level > this.mapView.provider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.material.map = MapNode.defaultTexture; - this.material.needsUpdate = true; - return; - } - try { - const image = yield this.mapView.provider.fetchTile(this.level, this.x, this.y); - yield this.applyTexture(image); - } - catch (e) { - if (this.disposed) { - return; - } - console.warn('Geo-Three: Failed to load node tile data.', this); - this.material.map = MapNode.defaultTexture; - } - this.material.needsUpdate = true; - }); - } - applyTexture(image) { - return __awaiter(this, void 0, void 0, function* () { - if (this.disposed) { - return; - } - const texture = new three.Texture(image); - if (parseInt(three.REVISION) >= 152) { - texture.colorSpace = 'srgb'; - } - texture.generateMipmaps = false; - texture.format = three.RGBAFormat; - texture.magFilter = three.LinearFilter; - texture.minFilter = three.LinearFilter; - texture.needsUpdate = true; - this.material.map = texture; - }); - } - nodeReady() { - if (this.disposed) { - console.warn('Geo-Three: nodeReady() called for disposed node.', this); - this.dispose(); - return; - } - if (this.parentNode !== null) { - this.parentNode.nodesLoaded++; - if (this.parentNode.nodesLoaded === MapNode.childrens) { - if (this.parentNode.subdivided === true) { - this.parentNode.isMesh = false; - } - for (let i = 0; i < this.parentNode.children.length; i++) { - this.parentNode.children[i].visible = true; - } - } - if (this.parentNode.nodesLoaded > MapNode.childrens) { - console.error('Geo-Three: Loaded more children objects than expected.', this.parentNode.nodesLoaded, this); - } - } - else { - this.visible = true; - } - } - dispose() { - this.disposed = true; - const self = this; - try { - const material = self.material; - material.dispose(); - if (material.map && material.map !== MapNode.defaultTexture) { - material.map.dispose(); - } - } - catch (e) { } - try { - self.geometry.dispose(); - } - catch (e) { } - } - } - MapNode.defaultTexture = TextureUtils.createFillTexture(); - MapNode.baseGeometry = null; - MapNode.baseScale = null; + class QuadTreePosition { + } + QuadTreePosition.root = -1; + QuadTreePosition.topLeft = 0; + QuadTreePosition.topRight = 1; + QuadTreePosition.bottomLeft = 2; + QuadTreePosition.bottomRight = 3; + class MapNode extends three.Mesh { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = null, material = null) { + super(geometry, material); + this.mapView = null; + this.parentNode = null; + this.subdivided = false; + this.disposed = false; + this.nodesLoaded = 0; + this.childrenCache = null; + this.isMesh = true; + this.mapView = mapView; + this.parentNode = parentNode; + this.disposed = false; + this.location = location; + this.level = level; + this.x = x; + this.y = y; + this.initialize(); + } + initialize() { + return __awaiter(this, void 0, void 0, function* () { }); + } + createChildNodes() { } + subdivide() { + const maxZoom = this.mapView.maxZoom(); + if (this.children.length > 0 || this.level + 1 > maxZoom || this.parentNode !== null && this.parentNode.nodesLoaded < MapNode.childrens) { + return; + } + if (this.mapView.cacheTiles && this.childrenCache !== null) { + this.isMesh = false; + this.children = this.childrenCache; + this.nodesLoaded = this.childrenCache.length; + } + else { + this.createChildNodes(); + } + this.subdivided = true; + } + simplify() { + const minZoom = this.mapView.minZoom(); + if (this.level - 1 < minZoom) { + return; + } + if (this.mapView.cacheTiles) { + this.childrenCache = this.children; + } + else { + for (let i = 0; i < this.children.length; i++) { + this.children[i].dispose(); + } + } + this.subdivided = false; + this.isMesh = true; + this.children = []; + this.nodesLoaded = 0; + } + loadData() { + return __awaiter(this, void 0, void 0, function* () { + if (this.level < this.mapView.provider.minZoom || this.level > this.mapView.provider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.material.map = MapNode.defaultTexture; + this.material.needsUpdate = true; + return; + } + try { + const image = yield this.mapView.provider.fetchTile(this.level, this.x, this.y); + yield this.applyTexture(image); + } + catch (e) { + if (this.disposed) { + return; + } + console.warn('Geo-Three: Failed to load node tile data.', this); + this.material.map = MapNode.defaultTexture; + } + this.material.needsUpdate = true; + }); + } + applyTexture(image) { + return __awaiter(this, void 0, void 0, function* () { + if (this.disposed) { + return; + } + const texture = new three.Texture(image); + if (parseInt(three.REVISION) >= 152) { + texture.colorSpace = 'srgb'; + } + texture.generateMipmaps = false; + texture.format = three.RGBAFormat; + texture.magFilter = three.LinearFilter; + texture.minFilter = three.LinearFilter; + texture.needsUpdate = true; + this.material.map = texture; + }); + } + nodeReady() { + if (this.disposed) { + console.warn('Geo-Three: nodeReady() called for disposed node.', this); + this.dispose(); + return; + } + if (this.parentNode !== null) { + this.parentNode.nodesLoaded++; + if (this.parentNode.nodesLoaded === MapNode.childrens) { + if (this.parentNode.subdivided === true) { + this.parentNode.isMesh = false; + } + for (let i = 0; i < this.parentNode.children.length; i++) { + this.parentNode.children[i].visible = true; + } + } + if (this.parentNode.nodesLoaded > MapNode.childrens) { + console.error('Geo-Three: Loaded more children objects than expected.', this.parentNode.nodesLoaded, this); + } + } + else { + this.visible = true; + } + } + dispose() { + this.disposed = true; + const self = this; + try { + const material = self.material; + material.dispose(); + if (material.map && material.map !== MapNode.defaultTexture) { + material.map.dispose(); + } + } + catch (e) { } + try { + self.geometry.dispose(); + } + catch (e) { } + } + } + MapNode.defaultTexture = TextureUtils.createFillTexture(); + MapNode.baseGeometry = null; + MapNode.baseScale = null; MapNode.childrens = 4; - class MapNodeGeometry extends three.BufferGeometry { - constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0) { - super(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); - if (skirt) { - MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); - } - this.setIndex(indices); - this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); - } - static buildPlane(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, indices, vertices, normals, uvs) { - const widthHalf = width / 2; - const heightHalf = height / 2; - const gridX = widthSegments + 1; - const gridZ = heightSegments + 1; - const segmentWidth = width / widthSegments; - const segmentHeight = height / heightSegments; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - vertices.push(x, 0, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 1 - iz / heightSegments); - } - } - for (let iz = 0; iz < heightSegments; iz++) { - for (let ix = 0; ix < widthSegments; ix++) { - const a = ix + gridX * iz; - const b = ix + gridX * (iz + 1); - const c = ix + 1 + gridX * (iz + 1); - const d = ix + 1 + gridX * iz; - indices.push(a, b, d, b, c, d); - } - } - } - static buildSkirt(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirtDepth, indices, vertices, normals, uvs) { - const widthHalf = width / 2; - const heightHalf = height / 2; - const gridX = widthSegments + 1; - const gridZ = heightSegments + 1; - const segmentWidth = width / widthSegments; - const segmentHeight = height / heightSegments; - let start = vertices.length / 3; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - const z = -heightHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 1); - } - for (let ix = 0; ix < widthSegments; ix++) { - const a = ix; - const d = ix + 1; - const b = ix + start; - const c = ix + start + 1; - indices.push(d, b, a, d, c, b); - } - start = vertices.length / 3; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - const z = heightSegments * segmentHeight - heightHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 0); - } - let offset = gridX * gridZ - widthSegments - 1; - for (let ix = 0; ix < widthSegments; ix++) { - const a = offset + ix; - const d = offset + ix + 1; - const b = ix + start; - const c = ix + start + 1; - indices.push(a, b, d, b, c, d); - } - start = vertices.length / 3; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - const x = -widthHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(0, 1 - iz / heightSegments); - } - for (let iz = 0; iz < heightSegments; iz++) { - const a = iz * gridZ; - const d = (iz + 1) * gridZ; - const b = iz + start; - const c = iz + start + 1; - indices.push(a, b, d, b, c, d); - } - start = vertices.length / 3; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - const x = widthSegments * segmentWidth - widthHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(1.0, 1 - iz / heightSegments); - } - for (let iz = 0; iz < heightSegments; iz++) { - const a = iz * gridZ + heightSegments; - const d = (iz + 1) * gridZ + heightSegments; - const b = iz + start; - const c = iz + start + 1; - indices.push(d, b, a, d, c, b); - } - } + class MapNodeGeometry extends three.BufferGeometry { + constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0) { + super(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); + if (skirt) { + MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); + } + this.setIndex(indices); + this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); + } + static buildPlane(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, indices, vertices, normals, uvs) { + const widthHalf = width / 2; + const heightHalf = height / 2; + const gridX = widthSegments + 1; + const gridZ = heightSegments + 1; + const segmentWidth = width / widthSegments; + const segmentHeight = height / heightSegments; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + vertices.push(x, 0, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 1 - iz / heightSegments); + } + } + for (let iz = 0; iz < heightSegments; iz++) { + for (let ix = 0; ix < widthSegments; ix++) { + const a = ix + gridX * iz; + const b = ix + gridX * (iz + 1); + const c = ix + 1 + gridX * (iz + 1); + const d = ix + 1 + gridX * iz; + indices.push(a, b, d, b, c, d); + } + } + } + static buildSkirt(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirtDepth, indices, vertices, normals, uvs) { + const widthHalf = width / 2; + const heightHalf = height / 2; + const gridX = widthSegments + 1; + const gridZ = heightSegments + 1; + const segmentWidth = width / widthSegments; + const segmentHeight = height / heightSegments; + let start = vertices.length / 3; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + const z = -heightHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 1); + } + for (let ix = 0; ix < widthSegments; ix++) { + const a = ix; + const d = ix + 1; + const b = ix + start; + const c = ix + start + 1; + indices.push(d, b, a, d, c, b); + } + start = vertices.length / 3; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + const z = heightSegments * segmentHeight - heightHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 0); + } + let offset = gridX * gridZ - widthSegments - 1; + for (let ix = 0; ix < widthSegments; ix++) { + const a = offset + ix; + const d = offset + ix + 1; + const b = ix + start; + const c = ix + start + 1; + indices.push(a, b, d, b, c, d); + } + start = vertices.length / 3; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + const x = -widthHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(0, 1 - iz / heightSegments); + } + for (let iz = 0; iz < heightSegments; iz++) { + const a = iz * gridZ; + const d = (iz + 1) * gridZ; + const b = iz + start; + const c = iz + start + 1; + indices.push(a, b, d, b, c, d); + } + start = vertices.length / 3; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + const x = widthSegments * segmentWidth - widthHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(1.0, 1 - iz / heightSegments); + } + for (let iz = 0; iz < heightSegments; iz++) { + const a = iz * gridZ + heightSegments; + const d = (iz + 1) * gridZ + heightSegments; + const b = iz + start; + const c = iz + start + 1; + indices.push(d, b, a, d, c, b); + } + } } - class Geolocation { - constructor(latitude, longitude) { - this.latitude = latitude; - this.longitude = longitude; - } + class Geolocation { + constructor(latitude, longitude) { + this.latitude = latitude; + this.longitude = longitude; + } } - class UnitsUtils { - static datumsToSpherical(latitude, longitude) { - const x = longitude * UnitsUtils.EARTH_ORIGIN / 180.0; - let y = Math.log(Math.tan((90 + latitude) * Math.PI / 360.0)) / (Math.PI / 180.0); - y = y * UnitsUtils.EARTH_ORIGIN / 180.0; - return new three.Vector2(x, y); - } - static sphericalToDatums(x, y) { - const longitude = x / UnitsUtils.EARTH_ORIGIN * 180.0; - let latitude = y / UnitsUtils.EARTH_ORIGIN * 180.0; - latitude = 180.0 / Math.PI * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); - return new Geolocation(latitude, longitude); - } - static quadtreeToDatums(zoom, x, y) { - const n = Math.pow(2.0, zoom); - const longitude = x / n * 360.0 - 180.0; - const latitudeRad = Math.atan(Math.sinh(Math.PI * (1.0 - 2.0 * y / n))); - const latitude = 180.0 * (latitudeRad / Math.PI); - return new Geolocation(latitude, longitude); - } - static vectorToDatums(dir) { - const radToDeg = 180 / Math.PI; - const latitude = Math.atan2(dir.y, Math.sqrt(Math.pow(dir.x, 2) + Math.pow(-dir.z, 2))) * radToDeg; - const longitude = Math.atan2(-dir.z, dir.x) * radToDeg; - return new Geolocation(latitude, longitude); - } - static datumsToVector(latitude, longitude) { - const degToRad = Math.PI / 180; - const rotX = longitude * degToRad; - const rotY = latitude * degToRad; - var cos = Math.cos(rotY); - return new three.Vector3(-Math.cos(rotX + Math.PI) * cos, Math.sin(rotY), Math.sin(rotX + Math.PI) * cos); - } - static mapboxAltitude(color) { - return (color.r * 255.0 * 65536.0 + color.g * 255.0 * 256.0 + color.b * 255.0) * 0.1 - 10000.0; - } - static getTileSize(zoom) { - const maxExtent = UnitsUtils.WEB_MERCATOR_MAX_EXTENT; - const numTiles = Math.pow(2, zoom); - return 2 * maxExtent / numTiles; - } - static tileBounds(zoom, x, y) { - const tileSize = UnitsUtils.getTileSize(zoom); - const minX = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * tileSize; - const minY = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - (y + 1) * tileSize; - return [minX, tileSize, minY, tileSize]; - } - static webMercatorToLatitude(zoom, y) { - const yMerc = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - y * UnitsUtils.getTileSize(zoom); - return Math.atan(Math.sinh(yMerc / UnitsUtils.EARTH_RADIUS)); - } - static webMercatorToLongitude(zoom, x) { - const xMerc = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * UnitsUtils.getTileSize(zoom); - return xMerc / UnitsUtils.EARTH_RADIUS; - } - } - UnitsUtils.EARTH_RADIUS = 6371008; - UnitsUtils.EARTH_RADIUS_A = 6378137.0; - UnitsUtils.EARTH_RADIUS_B = 6356752.314245; - UnitsUtils.EARTH_PERIMETER = 2 * Math.PI * UnitsUtils.EARTH_RADIUS; - UnitsUtils.EARTH_ORIGIN = UnitsUtils.EARTH_PERIMETER / 2.0; + class UnitsUtils { + static datumsToSpherical(latitude, longitude) { + const x = longitude * UnitsUtils.EARTH_ORIGIN / 180.0; + let y = Math.log(Math.tan((90 + latitude) * Math.PI / 360.0)) / (Math.PI / 180.0); + y = y * UnitsUtils.EARTH_ORIGIN / 180.0; + return new three.Vector2(x, y); + } + static sphericalToDatums(x, y) { + const longitude = x / UnitsUtils.EARTH_ORIGIN * 180.0; + let latitude = y / UnitsUtils.EARTH_ORIGIN * 180.0; + latitude = 180.0 / Math.PI * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); + return new Geolocation(latitude, longitude); + } + static quadtreeToDatums(zoom, x, y) { + const n = Math.pow(2.0, zoom); + const longitude = x / n * 360.0 - 180.0; + const latitudeRad = Math.atan(Math.sinh(Math.PI * (1.0 - 2.0 * y / n))); + const latitude = 180.0 * (latitudeRad / Math.PI); + return new Geolocation(latitude, longitude); + } + static vectorToDatums(dir) { + const radToDeg = 180 / Math.PI; + const latitude = Math.atan2(dir.y, Math.sqrt(Math.pow(dir.x, 2) + Math.pow(-dir.z, 2))) * radToDeg; + const longitude = Math.atan2(-dir.z, dir.x) * radToDeg; + return new Geolocation(latitude, longitude); + } + static datumsToVector(latitude, longitude) { + const degToRad = Math.PI / 180; + const rotX = longitude * degToRad; + const rotY = latitude * degToRad; + var cos = Math.cos(rotY); + return new three.Vector3(-Math.cos(rotX + Math.PI) * cos, Math.sin(rotY), Math.sin(rotX + Math.PI) * cos); + } + static mapboxAltitude(color) { + return (color.r * 255.0 * 65536.0 + color.g * 255.0 * 256.0 + color.b * 255.0) * 0.1 - 10000.0; + } + static getTileSize(zoom) { + const maxExtent = UnitsUtils.WEB_MERCATOR_MAX_EXTENT; + const numTiles = Math.pow(2, zoom); + return 2 * maxExtent / numTiles; + } + static tileBounds(zoom, x, y) { + const tileSize = UnitsUtils.getTileSize(zoom); + const minX = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * tileSize; + const minY = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - (y + 1) * tileSize; + return [minX, tileSize, minY, tileSize]; + } + static webMercatorToLatitude(zoom, y) { + const yMerc = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - y * UnitsUtils.getTileSize(zoom); + return Math.atan(Math.sinh(yMerc / UnitsUtils.EARTH_RADIUS)); + } + static webMercatorToLongitude(zoom, x) { + const xMerc = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * UnitsUtils.getTileSize(zoom); + return xMerc / UnitsUtils.EARTH_RADIUS; + } + } + UnitsUtils.EARTH_RADIUS = 6371008; + UnitsUtils.EARTH_RADIUS_A = 6378137.0; + UnitsUtils.EARTH_RADIUS_B = 6356752.314245; + UnitsUtils.EARTH_PERIMETER = 2 * Math.PI * UnitsUtils.EARTH_RADIUS; + UnitsUtils.EARTH_ORIGIN = UnitsUtils.EARTH_PERIMETER / 2.0; UnitsUtils.WEB_MERCATOR_MAX_EXTENT = 20037508.34; - class MapPlaneNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - super(parentNode, mapView, location, level, x, y, MapPlaneNode.geometry, new three.MeshBasicMaterial({ wireframe: false })); - this.matrixAutoUpdate = false; - this.isMesh = true; - this.visible = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - this.nodeReady(); - }); - } - createChildNodes() { - const level = this.level + 1; - const x = this.x * 2; - const y = this.y * 2; - const Constructor = Object.getPrototypeOf(this).constructor; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } - } - MapPlaneNode.geometry = new MapNodeGeometry(1, 1, 1, 1, false); - MapPlaneNode.baseGeometry = MapPlaneNode.geometry; + class MapPlaneNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + super(parentNode, mapView, location, level, x, y, MapPlaneNode.geometry, new three.MeshBasicMaterial({ wireframe: false })); + this.matrixAutoUpdate = false; + this.isMesh = true; + this.visible = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + this.nodeReady(); + }); + } + createChildNodes() { + const level = this.level + 1; + const x = this.x * 2; + const y = this.y * 2; + const Constructor = Object.getPrototypeOf(this).constructor; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } + } + MapPlaneNode.geometry = new MapNodeGeometry(1, 1, 1, 1, false); + MapPlaneNode.baseGeometry = MapPlaneNode.geometry; MapPlaneNode.baseScale = new three.Vector3(UnitsUtils.EARTH_PERIMETER, 1.0, UnitsUtils.EARTH_PERIMETER); - class MapNodeHeightGeometry extends three.BufferGeometry { - constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0, imageData = null, calculateNormals = true) { - super(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); - const data = imageData.data; - for (let i = 0, j = 0; i < data.length && j < vertices.length; i += 4, j += 3) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; - vertices[j + 1] = value; - } - if (skirt) { - MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); - } - this.setIndex(indices); - this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); - if (calculateNormals) { - this.computeNormals(widthSegments, heightSegments); - } - } - computeNormals(widthSegments, heightSegments) { - const positionAttribute = this.getAttribute('position'); - if (positionAttribute !== undefined) { - let normalAttribute = this.getAttribute('normal'); - const normalLength = heightSegments * widthSegments; - for (let i = 0; i < normalLength; i++) { - normalAttribute.setXYZ(i, 0, 0, 0); - } - const pA = new three.Vector3(), pB = new three.Vector3(), pC = new three.Vector3(); - const nA = new three.Vector3(), nB = new three.Vector3(), nC = new three.Vector3(); - const cb = new three.Vector3(), ab = new three.Vector3(); - const indexLength = heightSegments * widthSegments * 6; - for (let i = 0; i < indexLength; i += 3) { - const vA = this.index.getX(i + 0); - const vB = this.index.getX(i + 1); - const vC = this.index.getX(i + 2); - pA.fromBufferAttribute(positionAttribute, vA); - pB.fromBufferAttribute(positionAttribute, vB); - pC.fromBufferAttribute(positionAttribute, vC); - cb.subVectors(pC, pB); - ab.subVectors(pA, pB); - cb.cross(ab); - nA.fromBufferAttribute(normalAttribute, vA); - nB.fromBufferAttribute(normalAttribute, vB); - nC.fromBufferAttribute(normalAttribute, vC); - nA.add(cb); - nB.add(cb); - nC.add(cb); - normalAttribute.setXYZ(vA, nA.x, nA.y, nA.z); - normalAttribute.setXYZ(vB, nB.x, nB.y, nB.z); - normalAttribute.setXYZ(vC, nC.x, nC.y, nC.z); - } - this.normalizeNormals(); - normalAttribute.needsUpdate = true; - } - } + class MapNodeHeightGeometry extends three.BufferGeometry { + constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0, imageData = null, calculateNormals = true) { + super(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); + const data = imageData.data; + for (let i = 0, j = 0; i < data.length && j < vertices.length; i += 4, j += 3) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; + vertices[j + 1] = value; + } + if (skirt) { + MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); + } + this.setIndex(indices); + this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); + if (calculateNormals) { + this.computeNormals(widthSegments, heightSegments); + } + } + computeNormals(widthSegments, heightSegments) { + const positionAttribute = this.getAttribute('position'); + if (positionAttribute !== undefined) { + let normalAttribute = this.getAttribute('normal'); + const normalLength = heightSegments * widthSegments; + for (let i = 0; i < normalLength; i++) { + normalAttribute.setXYZ(i, 0, 0, 0); + } + const pA = new three.Vector3(), pB = new three.Vector3(), pC = new three.Vector3(); + const nA = new three.Vector3(), nB = new three.Vector3(), nC = new three.Vector3(); + const cb = new three.Vector3(), ab = new three.Vector3(); + const indexLength = heightSegments * widthSegments * 6; + for (let i = 0; i < indexLength; i += 3) { + const vA = this.index.getX(i + 0); + const vB = this.index.getX(i + 1); + const vC = this.index.getX(i + 2); + pA.fromBufferAttribute(positionAttribute, vA); + pB.fromBufferAttribute(positionAttribute, vB); + pC.fromBufferAttribute(positionAttribute, vC); + cb.subVectors(pC, pB); + ab.subVectors(pA, pB); + cb.cross(ab); + nA.fromBufferAttribute(normalAttribute, vA); + nB.fromBufferAttribute(normalAttribute, vB); + nC.fromBufferAttribute(normalAttribute, vC); + nA.add(cb); + nB.add(cb); + nC.add(cb); + normalAttribute.setXYZ(vA, nA.x, nA.y, nA.z); + normalAttribute.setXYZ(vB, nB.x, nB.y, nB.z); + normalAttribute.setXYZ(vC, nC.x, nC.y, nC.z); + } + this.normalizeNormals(); + normalAttribute.needsUpdate = true; + } + } } - class MapHeightNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = MapHeightNode.geometry, material = new three.MeshPhongMaterial({ wireframe: false, color: 0xffffff })) { - super(parentNode, mapView, location, level, x, y, geometry, material); - this.heightLoaded = false; - this.textureLoaded = false; - this.geometrySize = 16; - this.geometryNormals = false; - this.isMesh = true; - this.visible = false; - this.matrixAutoUpdate = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - yield this.loadHeightGeometry(); - this.nodeReady(); - }); - } - loadData() { - const _super = Object.create(null, { - loadData: { get: () => super.loadData } - }); - return __awaiter(this, void 0, void 0, function* () { - yield _super.loadData.call(this); - this.textureLoaded = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.geometry = MapPlaneNode.baseGeometry; - return; - } - try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); - const context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); - } - catch (e) { - if (this.disposed) { - return; - } - this.geometry = MapPlaneNode.baseGeometry; - } - this.heightLoaded = true; - }); - } - createChildNodes() { - const level = this.level + 1; - const Constructor = Object.getPrototypeOf(this).constructor; - const x = this.x * 2; - const y = this.y * 2; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } - } - MapHeightNode.tileSize = 256; - MapHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); - MapHeightNode.baseGeometry = MapPlaneNode.geometry; + class MapHeightNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = MapHeightNode.geometry, material = new three.MeshPhongMaterial({ wireframe: false, color: 0xffffff })) { + super(parentNode, mapView, location, level, x, y, geometry, material); + this.heightLoaded = false; + this.textureLoaded = false; + this.geometrySize = 16; + this.geometryNormals = false; + this.isMesh = true; + this.visible = false; + this.matrixAutoUpdate = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + yield this.loadHeightGeometry(); + this.nodeReady(); + }); + } + loadData() { + const _super = Object.create(null, { + loadData: { get: () => super.loadData } + }); + return __awaiter(this, void 0, void 0, function* () { + yield _super.loadData.call(this); + this.textureLoaded = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.geometry = MapPlaneNode.baseGeometry; + return; + } + try { + let imageData; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + if (this.disposed) { + return; + } + this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); + } + catch (e) { + if (this.disposed) { + return; + } + this.geometry = MapPlaneNode.baseGeometry; + } + this.heightLoaded = true; + }); + } + createChildNodes() { + const level = this.level + 1; + const Constructor = Object.getPrototypeOf(this).constructor; + const x = this.x * 2; + const y = this.y * 2; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } + } + MapHeightNode.tileSize = 256; + MapHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); + MapHeightNode.baseGeometry = MapPlaneNode.geometry; MapHeightNode.baseScale = new three.Vector3(UnitsUtils.EARTH_PERIMETER, 1, UnitsUtils.EARTH_PERIMETER); - class MapSphereNodeGeometry extends three.BufferGeometry { - constructor(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength) { - super(); - const thetaEnd = thetaStart + thetaLength; - let index = 0; - const grid = []; - const vertex = new three.Vector3(); - const normal = new three.Vector3(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - for (let iy = 0; iy <= heightSegments; iy++) { - const verticesRow = []; - const v = iy / heightSegments; - for (let ix = 0; ix <= widthSegments; ix++) { - const u = ix / widthSegments; - vertex.x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); - vertex.y = radius * Math.cos(thetaStart + v * thetaLength); - vertex.z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); - vertices.push(vertex.x, vertex.y, vertex.z); - normal.set(vertex.x, vertex.y, vertex.z).normalize(); - normals.push(normal.x, normal.y, normal.z); - uvs.push(u, 1 - v); - verticesRow.push(index++); - } - grid.push(verticesRow); - } - for (let iy = 0; iy < heightSegments; iy++) { - for (let ix = 0; ix < widthSegments; ix++) { - const a = grid[iy][ix + 1]; - const b = grid[iy][ix]; - const c = grid[iy + 1][ix]; - const d = grid[iy + 1][ix + 1]; - if (iy !== 0 || thetaStart > 0) { - indices.push(a, b, d); - } - if (iy !== heightSegments - 1 || thetaEnd < Math.PI) { - indices.push(b, c, d); - } - } - } - this.setIndex(indices); - this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); - } + class MapSphereNodeGeometry extends three.BufferGeometry { + constructor(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength) { + super(); + const thetaEnd = thetaStart + thetaLength; + let index = 0; + const grid = []; + const vertex = new three.Vector3(); + const normal = new three.Vector3(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + for (let iy = 0; iy <= heightSegments; iy++) { + const verticesRow = []; + const v = iy / heightSegments; + for (let ix = 0; ix <= widthSegments; ix++) { + const u = ix / widthSegments; + vertex.x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); + vertex.y = radius * Math.cos(thetaStart + v * thetaLength); + vertex.z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); + vertices.push(vertex.x, vertex.y, vertex.z); + normal.set(vertex.x, vertex.y, vertex.z).normalize(); + normals.push(normal.x, normal.y, normal.z); + uvs.push(u, 1 - v); + verticesRow.push(index++); + } + grid.push(verticesRow); + } + for (let iy = 0; iy < heightSegments; iy++) { + for (let ix = 0; ix < widthSegments; ix++) { + const a = grid[iy][ix + 1]; + const b = grid[iy][ix]; + const c = grid[iy + 1][ix]; + const d = grid[iy + 1][ix + 1]; + if (iy !== 0 || thetaStart > 0) { + indices.push(a, b, d); + } + if (iy !== heightSegments - 1 || thetaEnd < Math.PI) { + indices.push(b, c, d); + } + } + } + this.setIndex(indices); + this.setAttribute('position', new three.Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new three.Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new three.Float32BufferAttribute(uvs, 2)); + } } - class MapSphereNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - let bounds = UnitsUtils.tileBounds(level, x, y); - const vertexShader = ` - varying vec3 vPosition; - - void main() { - vPosition = position; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `; - const fragmentShader = ` - #define PI 3.1415926538 - varying vec3 vPosition; - uniform sampler2D uTexture; - uniform vec4 webMercatorBounds; - - void main() { - // this could also be a constant, but for some reason using a constant causes more visible tile gaps at high zoom - float radius = length(vPosition); - - float latitude = asin(vPosition.y / radius); - float longitude = atan(-vPosition.z, vPosition.x); - - float web_mercator_x = radius * longitude; - float web_mercator_y = radius * log(tan(PI / 4.0 + latitude / 2.0)); - float y = (web_mercator_y - webMercatorBounds.z) / webMercatorBounds.w; - float x = (web_mercator_x - webMercatorBounds.x) / webMercatorBounds.y; - - vec4 color = texture2D(uTexture, vec2(x, y)); - gl_FragColor = color; - ${parseInt(three.REVISION) < 152 ? '' : ` - #include - #include ${parseInt(three.REVISION) >= 154 ? '' : ''} - `} - } - `; - let vBounds = new three.Vector4(...bounds); - const material = new three.ShaderMaterial({ - uniforms: { uTexture: { value: new three.Texture() }, webMercatorBounds: { value: vBounds } }, - vertexShader: vertexShader, - fragmentShader: fragmentShader - }); - super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), material); - this.applyScaleNode(); - this.matrixAutoUpdate = false; - this.isMesh = true; - this.visible = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - this.nodeReady(); - }); - } - static createGeometry(zoom, x, y) { - const range = Math.pow(2, zoom); - const max = 40; - const segments = Math.floor(MapSphereNode.segments * (max / (zoom + 1)) / max); - const lon1 = x > 0 ? UnitsUtils.webMercatorToLongitude(zoom, x) + Math.PI : 0; - const lon2 = x < range - 1 ? UnitsUtils.webMercatorToLongitude(zoom, x + 1) + Math.PI : 2 * Math.PI; - const phiStart = lon1; - const phiLength = lon2 - lon1; - const lat1 = y > 0 ? UnitsUtils.webMercatorToLatitude(zoom, y) : Math.PI / 2; - const lat2 = y < range - 1 ? UnitsUtils.webMercatorToLatitude(zoom, y + 1) : -Math.PI / 2; - const thetaLength = lat1 - lat2; - const thetaStart = Math.PI - (lat1 + Math.PI / 2); - return new MapSphereNodeGeometry(1, segments, segments, phiStart, phiLength, thetaStart, thetaLength); - } - applyTexture(image) { - return __awaiter(this, void 0, void 0, function* () { - const textureLoader = new three.TextureLoader(); - const texture = textureLoader.load(image.src, function () { - if (parseInt(three.REVISION) >= 152) { - texture.colorSpace = 'srgb'; - } - }); - this.material.uniforms.uTexture.value = texture; - this.material.uniforms.uTexture.needsUpdate = true; - }); - } - applyScaleNode() { - this.geometry.computeBoundingBox(); - const box = this.geometry.boundingBox.clone(); - const center = box.getCenter(new three.Vector3()); - const matrix = new three.Matrix4(); - matrix.compose(new three.Vector3(-center.x, -center.y, -center.z), new three.Quaternion(), new three.Vector3(UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS)); - this.geometry.applyMatrix4(matrix); - this.position.copy(center); - this.updateMatrix(); - this.updateMatrixWorld(); - } - updateMatrix() { - this.matrix.setPosition(this.position); - this.matrixWorldNeedsUpdate = true; - } - updateMatrixWorld(force = false) { - if (this.matrixWorldNeedsUpdate || force) { - this.matrixWorld.copy(this.matrix); - this.matrixWorldNeedsUpdate = false; - } - } - createChildNodes() { - const level = this.level + 1; - const x = this.x * 2; - const y = this.y * 2; - const Constructor = Object.getPrototypeOf(this).constructor; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - this.add(node); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } - } - MapSphereNode.baseGeometry = new MapSphereNodeGeometry(UnitsUtils.EARTH_RADIUS, 64, 64, 0, 2 * Math.PI, 0, Math.PI); - MapSphereNode.baseScale = new three.Vector3(1, 1, 1); + class MapSphereNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + let bounds = UnitsUtils.tileBounds(level, x, y); + const vertexShader = ` + varying vec3 vPosition; + + void main() { + vPosition = position; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `; + const fragmentShader = ` + #define PI 3.1415926538 + varying vec3 vPosition; + uniform sampler2D uTexture; + uniform vec4 webMercatorBounds; + + void main() { + // this could also be a constant, but for some reason using a constant causes more visible tile gaps at high zoom + float radius = length(vPosition); + + float latitude = asin(vPosition.y / radius); + float longitude = atan(-vPosition.z, vPosition.x); + + float web_mercator_x = radius * longitude; + float web_mercator_y = radius * log(tan(PI / 4.0 + latitude / 2.0)); + float y = (web_mercator_y - webMercatorBounds.z) / webMercatorBounds.w; + float x = (web_mercator_x - webMercatorBounds.x) / webMercatorBounds.y; + + vec4 color = texture2D(uTexture, vec2(x, y)); + gl_FragColor = color; + ${parseInt(three.REVISION) < 152 ? '' : ` + #include + #include ${parseInt(three.REVISION) >= 154 ? '' : ''} + `} + } + `; + let vBounds = new three.Vector4(...bounds); + const material = new three.ShaderMaterial({ + uniforms: { uTexture: { value: new three.Texture() }, webMercatorBounds: { value: vBounds } }, + vertexShader: vertexShader, + fragmentShader: fragmentShader + }); + super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), material); + this.applyScaleNode(); + this.matrixAutoUpdate = false; + this.isMesh = true; + this.visible = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + this.nodeReady(); + }); + } + static createGeometry(zoom, x, y) { + const range = Math.pow(2, zoom); + const max = 40; + const segments = Math.floor(MapSphereNode.segments * (max / (zoom + 1)) / max); + const lon1 = x > 0 ? UnitsUtils.webMercatorToLongitude(zoom, x) + Math.PI : 0; + const lon2 = x < range - 1 ? UnitsUtils.webMercatorToLongitude(zoom, x + 1) + Math.PI : 2 * Math.PI; + const phiStart = lon1; + const phiLength = lon2 - lon1; + const lat1 = y > 0 ? UnitsUtils.webMercatorToLatitude(zoom, y) : Math.PI / 2; + const lat2 = y < range - 1 ? UnitsUtils.webMercatorToLatitude(zoom, y + 1) : -Math.PI / 2; + const thetaLength = lat1 - lat2; + const thetaStart = Math.PI - (lat1 + Math.PI / 2); + return new MapSphereNodeGeometry(1, segments, segments, phiStart, phiLength, thetaStart, thetaLength); + } + applyTexture(image) { + return __awaiter(this, void 0, void 0, function* () { + const textureLoader = new three.TextureLoader(); + const texture = textureLoader.load(image.src, function () { + if (parseInt(three.REVISION) >= 152) { + texture.colorSpace = 'srgb'; + } + }); + this.material.uniforms.uTexture.value = texture; + this.material.uniforms.uTexture.needsUpdate = true; + }); + } + applyScaleNode() { + this.geometry.computeBoundingBox(); + const box = this.geometry.boundingBox.clone(); + const center = box.getCenter(new three.Vector3()); + const matrix = new three.Matrix4(); + matrix.compose(new three.Vector3(-center.x, -center.y, -center.z), new three.Quaternion(), new three.Vector3(UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS)); + this.geometry.applyMatrix4(matrix); + this.position.copy(center); + this.updateMatrix(); + this.updateMatrixWorld(); + } + updateMatrix() { + this.matrix.setPosition(this.position); + this.matrixWorldNeedsUpdate = true; + } + updateMatrixWorld(force = false) { + if (this.matrixWorldNeedsUpdate || force) { + this.matrixWorld.copy(this.matrix); + this.matrixWorldNeedsUpdate = false; + } + } + createChildNodes() { + const level = this.level + 1; + const x = this.x * 2; + const y = this.y * 2; + const Constructor = Object.getPrototypeOf(this).constructor; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + this.add(node); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } + } + MapSphereNode.baseGeometry = new MapSphereNodeGeometry(UnitsUtils.EARTH_RADIUS, 64, 64, 0, 2 * Math.PI, 0, Math.PI); + MapSphereNode.baseScale = new three.Vector3(1, 1, 1); MapSphereNode.segments = 80; - class MapHeightNodeShader extends MapHeightNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - const material = MapHeightNodeShader.prepareMaterial(new three.MeshPhongMaterial({ map: MapNode.defaultTexture, color: 0xFFFFFF })); - super(parentNode, mapView, location, level, x, y, MapHeightNodeShader.geometry, material); - this.frustumCulled = false; - } - static prepareMaterial(material) { - material.userData = { heightMap: { value: MapHeightNodeShader.defaultHeightTexture } }; - material.onBeforeCompile = (shader) => { - for (const i in material.userData) { - shader.uniforms[i] = material.userData[i]; - } - shader.vertexShader = - ` - uniform sampler2D heightMap; - ` + shader.vertexShader; - shader.vertexShader = shader.vertexShader.replace('#include ', ` - #include - - // Calculate height of the title - vec4 _theight = texture2D(heightMap, vUv); - float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; - vec3 _transformed = position + _height * normal; - - // Vertex position based on height - gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); - `); - }; - return material; - } - loadData() { - const _super = Object.create(null, { - loadData: { get: () => super.loadData } - }); - return __awaiter(this, void 0, void 0, function* () { - yield _super.loadData.call(this); - this.textureLoaded = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.material.map = MapHeightNodeShader.defaultTexture; - this.material.needsUpdate = true; - return; - } - try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - const texture = new three.Texture(image); - texture.generateMipmaps = false; - texture.format = three.RGBAFormat; - texture.magFilter = three.NearestFilter; - texture.minFilter = three.NearestFilter; - texture.needsUpdate = true; - this.material.userData.heightMap.value = texture; - } - catch (e) { - if (this.disposed) { - return; - } - console.error('Geo-Three: Failed to load node tile height data.', this); - this.material.userData.heightMap.value = MapHeightNodeShader.defaultHeightTexture; - } - this.material.needsUpdate = true; - this.heightLoaded = true; - }); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - this.geometry = MapPlaneNode.geometry; - super.raycast(raycaster, intersects); - this.geometry = MapHeightNodeShader.geometry; - } - } - dispose() { - super.dispose(); - if (this.material.userData.heightMap.value && this.material.userData.heightMap.value !== MapHeightNodeShader.defaultHeightTexture) { - this.material.userData.heightMap.value.dispose(); - } - } - } - MapHeightNodeShader.defaultHeightTexture = TextureUtils.createFillTexture('#0186C0'); - MapHeightNodeShader.geometrySize = 256; - MapHeightNodeShader.geometry = new MapNodeGeometry(1.0, 1.0, MapHeightNodeShader.geometrySize, MapHeightNodeShader.geometrySize, true); - MapHeightNodeShader.baseGeometry = MapPlaneNode.geometry; + class MapHeightNodeShader extends MapHeightNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + const material = MapHeightNodeShader.prepareMaterial(new three.MeshPhongMaterial({ map: MapNode.defaultTexture, color: 0xFFFFFF })); + super(parentNode, mapView, location, level, x, y, MapHeightNodeShader.geometry, material); + this.frustumCulled = false; + } + static prepareMaterial(material) { + material.userData = { heightMap: { value: MapHeightNodeShader.defaultHeightTexture } }; + material.onBeforeCompile = (shader) => { + for (const i in material.userData) { + shader.uniforms[i] = material.userData[i]; + } + shader.vertexShader = + ` + uniform sampler2D heightMap; + ` + shader.vertexShader; + shader.vertexShader = shader.vertexShader.replace('#include ', ` + #include + + // Calculate height of the title + vec4 _theight = texture2D(heightMap, vMapUv); + float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; + vec3 _transformed = position + _height * normal; + + // Vertex position based on height + gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); + `); + }; + return material; + } + loadData() { + const _super = Object.create(null, { + loadData: { get: () => super.loadData } + }); + return __awaiter(this, void 0, void 0, function* () { + yield _super.loadData.call(this); + this.textureLoaded = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.material.map = MapHeightNodeShader.defaultTexture; + this.material.needsUpdate = true; + return; + } + try { + let texture; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + texture = new three.Texture(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + texture = new three.Texture(image); + } + if (this.disposed) { + return; + } + texture.generateMipmaps = false; + texture.format = three.RGBAFormat; + texture.magFilter = three.NearestFilter; + texture.minFilter = three.NearestFilter; + texture.needsUpdate = true; + this.material.userData.heightMap.value = texture; + } + catch (e) { + if (this.disposed) { + return; + } + console.error('Geo-Three: Failed to load node tile height data.', this); + this.material.userData.heightMap.value = MapHeightNodeShader.defaultHeightTexture; + } + this.material.needsUpdate = true; + this.heightLoaded = true; + }); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + this.geometry = MapPlaneNode.geometry; + super.raycast(raycaster, intersects); + this.geometry = MapHeightNodeShader.geometry; + } + } + dispose() { + super.dispose(); + if (this.material.userData.heightMap.value && this.material.userData.heightMap.value !== MapHeightNodeShader.defaultHeightTexture) { + this.material.userData.heightMap.value.dispose(); + } + } + } + MapHeightNodeShader.defaultHeightTexture = TextureUtils.createFillTexture('#0186C0'); + MapHeightNodeShader.geometrySize = 256; + MapHeightNodeShader.geometry = new MapNodeGeometry(1.0, 1.0, MapHeightNodeShader.geometrySize, MapHeightNodeShader.geometrySize, true); + MapHeightNodeShader.baseGeometry = MapPlaneNode.geometry; MapHeightNodeShader.baseScale = new three.Vector3(UnitsUtils.EARTH_PERIMETER, 1, UnitsUtils.EARTH_PERIMETER); - class LODRaycast { - constructor() { - this.subdivisionRays = 1; - this.thresholdUp = 0.6; - this.thresholdDown = 0.15; - this.raycaster = new three.Raycaster(); - this.mouse = new three.Vector2(); - this.powerDistance = false; - this.scaleDistance = true; - } - updateLOD(view, camera, renderer, scene) { - const intersects = []; - for (let t = 0; t < this.subdivisionRays; t++) { - this.mouse.set(Math.random() * 2 - 1, Math.random() * 2 - 1); - this.raycaster.setFromCamera(this.mouse, camera); - let myIntersects = []; - this.raycaster.intersectObjects(view.children, true, myIntersects); - if (myIntersects.length > 0) { - intersects.push(myIntersects[0]); - } - } - for (let i = 0; i < intersects.length; i++) { - const node = intersects[i].object; - let distance = intersects[i].distance; - if (this.powerDistance) { - distance = Math.pow(distance * 2, node.level); - } - if (this.scaleDistance) { - const matrix = node.matrixWorld.elements; - const vector = new three.Vector3(matrix[0], matrix[1], matrix[2]); - distance = vector.length() / distance; - } - if (distance > this.thresholdUp) { - node.subdivide(); - } - else if (distance < this.thresholdDown && node.parentNode) { - node.parentNode.simplify(); - } - } - } + class LODRaycast { + constructor() { + this.subdivisionRays = 1; + this.thresholdUp = 0.6; + this.thresholdDown = 0.15; + this.raycaster = new three.Raycaster(); + this.mouse = new three.Vector2(); + this.powerDistance = false; + this.scaleDistance = true; + } + updateLOD(view, camera, renderer, scene) { + const intersects = []; + for (let t = 0; t < this.subdivisionRays; t++) { + this.mouse.set(Math.random() * 2 - 1, Math.random() * 2 - 1); + this.raycaster.setFromCamera(this.mouse, camera); + let myIntersects = []; + this.raycaster.intersectObjects(view.children, true, myIntersects); + if (myIntersects.length > 0) { + intersects.push(myIntersects[0]); + } + } + for (let i = 0; i < intersects.length; i++) { + const node = intersects[i].object; + let distance = intersects[i].distance; + if (this.powerDistance) { + distance = Math.pow(distance * 2, node.level); + } + if (this.scaleDistance) { + const matrix = node.matrixWorld.elements; + const vector = new three.Vector3(matrix[0], matrix[1], matrix[2]); + distance = vector.length() / distance; + } + if (distance > this.thresholdUp) { + node.subdivide(); + } + else if (distance < this.thresholdDown && node.parentNode) { + node.parentNode.simplify(); + } + } + } } - class Martini { - constructor(gridSize = 257) { - this.gridSize = gridSize; - const tileSize = gridSize - 1; - if (tileSize & tileSize - 1) { - throw new Error(`Expected grid size to be 2^n+1, got ${gridSize}.`); - } - this.numTriangles = tileSize * tileSize * 2 - 2; - this.numParentTriangles = this.numTriangles - tileSize * tileSize; - this.indices = new Uint32Array(this.gridSize * this.gridSize); - this.coords = new Uint16Array(this.numTriangles * 4); - for (let i = 0; i < this.numTriangles; i++) { - let id = i + 2; - let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; - if (id & 1) { - bx = by = cx = tileSize; - } - else { - ax = ay = cy = tileSize; - } - while ((id >>= 1) > 1) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (id & 1) { - bx = ax; - by = ay; - ax = cx; - ay = cy; - } - else { - ax = bx; - ay = by; - bx = cx; - by = cy; - } - cx = mx; - cy = my; - } - const k = i * 4; - this.coords[k + 0] = ax; - this.coords[k + 1] = ay; - this.coords[k + 2] = bx; - this.coords[k + 3] = by; - } - } - createTile(terrain) { - return new Tile(terrain, this); - } - } - class Tile { - constructor(terrain, martini) { - const size = martini.gridSize; - if (terrain.length !== size * size) { - throw new Error(`Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`); - } - this.terrain = terrain; - this.martini = martini; - this.errors = new Float32Array(terrain.length); - this.update(); - } - update() { - const { numTriangles, numParentTriangles, coords, gridSize: size } = this.martini; - const { terrain, errors } = this; - for (let i = numTriangles - 1; i >= 0; i--) { - const k = i * 4; - const ax = coords[k + 0]; - const ay = coords[k + 1]; - const bx = coords[k + 2]; - const by = coords[k + 3]; - const mx = ax + bx >> 1; - const my = ay + by >> 1; - const cx = mx + my - ay; - const cy = my + ax - mx; - const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2; - const middleIndex = my * size + mx; - const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]); - errors[middleIndex] = Math.max(errors[middleIndex], middleError); - if (i < numParentTriangles) { - const leftChildIndex = (ay + cy >> 1) * size + (ax + cx >> 1); - const rightChildIndex = (by + cy >> 1) * size + (bx + cx >> 1); - errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]); - } - } - } - getMesh(maxError = 0, withSkirts = false) { - const { gridSize: size, indices } = this.martini; - const { errors } = this; - let numVertices = 0; - let numTriangles = 0; - const max = size - 1; - let aIndex, bIndex, cIndex = 0; - const leftSkirtIndices = []; - const rightSkirtIndices = []; - const bottomSkirtIndices = []; - const topSkirtIndices = []; - indices.fill(0); - function countElements(ax, ay, bx, by, cx, cy) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { - countElements(cx, cy, ax, ay, mx, my); - countElements(bx, by, cx, cy, mx, my); - } - else { - aIndex = ay * size + ax; - bIndex = by * size + bx; - cIndex = cy * size + cx; - if (indices[aIndex] === 0) { - if (withSkirts) { - if (ax === 0) { - leftSkirtIndices.push(numVertices); - } - else if (ax === max) { - rightSkirtIndices.push(numVertices); - } - if (ay === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (ay === max) { - topSkirtIndices.push(numVertices); - } - } - indices[aIndex] = ++numVertices; - } - if (indices[bIndex] === 0) { - if (withSkirts) { - if (bx === 0) { - leftSkirtIndices.push(numVertices); - } - else if (bx === max) { - rightSkirtIndices.push(numVertices); - } - if (by === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (by === max) { - topSkirtIndices.push(numVertices); - } - } - indices[bIndex] = ++numVertices; - } - if (indices[cIndex] === 0) { - if (withSkirts) { - if (cx === 0) { - leftSkirtIndices.push(numVertices); - } - else if (cx === max) { - rightSkirtIndices.push(numVertices); - } - if (cy === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (cy === max) { - topSkirtIndices.push(numVertices); - } - } - indices[cIndex] = ++numVertices; - } - numTriangles++; - } - } - countElements(0, 0, max, max, max, 0); - countElements(max, max, 0, 0, 0, max); - let numTotalVertices = numVertices * 2; - let numTotalTriangles = numTriangles * 3; - if (withSkirts) { - numTotalVertices += (leftSkirtIndices.length + rightSkirtIndices.length + bottomSkirtIndices.length + topSkirtIndices.length) * 2; - numTotalTriangles += ((leftSkirtIndices.length - 1) * 2 + (rightSkirtIndices.length - 1) * 2 + (bottomSkirtIndices.length - 1) * 2 + (topSkirtIndices.length - 1) * 2) * 3; - } - const vertices = new Uint16Array(numTotalVertices); - const triangles = new Uint32Array(numTotalTriangles); - let triIndex = 0; - function processTriangle(ax, ay, bx, by, cx, cy) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { - processTriangle(cx, cy, ax, ay, mx, my); - processTriangle(bx, by, cx, cy, mx, my); - } - else { - const a = indices[ay * size + ax] - 1; - const b = indices[by * size + bx] - 1; - const c = indices[cy * size + cx] - 1; - vertices[2 * a] = ax; - vertices[2 * a + 1] = ay; - vertices[2 * b] = bx; - vertices[2 * b + 1] = by; - vertices[2 * c] = cx; - vertices[2 * c + 1] = cy; - triangles[triIndex++] = a; - triangles[triIndex++] = b; - triangles[triIndex++] = c; - } - } - processTriangle(0, 0, max, max, max, 0); - processTriangle(max, max, 0, 0, 0, max); - if (withSkirts) { - leftSkirtIndices.sort((a, b) => { return vertices[2 * a + 1] - vertices[2 * b + 1]; }); - rightSkirtIndices.sort((a, b) => { return vertices[2 * b + 1] - vertices[2 * a + 1]; }); - bottomSkirtIndices.sort((a, b) => { return vertices[2 * b] - vertices[2 * a]; }); - topSkirtIndices.sort((a, b) => { return vertices[2 * a] - vertices[2 * b]; }); - let skirtIndex = numVertices * 2; - function constructSkirt(skirt) { - const skirtLength = skirt.length; - for (let i = 0; i < skirtLength - 1; i++) { - const currIndex = skirt[i]; - const nextIndex = skirt[i + 1]; - const currentSkirt = skirtIndex / 2; - const nextSkirt = (skirtIndex + 2) / 2; - vertices[skirtIndex++] = vertices[2 * currIndex]; - vertices[skirtIndex++] = vertices[2 * currIndex + 1]; - triangles[triIndex++] = currIndex; - triangles[triIndex++] = currentSkirt; - triangles[triIndex++] = nextIndex; - triangles[triIndex++] = currentSkirt; - triangles[triIndex++] = nextSkirt; - triangles[triIndex++] = nextIndex; - } - vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1]]; - vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1] + 1]; - } - constructSkirt(leftSkirtIndices); - constructSkirt(rightSkirtIndices); - constructSkirt(bottomSkirtIndices); - constructSkirt(topSkirtIndices); - } - return { vertices: vertices, triangles: triangles, numVerticesWithoutSkirts: numVertices }; - } + class Martini { + constructor(gridSize = 257) { + this.gridSize = gridSize; + const tileSize = gridSize - 1; + if (tileSize & tileSize - 1) { + throw new Error(`Expected grid size to be 2^n+1, got ${gridSize}.`); + } + this.numTriangles = tileSize * tileSize * 2 - 2; + this.numParentTriangles = this.numTriangles - tileSize * tileSize; + this.indices = new Uint32Array(this.gridSize * this.gridSize); + this.coords = new Uint16Array(this.numTriangles * 4); + for (let i = 0; i < this.numTriangles; i++) { + let id = i + 2; + let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; + if (id & 1) { + bx = by = cx = tileSize; + } + else { + ax = ay = cy = tileSize; + } + while ((id >>= 1) > 1) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (id & 1) { + bx = ax; + by = ay; + ax = cx; + ay = cy; + } + else { + ax = bx; + ay = by; + bx = cx; + by = cy; + } + cx = mx; + cy = my; + } + const k = i * 4; + this.coords[k + 0] = ax; + this.coords[k + 1] = ay; + this.coords[k + 2] = bx; + this.coords[k + 3] = by; + } + } + createTile(terrain) { + return new Tile(terrain, this); + } + } + class Tile { + constructor(terrain, martini) { + const size = martini.gridSize; + if (terrain.length !== size * size) { + throw new Error(`Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`); + } + this.terrain = terrain; + this.martini = martini; + this.errors = new Float32Array(terrain.length); + this.update(); + } + update() { + const { numTriangles, numParentTriangles, coords, gridSize: size } = this.martini; + const { terrain, errors } = this; + for (let i = numTriangles - 1; i >= 0; i--) { + const k = i * 4; + const ax = coords[k + 0]; + const ay = coords[k + 1]; + const bx = coords[k + 2]; + const by = coords[k + 3]; + const mx = ax + bx >> 1; + const my = ay + by >> 1; + const cx = mx + my - ay; + const cy = my + ax - mx; + const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2; + const middleIndex = my * size + mx; + const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]); + errors[middleIndex] = Math.max(errors[middleIndex], middleError); + if (i < numParentTriangles) { + const leftChildIndex = (ay + cy >> 1) * size + (ax + cx >> 1); + const rightChildIndex = (by + cy >> 1) * size + (bx + cx >> 1); + errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]); + } + } + } + getMesh(maxError = 0, withSkirts = false) { + const { gridSize: size, indices } = this.martini; + const { errors } = this; + let numVertices = 0; + let numTriangles = 0; + const max = size - 1; + let aIndex, bIndex, cIndex = 0; + const leftSkirtIndices = []; + const rightSkirtIndices = []; + const bottomSkirtIndices = []; + const topSkirtIndices = []; + indices.fill(0); + function countElements(ax, ay, bx, by, cx, cy) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { + countElements(cx, cy, ax, ay, mx, my); + countElements(bx, by, cx, cy, mx, my); + } + else { + aIndex = ay * size + ax; + bIndex = by * size + bx; + cIndex = cy * size + cx; + if (indices[aIndex] === 0) { + if (withSkirts) { + if (ax === 0) { + leftSkirtIndices.push(numVertices); + } + else if (ax === max) { + rightSkirtIndices.push(numVertices); + } + if (ay === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (ay === max) { + topSkirtIndices.push(numVertices); + } + } + indices[aIndex] = ++numVertices; + } + if (indices[bIndex] === 0) { + if (withSkirts) { + if (bx === 0) { + leftSkirtIndices.push(numVertices); + } + else if (bx === max) { + rightSkirtIndices.push(numVertices); + } + if (by === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (by === max) { + topSkirtIndices.push(numVertices); + } + } + indices[bIndex] = ++numVertices; + } + if (indices[cIndex] === 0) { + if (withSkirts) { + if (cx === 0) { + leftSkirtIndices.push(numVertices); + } + else if (cx === max) { + rightSkirtIndices.push(numVertices); + } + if (cy === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (cy === max) { + topSkirtIndices.push(numVertices); + } + } + indices[cIndex] = ++numVertices; + } + numTriangles++; + } + } + countElements(0, 0, max, max, max, 0); + countElements(max, max, 0, 0, 0, max); + let numTotalVertices = numVertices * 2; + let numTotalTriangles = numTriangles * 3; + if (withSkirts) { + numTotalVertices += (leftSkirtIndices.length + rightSkirtIndices.length + bottomSkirtIndices.length + topSkirtIndices.length) * 2; + numTotalTriangles += ((leftSkirtIndices.length - 1) * 2 + (rightSkirtIndices.length - 1) * 2 + (bottomSkirtIndices.length - 1) * 2 + (topSkirtIndices.length - 1) * 2) * 3; + } + const vertices = new Uint16Array(numTotalVertices); + const triangles = new Uint32Array(numTotalTriangles); + let triIndex = 0; + function processTriangle(ax, ay, bx, by, cx, cy) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { + processTriangle(cx, cy, ax, ay, mx, my); + processTriangle(bx, by, cx, cy, mx, my); + } + else { + const a = indices[ay * size + ax] - 1; + const b = indices[by * size + bx] - 1; + const c = indices[cy * size + cx] - 1; + vertices[2 * a] = ax; + vertices[2 * a + 1] = ay; + vertices[2 * b] = bx; + vertices[2 * b + 1] = by; + vertices[2 * c] = cx; + vertices[2 * c + 1] = cy; + triangles[triIndex++] = a; + triangles[triIndex++] = b; + triangles[triIndex++] = c; + } + } + processTriangle(0, 0, max, max, max, 0); + processTriangle(max, max, 0, 0, 0, max); + if (withSkirts) { + leftSkirtIndices.sort((a, b) => { return vertices[2 * a + 1] - vertices[2 * b + 1]; }); + rightSkirtIndices.sort((a, b) => { return vertices[2 * b + 1] - vertices[2 * a + 1]; }); + bottomSkirtIndices.sort((a, b) => { return vertices[2 * b] - vertices[2 * a]; }); + topSkirtIndices.sort((a, b) => { return vertices[2 * a] - vertices[2 * b]; }); + let skirtIndex = numVertices * 2; + function constructSkirt(skirt) { + const skirtLength = skirt.length; + for (let i = 0; i < skirtLength - 1; i++) { + const currIndex = skirt[i]; + const nextIndex = skirt[i + 1]; + const currentSkirt = skirtIndex / 2; + const nextSkirt = (skirtIndex + 2) / 2; + vertices[skirtIndex++] = vertices[2 * currIndex]; + vertices[skirtIndex++] = vertices[2 * currIndex + 1]; + triangles[triIndex++] = currIndex; + triangles[triIndex++] = currentSkirt; + triangles[triIndex++] = nextIndex; + triangles[triIndex++] = currentSkirt; + triangles[triIndex++] = nextSkirt; + triangles[triIndex++] = nextIndex; + } + vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1]]; + vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1] + 1]; + } + constructSkirt(leftSkirtIndices); + constructSkirt(rightSkirtIndices); + constructSkirt(bottomSkirtIndices); + constructSkirt(topSkirtIndices); + } + return { vertices: vertices, triangles: triangles, numVerticesWithoutSkirts: numVertices }; + } } - class MapMartiniHeightNode extends MapHeightNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, { elevationDecoder = null, meshMaxError = 10, exageration = 1 } = {}) { - super(parentNode, mapView, location, level, x, y, MapMartiniHeightNode.geometry, MapMartiniHeightNode.prepareMaterial(new three.MeshPhongMaterial({ - map: MapMartiniHeightNode.emptyTexture, - color: 0xFFFFFF, - side: three.DoubleSide - }), level, exageration)); - this.elevationDecoder = { - rScaler: 256, - gScaler: 1, - bScaler: 1 / 256, - offset: -32768 - }; - this.exageration = 1.0; - this.meshMaxError = 10; - if (elevationDecoder) { - this.elevationDecoder = elevationDecoder; - } - this.meshMaxError = meshMaxError; - this.exageration = exageration; - this.frustumCulled = false; - } - static prepareMaterial(material, level, exageration = 1.0) { - material.userData = { - heightMap: { value: MapMartiniHeightNode.emptyTexture }, - drawNormals: { value: 0 }, - drawBlack: { value: 0 }, - zoomlevel: { value: level }, - computeNormals: { value: 1 }, - drawTexture: { value: 1 } - }; - material.onBeforeCompile = (shader) => { - for (let i in material.userData) { - shader.uniforms[i] = material.userData[i]; - } - shader.vertexShader = - ` - uniform bool computeNormals; - uniform float zoomlevel; - uniform sampler2D heightMap; - ` + shader.vertexShader; - shader.fragmentShader = - ` - uniform bool drawNormals; - uniform bool drawTexture; - uniform bool drawBlack; - ` + shader.fragmentShader; - shader.fragmentShader = shader.fragmentShader.replace('#include ', ` - if(drawBlack) { - gl_FragColor = vec4( 0.0,0.0,0.0, 1.0 ); - } else if(drawNormals) { - gl_FragColor = vec4( ( 0.5 * vNormal + 0.5 ), 1.0 ); - } else if (!drawTexture) { - gl_FragColor = vec4( 0.0,0.0,0.0, 0.0 ); - }`); - shader.vertexShader = shader.vertexShader.replace('#include ', ` - #include - - // queried pixels: - // +-----------+ - // | | | | - // | a | b | c | - // | | | | - // +-----------+ - // | | | | - // | d | e | f | - // | | | | - // +-----------+ - // | | | | - // | g | h | i | - // | | | | - // +-----------+ - - if (computeNormals) { - float e = getElevation(vUv, 0.0); - ivec2 size = textureSize(heightMap, 0); - float offset = 1.0 / float(size.x); - float a = getElevation(vUv + vec2(-offset, -offset), 0.0); - float b = getElevation(vUv + vec2(0, -offset), 0.0); - float c = getElevation(vUv + vec2(offset, -offset), 0.0); - float d = getElevation(vUv + vec2(-offset, 0), 0.0); - float f = getElevation(vUv + vec2(offset, 0), 0.0); - float g = getElevation(vUv + vec2(-offset, offset), 0.0); - float h = getElevation(vUv + vec2(0, offset), 0.0); - float i = getElevation(vUv + vec2(offset,offset), 0.0); - - - float normalLength = 500.0 / zoomlevel; - - vec3 v0 = vec3(0.0, 0.0, 0.0); - vec3 v1 = vec3(0.0, normalLength, 0.0); - vec3 v2 = vec3(normalLength, 0.0, 0.0); - v0.z = (e + d + g + h) / 4.0; - v1.z = (e+ b + a + d) / 4.0; - v2.z = (e+ h + i + f) / 4.0; - vNormal = (normalize(cross(v2 - v0, v1 - v0))).rbg; - } - `); - }; - return material; - } - static getTerrain(imageData, tileSize, elevation) { - const { rScaler, bScaler, gScaler, offset } = elevation; - const gridSize = tileSize + 1; - const terrain = new Float32Array(gridSize * gridSize); - for (let i = 0, y = 0; y < tileSize; y++) { - for (let x = 0; x < tileSize; x++, i++) { - const k = i * 4; - const r = imageData[k + 0]; - const g = imageData[k + 1]; - const b = imageData[k + 2]; - terrain[i + y] = r * rScaler + g * gScaler + b * bScaler + offset; - } - } - for (let i = gridSize * (gridSize - 1), x = 0; x < gridSize - 1; x++, i++) { - terrain[i] = terrain[i - gridSize]; - } - for (let i = gridSize - 1, y = 0; y < gridSize; y++, i += gridSize) { - terrain[i] = terrain[i - 1]; - } - return terrain; - } - static getMeshAttributes(vertices, terrain, tileSize, bounds, exageration) { - const gridSize = tileSize + 1; - const numOfVerticies = vertices.length / 2; - const positions = new Float32Array(numOfVerticies * 3); - const texCoords = new Float32Array(numOfVerticies * 2); - const [minX, minY, maxX, maxY] = bounds || [0, 0, tileSize, tileSize]; - const xScale = (maxX - minX) / tileSize; - const yScale = (maxY - minY) / tileSize; - for (let i = 0; i < numOfVerticies; i++) { - const x = vertices[i * 2]; - const y = vertices[i * 2 + 1]; - const pixelIdx = y * gridSize + x; - positions[3 * i + 0] = x * xScale + minX; - positions[3 * i + 1] = -terrain[pixelIdx] * exageration; - positions[3 * i + 2] = -y * yScale + maxY; - texCoords[2 * i + 0] = x / tileSize; - texCoords[2 * i + 1] = y / tileSize; - } - return { - position: { value: positions, size: 3 }, - uv: { value: texCoords, size: 2 } - }; - } - processHeight(image) { - return __awaiter(this, void 0, void 0, function* () { - const tileSize = image.width; - const gridSize = tileSize + 1; - var canvas = CanvasUtils.createOffscreenCanvas(tileSize, tileSize); - var context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, tileSize, tileSize, 0, 0, canvas.width, canvas.height); - var imageData = context.getImageData(0, 0, canvas.width, canvas.height); - var data = imageData.data; - const terrain = MapMartiniHeightNode.getTerrain(data, tileSize, this.elevationDecoder); - const martini = new Martini(gridSize); - const tile = martini.createTile(terrain); - const { vertices, triangles } = tile.getMesh(typeof this.meshMaxError === 'function' ? this.meshMaxError(this.level) : this.meshMaxError); - const attributes = MapMartiniHeightNode.getMeshAttributes(vertices, terrain, tileSize, [-0.5, -0.5, 0.5, 0.5], this.exageration); - this.geometry = new three.BufferGeometry(); - this.geometry.setIndex(new three.Uint32BufferAttribute(triangles, 1)); - this.geometry.setAttribute('position', new three.Float32BufferAttribute(attributes.position.value, attributes.position.size)); - this.geometry.setAttribute('uv', new three.Float32BufferAttribute(attributes.uv.value, attributes.uv.size)); - this.geometry.rotateX(Math.PI); - var texture = new three.Texture(image); - texture.generateMipmaps = false; - texture.format = three.RGBAFormat; - texture.magFilter = three.NearestFilter; - texture.minFilter = three.NearestFilter; - texture.needsUpdate = true; - this.material.userData.heightMap.value = texture; - this.material.map = texture; - this.material.needsUpdate = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - this.processHeight(image); - this.heightLoaded = true; - this.nodeReady(); - }); - } - } - MapMartiniHeightNode.geometrySize = 16; - MapMartiniHeightNode.emptyTexture = new three.Texture(); - MapMartiniHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); + class MapMartiniHeightNode extends MapHeightNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, { elevationDecoder = null, meshMaxError = 10, exageration = 1 } = {}) { + super(parentNode, mapView, location, level, x, y, MapMartiniHeightNode.geometry, MapMartiniHeightNode.prepareMaterial(new three.MeshPhongMaterial({ + map: MapMartiniHeightNode.emptyTexture, + color: 0xFFFFFF, + side: three.DoubleSide + }), level, exageration)); + this.elevationDecoder = { + rScaler: 256, + gScaler: 1, + bScaler: 1 / 256, + offset: -32768 + }; + this.exageration = 1.0; + this.meshMaxError = 10; + if (elevationDecoder) { + this.elevationDecoder = elevationDecoder; + } + this.meshMaxError = meshMaxError; + this.exageration = exageration; + this.frustumCulled = false; + } + static prepareMaterial(material, level, exageration = 1.0) { + material.userData = { + heightMap: { value: MapMartiniHeightNode.emptyTexture }, + drawNormals: { value: 0 }, + drawBlack: { value: 0 }, + zoomlevel: { value: level }, + computeNormals: { value: 1 }, + drawTexture: { value: 1 } + }; + material.onBeforeCompile = (shader) => { + for (let i in material.userData) { + shader.uniforms[i] = material.userData[i]; + } + shader.vertexShader = + ` + uniform bool computeNormals; + uniform float zoomlevel; + uniform sampler2D heightMap; + ` + shader.vertexShader; + shader.fragmentShader = + ` + uniform bool drawNormals; + uniform bool drawTexture; + uniform bool drawBlack; + ` + shader.fragmentShader; + shader.fragmentShader = shader.fragmentShader.replace('#include ', ` + if(drawBlack) { + gl_FragColor = vec4( 0.0,0.0,0.0, 1.0 ); + } else if(drawNormals) { + gl_FragColor = vec4( ( 0.5 * vNormal + 0.5 ), 1.0 ); + } else if (!drawTexture) { + gl_FragColor = vec4( 0.0,0.0,0.0, 0.0 ); + }`); + shader.vertexShader = shader.vertexShader.replace('#include ', ` + #include + + // queried pixels: + // +-----------+ + // | | | | + // | a | b | c | + // | | | | + // +-----------+ + // | | | | + // | d | e | f | + // | | | | + // +-----------+ + // | | | | + // | g | h | i | + // | | | | + // +-----------+ + + if (computeNormals) { + float e = getElevation(vUv, 0.0); + ivec2 size = textureSize(heightMap, 0); + float offset = 1.0 / float(size.x); + float a = getElevation(vUv + vec2(-offset, -offset), 0.0); + float b = getElevation(vUv + vec2(0, -offset), 0.0); + float c = getElevation(vUv + vec2(offset, -offset), 0.0); + float d = getElevation(vUv + vec2(-offset, 0), 0.0); + float f = getElevation(vUv + vec2(offset, 0), 0.0); + float g = getElevation(vUv + vec2(-offset, offset), 0.0); + float h = getElevation(vUv + vec2(0, offset), 0.0); + float i = getElevation(vUv + vec2(offset,offset), 0.0); + + + float normalLength = 500.0 / zoomlevel; + + vec3 v0 = vec3(0.0, 0.0, 0.0); + vec3 v1 = vec3(0.0, normalLength, 0.0); + vec3 v2 = vec3(normalLength, 0.0, 0.0); + v0.z = (e + d + g + h) / 4.0; + v1.z = (e+ b + a + d) / 4.0; + v2.z = (e+ h + i + f) / 4.0; + vNormal = (normalize(cross(v2 - v0, v1 - v0))).rbg; + } + `); + }; + return material; + } + static getTerrain(imageData, tileSize, elevation) { + const { rScaler, bScaler, gScaler, offset } = elevation; + const gridSize = tileSize + 1; + const terrain = new Float32Array(gridSize * gridSize); + for (let i = 0, y = 0; y < tileSize; y++) { + for (let x = 0; x < tileSize; x++, i++) { + const k = i * 4; + const r = imageData[k + 0]; + const g = imageData[k + 1]; + const b = imageData[k + 2]; + terrain[i + y] = r * rScaler + g * gScaler + b * bScaler + offset; + } + } + for (let i = gridSize * (gridSize - 1), x = 0; x < gridSize - 1; x++, i++) { + terrain[i] = terrain[i - gridSize]; + } + for (let i = gridSize - 1, y = 0; y < gridSize; y++, i += gridSize) { + terrain[i] = terrain[i - 1]; + } + return terrain; + } + static getMeshAttributes(vertices, terrain, tileSize, bounds, exageration) { + const gridSize = tileSize + 1; + const numOfVerticies = vertices.length / 2; + const positions = new Float32Array(numOfVerticies * 3); + const texCoords = new Float32Array(numOfVerticies * 2); + const [minX, minY, maxX, maxY] = bounds || [0, 0, tileSize, tileSize]; + const xScale = (maxX - minX) / tileSize; + const yScale = (maxY - minY) / tileSize; + for (let i = 0; i < numOfVerticies; i++) { + const x = vertices[i * 2]; + const y = vertices[i * 2 + 1]; + const pixelIdx = y * gridSize + x; + positions[3 * i + 0] = x * xScale + minX; + positions[3 * i + 1] = -terrain[pixelIdx] * exageration; + positions[3 * i + 2] = -y * yScale + maxY; + texCoords[2 * i + 0] = x / tileSize; + texCoords[2 * i + 1] = y / tileSize; + } + return { + position: { value: positions, size: 3 }, + uv: { value: texCoords, size: 2 } + }; + } + processHeight(image) { + return __awaiter(this, void 0, void 0, function* () { + const tileSize = image.width; + const gridSize = tileSize + 1; + var canvas = CanvasUtils.createOffscreenCanvas(tileSize, tileSize); + var context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, tileSize, tileSize, 0, 0, canvas.width, canvas.height); + var imageData = context.getImageData(0, 0, canvas.width, canvas.height); + var data = imageData.data; + const terrain = MapMartiniHeightNode.getTerrain(data, tileSize, this.elevationDecoder); + const martini = new Martini(gridSize); + const tile = martini.createTile(terrain); + const { vertices, triangles } = tile.getMesh(typeof this.meshMaxError === 'function' ? this.meshMaxError(this.level) : this.meshMaxError); + const attributes = MapMartiniHeightNode.getMeshAttributes(vertices, terrain, tileSize, [-0.5, -0.5, 0.5, 0.5], this.exageration); + this.geometry = new three.BufferGeometry(); + this.geometry.setIndex(new three.Uint32BufferAttribute(triangles, 1)); + this.geometry.setAttribute('position', new three.Float32BufferAttribute(attributes.position.value, attributes.position.size)); + this.geometry.setAttribute('uv', new three.Float32BufferAttribute(attributes.uv.value, attributes.uv.size)); + this.geometry.rotateX(Math.PI); + var texture = new three.Texture(image); + texture.generateMipmaps = false; + texture.format = three.RGBAFormat; + texture.magFilter = three.NearestFilter; + texture.minFilter = three.NearestFilter; + texture.needsUpdate = true; + this.material.userData.heightMap.value = texture; + this.material.map = texture; + this.material.needsUpdate = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + yield this.processHeight(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + yield this.processHeight(image); + } + this.heightLoaded = true; + this.nodeReady(); + }); + } + } + MapMartiniHeightNode.geometrySize = 16; + MapMartiniHeightNode.emptyTexture = new three.Texture(); + MapMartiniHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); MapMartiniHeightNode.tileSize = 256; - class MapView extends three.Mesh { - constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null) { - super(undefined, new three.MeshBasicMaterial({ transparent: true, opacity: 0.0, depthWrite: false, colorWrite: false })); - this.lod = null; - this.provider = null; - this.heightProvider = null; - this.root = null; - this.cacheTiles = false; - this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { - this.lod.updateLOD(this, camera, renderer, scene); - }; - this.lod = new LODRaycast(); - this.provider = provider; - this.heightProvider = heightProvider; - this.setRoot(root); - this.preSubdivide(); - } - setRoot(root) { - if (typeof root === 'number') { - if (!MapView.mapModes.has(root)) { - throw new Error('Map mode ' + root + ' does is not registered.'); - } - const rootConstructor = MapView.mapModes.get(root); - root = new rootConstructor(null, this); - } - if (this.root !== null) { - this.remove(this.root); - this.root = null; - } - this.root = root; - if (this.root !== null) { - this.geometry = this.root.constructor.baseGeometry; - this.scale.copy(this.root.constructor.baseScale); - this.root.mapView = this; - this.add(this.root); - this.root.initialize(); - } - } - preSubdivide() { - var _a, _b; - function subdivide(node, depth) { - if (depth <= 0) { - return; - } - node.subdivide(); - for (let i = 0; i < node.children.length; i++) { - if (node.children[i] instanceof MapNode) { - const child = node.children[i]; - subdivide(child, depth - 1); - } - } - } - const minZoom = Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); - if (minZoom > 0) { - subdivide(this.root, minZoom); - } - } - setProvider(provider) { - if (provider !== this.provider) { - this.provider = provider; - this.clear(); - } - } - setHeightProvider(heightProvider) { - if (heightProvider !== this.heightProvider) { - this.heightProvider = heightProvider; - this.clear(); - } - } - clear() { - this.traverse(function (children) { - if (children.childrenCache) { - children.childrenCache = null; - } - if (children.initialize) { - children.initialize(); - } - }); - return this; - } - minZoom() { - var _a, _b; - return Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); - } - maxZoom() { - var _a, _b; - return Math.min(this.provider.maxZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.maxZoom) !== null && _b !== void 0 ? _b : Infinity); - } - getMetaData() { - this.provider.getMetaData(); - } - raycast(raycaster, intersects) { - return false; - } - } - MapView.PLANAR = 200; - MapView.SPHERICAL = 201; - MapView.HEIGHT = 202; - MapView.HEIGHT_SHADER = 203; - MapView.MARTINI = 204; - MapView.mapModes = new Map([ - [MapView.PLANAR, MapPlaneNode], - [MapView.SPHERICAL, MapSphereNode], - [MapView.HEIGHT, MapHeightNode], - [MapView.HEIGHT_SHADER, MapHeightNodeShader], - [MapView.MARTINI, MapMartiniHeightNode] + class MapView extends three.Mesh { + constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null, lod = new LODRaycast()) { + super(undefined, new three.MeshBasicMaterial({ transparent: true, opacity: 0.0, depthWrite: false, colorWrite: false })); + this.lod = null; + this.provider = null; + this.heightProvider = null; + this.root = null; + this.cacheTiles = false; + this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { + this.lod.updateLOD(this, camera, renderer, scene); + }; + this.lod = lod; + this.provider = provider; + this.heightProvider = heightProvider; + this.setRoot(root); + this.preSubdivide(); + } + setRoot(root) { + if (typeof root === 'number') { + if (!MapView.mapModes.has(root)) { + throw new Error('Map mode ' + root + ' does is not registered.'); + } + const rootConstructor = MapView.mapModes.get(root); + root = new rootConstructor(null, this); + } + if (this.root !== null) { + this.remove(this.root); + this.root = null; + } + this.root = root; + if (this.root !== null) { + this.geometry = this.root.constructor.baseGeometry; + this.scale.copy(this.root.constructor.baseScale); + this.root.mapView = this; + this.add(this.root); + this.root.initialize(); + } + } + preSubdivide() { + var _a, _b; + function subdivide(node, depth) { + if (depth <= 0) { + return; + } + node.subdivide(); + for (let i = 0; i < node.children.length; i++) { + if (node.children[i] instanceof MapNode) { + const child = node.children[i]; + subdivide(child, depth - 1); + } + } + } + const minZoom = Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); + if (minZoom > 0) { + subdivide(this.root, minZoom); + } + } + setProvider(provider) { + if (provider !== this.provider) { + this.provider = provider; + this.clear(); + } + } + setHeightProvider(heightProvider) { + if (heightProvider !== this.heightProvider) { + this.heightProvider = heightProvider; + this.clear(); + } + } + clear() { + this.traverse(function (children) { + if (children.childrenCache) { + children.childrenCache = null; + } + if (children.initialize) { + children.initialize(); + } + }); + return this; + } + minZoom() { + var _a, _b; + return Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); + } + maxZoom() { + var _a, _b; + return Math.min(this.provider.maxZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.maxZoom) !== null && _b !== void 0 ? _b : Infinity); + } + getMetaData() { + this.provider.getMetaData(); + } + raycast(raycaster, intersects) { + return false; + } + } + MapView.PLANAR = 200; + MapView.SPHERICAL = 201; + MapView.HEIGHT = 202; + MapView.HEIGHT_SHADER = 203; + MapView.MARTINI = 204; + MapView.mapModes = new Map([ + [MapView.PLANAR, MapPlaneNode], + [MapView.SPHERICAL, MapSphereNode], + [MapView.HEIGHT, MapHeightNode], + [MapView.HEIGHT_SHADER, MapHeightNodeShader], + [MapView.MARTINI, MapMartiniHeightNode] ]); - const pov$1 = new three.Vector3(); - const position$1 = new three.Vector3(); - class LODRadial { - constructor(subdivideDistance = 50, simplifyDistance = 300) { - this.subdivideDistance = subdivideDistance; - this.simplifyDistance = simplifyDistance; - } - updateLOD(view, camera, renderer, scene) { - camera.getWorldPosition(pov$1); - view.children[0].traverse((node) => { - node.getWorldPosition(position$1); - let distance = pov$1.distanceTo(position$1); - distance /= Math.pow(2, view.provider.maxZoom - node.level); - if (distance < this.subdivideDistance) { - node.subdivide(); - } - else if (distance > this.simplifyDistance && node.parentNode) { - node.parentNode.simplify(); - } - }); - } + const pov$2 = new three.Vector3(); + const position$2 = new three.Vector3(); + class LODRadial { + constructor(subdivideDistance = 50, simplifyDistance = 300) { + this.subdivideDistance = subdivideDistance; + this.simplifyDistance = simplifyDistance; + } + updateLOD(view, camera, renderer, scene) { + camera.getWorldPosition(pov$2); + view.children[0].traverse((node) => { + node.getWorldPosition(position$2); + let distance = pov$2.distanceTo(position$2); + distance /= Math.pow(2, view.provider.maxZoom - node.level); + if (distance < this.subdivideDistance) { + node.subdivide(); + } + else if (distance > this.simplifyDistance && node.parentNode) { + node.parentNode.simplify(); + } + }); + } + } + + const projection$1 = new three.Matrix4(); + const pov$1 = new three.Vector3(); + const frustum$1 = new three.Frustum(); + const position$1 = new three.Vector3(); + class LODFrustum extends LODRadial { + constructor(subdivideDistance = 120, simplifyDistance = 400) { + super(subdivideDistance, simplifyDistance); + this.testCenter = true; + this.pointOnly = false; + } + updateLOD(view, camera, renderer, scene) { + projection$1.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum$1.setFromProjectionMatrix(projection$1); + camera.getWorldPosition(pov$1); + view.children[0].traverse((node) => { + node.getWorldPosition(position$1); + let distance = pov$1.distanceTo(position$1); + distance /= Math.pow(2, view.provider.maxZoom - node.level); + const inFrustum = this.pointOnly ? frustum$1.containsPoint(position$1) : frustum$1.intersectsObject(node); + if (distance < this.subdivideDistance && inFrustum) { + node.subdivide(); + } + else if (distance > this.simplifyDistance && node.parentNode) { + node.parentNode.simplify(); + } + }); + } } - const projection = new three.Matrix4(); - const pov = new three.Vector3(); - const frustum = new three.Frustum(); - const position = new three.Vector3(); - class LODFrustum extends LODRadial { - constructor(subdivideDistance = 120, simplifyDistance = 400) { - super(subdivideDistance, simplifyDistance); - this.testCenter = true; - this.pointOnly = false; - } - updateLOD(view, camera, renderer, scene) { - projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); - frustum.setFromProjectionMatrix(projection); - camera.getWorldPosition(pov); - view.children[0].traverse((node) => { - node.getWorldPosition(position); - let distance = pov.distanceTo(position); - distance /= Math.pow(2, view.provider.maxZoom - node.level); - const inFrustum = this.pointOnly ? frustum.containsPoint(position) : frustum.intersectsObject(node); - if (distance < this.subdivideDistance && inFrustum) { - node.subdivide(); - } - else if (distance > this.simplifyDistance && node.parentNode) { - node.parentNode.simplify(); - } - }); - } + const projection = new three.Matrix4(); + const pov = new three.Vector3(); + const frustum = new three.Frustum(); + const position = new three.Vector3(); + const zoomLevelPixelRatios = [ + 78271.484, 39135.742, 19567.871, 9783.936, 4891.968, 2445.984, 1222.992, + 611.496, 305.748, 152.874, 76.437, 38.218, 19.109, 9.555, 4.777, 2.389, 1.194, + 0.597, 0.299, 0.149, 0.075, 0.037, 0.019 + ]; + class LODFrustumOrthographic extends LODFrustum { + updateLOD(view, camera, renderer, scene) { + const isOrthographic = camera.isOrthographicCamera; + if (!isOrthographic) { + super.updateLOD(view, camera, renderer, scene); + return; + } + projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum.setFromProjectionMatrix(projection); + camera.getWorldPosition(pov); + view.children[0].traverse((obj) => { + var _a; + const node = obj; + node.getWorldPosition(position); + const nodeBox = new three.Box3().setFromObject(node); + let distance = nodeBox.distanceToPoint(pov); + distance /= Math.pow(2, (view.provider.maxZoom - node.level)); + const inFrustum = frustum.intersectsObject(node); + if (inFrustum) { + const metresPerPixel = 1 / camera.zoom; + let closestZoomLevel = 0; + let minDifference = Number.POSITIVE_INFINITY; + for (let i = 0; i < zoomLevelPixelRatios.length; i++) { + const difference = Math.abs(zoomLevelPixelRatios[i] - metresPerPixel); + if (difference < minDifference) { + minDifference = difference; + closestZoomLevel = i; + } + } + if (node.level < closestZoomLevel) { + if (!(node.children.length > 0)) { + node.subdivide(); + } + } + else if (node.level > closestZoomLevel) { + (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.simplify(); + } + } + }); + } } - class XHRUtils { - static get(url) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise(function (resolve, reject) { - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.open('GET', url, true); - xhr.onload = function () { - resolve(xhr.response); - }; - xhr.onerror = reject; - xhr.send(null); - }); - }); - } - static getRaw(url) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - xhr.responseType = 'arraybuffer'; - xhr.open('GET', url, true); - xhr.onload = function () { - resolve(xhr.response); - }; - xhr.onerror = reject; - xhr.send(null); - }); - }); - } - static request(url, type, header, body, onLoad, onError, onProgress) { - function parseResponse(response) { - try { - return JSON.parse(response); - } - catch (e) { - return response; - } - } - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.open(type, url, true); - if (header !== null && header !== undefined) { - for (const i in header) { - xhr.setRequestHeader(i, header[i]); - } - } - if (onLoad !== undefined) { - xhr.onload = function (event) { - onLoad(parseResponse(xhr.response), xhr); - }; - } - if (onError !== undefined) { - xhr.onerror = onError; - } - if (onProgress !== undefined) { - xhr.onprogress = onProgress; - } - xhr.send(body !== undefined ? body : null); - return xhr; - } + class XHRUtils { + static get(url) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise(function (resolve, reject) { + const xhr = new XMLHttpRequest(); + xhr.overrideMimeType('text/plain'); + xhr.open('GET', url, true); + xhr.onload = function () { + resolve(xhr.response); + }; + xhr.onerror = reject; + xhr.send(null); + }); + }); + } + static getRaw(url) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + xhr.open('GET', url, true); + xhr.onload = function () { + resolve(xhr.response); + }; + xhr.onerror = reject; + xhr.send(null); + }); + }); + } + static request(url, type, header, body, onLoad, onError, onProgress) { + function parseResponse(response) { + try { + return JSON.parse(response); + } + catch (e) { + return response; + } + } + const xhr = new XMLHttpRequest(); + xhr.overrideMimeType('text/plain'); + xhr.open(type, url, true); + if (header !== null && header !== undefined) { + for (const i in header) { + xhr.setRequestHeader(i, header[i]); + } + } + if (onLoad !== undefined) { + xhr.onload = function (event) { + onLoad(parseResponse(xhr.response), xhr); + }; + } + if (onError !== undefined) { + xhr.onerror = onError; + } + if (onProgress !== undefined) { + xhr.onprogress = onProgress; + } + xhr.send(body !== undefined ? body : null); + return xhr; + } } - class BingMapsProvider extends MapProvider { - constructor(apiKey = '', type = BingMapsProvider.AERIAL) { - super(); - this.maxZoom = 19; - this.minZoom = 1; - this.format = 'jpeg'; - this.mapSize = 512; - this.subdomain = 't1'; - this.meta = null; - this.apiKey = apiKey; - this.type = type; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = BingMapsProvider.ADDRESS + '/REST/V1/Imagery/Metadata/RoadOnDemand?output=json&include=ImageryProviders&key=' + this.apiKey; - const data = yield XHRUtils.get(address); - this.meta = JSON.parse(data); - }); - } - static quadKey(zoom, x, y) { - let quad = ''; - for (let i = zoom; i > 0; i--) { - const mask = 1 << i - 1; - let cell = 0; - if ((x & mask) !== 0) { - cell++; - } - if ((y & mask) !== 0) { - cell += 2; - } - quad += cell; - } - return quad; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'http://ecn.' + this.subdomain + '.tiles.virtualearth.net/tiles/' + this.type + BingMapsProvider.quadKey(zoom, x, y) + '.jpeg?g=1173'; - }); - } - } - BingMapsProvider.ADDRESS = 'https://dev.virtualearth.net'; - BingMapsProvider.AERIAL = 'a'; - BingMapsProvider.ROAD = 'r'; - BingMapsProvider.AERIAL_LABELS = 'h'; - BingMapsProvider.OBLIQUE = 'o'; + class BingMapsProvider extends MapProvider { + constructor(apiKey = '', type = BingMapsProvider.AERIAL) { + super(); + this.maxZoom = 19; + this.minZoom = 1; + this.format = 'jpeg'; + this.mapSize = 512; + this.subdomain = 't1'; + this.meta = null; + this.apiKey = apiKey; + this.type = type; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = BingMapsProvider.ADDRESS + '/REST/V1/Imagery/Metadata/RoadOnDemand?output=json&include=ImageryProviders&key=' + this.apiKey; + const data = yield XHRUtils.get(address); + this.meta = JSON.parse(data); + }); + } + static quadKey(zoom, x, y) { + let quad = ''; + for (let i = zoom; i > 0; i--) { + const mask = 1 << i - 1; + let cell = 0; + if ((x & mask) !== 0) { + cell++; + } + if ((y & mask) !== 0) { + cell += 2; + } + quad += cell; + } + return quad; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'http://ecn.' + this.subdomain + '.tiles.virtualearth.net/tiles/' + this.type + BingMapsProvider.quadKey(zoom, x, y) + '.jpeg?g=1173'; + }); + } + } + BingMapsProvider.ADDRESS = 'https://dev.virtualearth.net'; + BingMapsProvider.AERIAL = 'a'; + BingMapsProvider.ROAD = 'r'; + BingMapsProvider.AERIAL_LABELS = 'h'; + BingMapsProvider.OBLIQUE = 'o'; BingMapsProvider.OBLIQUE_LABELS = 'b'; - class GoogleMapsProvider extends MapProvider { - constructor(apiToken) { - super(); - this.sessionToken = null; - this.orientation = 0; - this.format = 'png'; - this.mapType = 'roadmap'; - this.overlay = false; - this.apiToken = apiToken !== undefined ? apiToken : ''; - this.createSession(); - } - createSession() { - const address = 'https://tile.googleapis.com/v1/createSession?key=' + this.apiToken; - const data = JSON.stringify({ - mapType: this.mapType, - language: 'en-EN', - region: 'en', - layerTypes: ['layerRoadmap', 'layerStreetview'], - overlay: this.overlay, - scale: 'scaleFactor1x' - }); - XHRUtils.request(address, 'POST', { 'Content-Type': 'text/json' }, data, (response, xhr) => { - this.sessionToken = response.session; - }, function (xhr) { - throw new Error('Unable to create a google maps session.'); - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://tile.googleapis.com/v1/2dtiles/' + zoom + '/' + x + '/' + y + '?session=' + this.sessionToken + '&orientation=' + this.orientation + '&key=' + this.apiToken; - }); - } + class GoogleMapsProvider extends MapProvider { + constructor(apiToken) { + super(); + this.sessionToken = null; + this.orientation = 0; + this.format = 'png'; + this.mapType = 'roadmap'; + this.overlay = false; + this.apiToken = apiToken !== undefined ? apiToken : ''; + this.createSession(); + } + createSession() { + const address = 'https://tile.googleapis.com/v1/createSession?key=' + this.apiToken; + const data = JSON.stringify({ + mapType: this.mapType, + language: 'en-EN', + region: 'en', + layerTypes: ['layerRoadmap', 'layerStreetview'], + overlay: this.overlay, + scale: 'scaleFactor1x' + }); + XHRUtils.request(address, 'POST', { 'Content-Type': 'text/json' }, data, (response, xhr) => { + this.sessionToken = response.session; + }, function (xhr) { + throw new Error('Unable to create a google maps session.'); + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://tile.googleapis.com/v1/2dtiles/' + zoom + '/' + x + '/' + y + '?session=' + this.sessionToken + '&orientation=' + this.orientation + '&key=' + this.apiToken; + }); + } } - class HereMapsProvider extends MapProvider { - constructor(appId = '', appCode = '', style = 'base', scheme = 'normal.day', format = 'png', size = 512) { - super(); - this.appId = appId; - this.appCode = appCode; - this.style = style; - this.scheme = scheme; - this.format = format; - this.size = size; - this.version = 'newest'; - this.server = 1; - } - nextServer() { - this.server = this.server % 4 === 0 ? 1 : this.server + 1; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { }); - } - fetchTile(zoom, x, y) { - this.nextServer(); - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://' + this.server + '.' + this.style + '.maps.api.here.com/maptile/2.1/maptile/' + - this.version + '/' + this.scheme + '/' + zoom + '/' + x + '/' + y + '/' + - this.size + '/' + this.format + '?app_id=' + this.appId + '&app_code=' + this.appCode; - }); - } - } + class HereMapsProvider extends MapProvider { + constructor(appId = '', appCode = '', style = 'base', scheme = 'normal.day', format = 'png', size = 512) { + super(); + this.appId = appId; + this.appCode = appCode; + this.style = style; + this.scheme = scheme; + this.format = format; + this.size = size; + this.version = 'newest'; + this.server = 1; + } + nextServer() { + this.server = this.server % 4 === 0 ? 1 : this.server + 1; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { }); + } + fetchTile(zoom, x, y) { + this.nextServer(); + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://' + this.server + '.' + this.style + '.maps.api.here.com/maptile/2.1/maptile/' + + this.version + '/' + this.scheme + '/' + zoom + '/' + x + '/' + y + '/' + + this.size + '/' + this.format + '?app_id=' + this.appId + '&app_code=' + this.appCode; + }); + } + } HereMapsProvider.PATH = '/maptile/2.1/'; - class MapBoxProvider extends MapProvider { - constructor(apiToken = '', id = '', mode = MapBoxProvider.STYLE, format = 'png', useHDPI = false, version = 'v4') { - super(); - this.apiToken = apiToken; - this.format = format; - this.useHDPI = useHDPI; - this.mode = mode; - this.mapId = id; - this.style = id; - this.version = version; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = MapBoxProvider.ADDRESS + this.version + '/' + this.mapId + '.json?access_token=' + this.apiToken; - const data = yield XHRUtils.get(address); - const meta = JSON.parse(data); - this.name = meta.name; - this.minZoom = meta.minZoom; - this.maxZoom = meta.maxZoom; - this.bounds = meta.bounds; - this.center = meta.center; - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - if (this.mode === MapBoxProvider.STYLE) { - image.src = MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; - } - else { - image.src = MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; - } - }); - } - } - MapBoxProvider.ADDRESS = 'https://api.mapbox.com/'; - MapBoxProvider.STYLE = 100; + class MapBoxProvider extends MapProvider { + constructor(apiToken = '', id = '', mode = MapBoxProvider.STYLE, format = 'png', useHDPI = false, version = 'v4') { + super(); + this.apiToken = apiToken; + this.format = format; + this.useHDPI = useHDPI; + this.mode = mode; + this.mapId = id; + this.style = id; + this.version = version; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = MapBoxProvider.ADDRESS + this.version + '/' + this.mapId + '.json?access_token=' + this.apiToken; + const data = yield XHRUtils.get(address); + const meta = JSON.parse(data); + this.name = meta.name; + this.minZoom = meta.minZoom; + this.maxZoom = meta.maxZoom; + this.bounds = meta.bounds; + this.center = meta.center; + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.getTileUrl(zoom, x, y); + }); + } + fetchTileBuffer(zoom, x, y) { + return __awaiter(this, void 0, void 0, function* () { + const response = yield fetch(this.getTileUrl(zoom, x, y)); + if (!response.ok) { + throw new Error('MapBoxProvider: Failed to fetch tile buffer. Status ' + response.status); + } + return response.arrayBuffer(); + }); + } + getTileUrl(zoom, x, y) { + if (this.mode === MapBoxProvider.STYLE) { + return MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; + } + else { + return MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; + } + } + } + MapBoxProvider.ADDRESS = 'https://api.mapbox.com/'; + MapBoxProvider.STYLE = 100; MapBoxProvider.MAP_ID = 101; - class MapTilerProvider extends MapProvider { - constructor(apiKey, category, style, format) { - super(); - this.apiKey = apiKey !== undefined ? apiKey : ''; - this.format = format !== undefined ? format : 'png'; - this.category = category !== undefined ? category : 'maps'; - this.style = style !== undefined ? style : 'satellite'; - this.resolution = 512; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://api.maptiler.com/' + this.category + '/' + this.style + '/' + zoom + '/' + x + '/' + y + '.' + this.format + '?key=' + this.apiKey; - }); - } + class MapTilerProvider extends MapProvider { + constructor(apiKey, category, style, format) { + super(); + this.apiKey = apiKey !== undefined ? apiKey : ''; + this.format = format !== undefined ? format : 'png'; + this.category = category !== undefined ? category : 'maps'; + this.style = style !== undefined ? style : 'satellite'; + this.resolution = 512; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://api.maptiler.com/' + this.category + '/' + this.style + '/' + zoom + '/' + x + '/' + y + '.' + this.format + '?key=' + this.apiKey; + }); + } } - class OpenMapTilesProvider extends MapProvider { - constructor(address, format = 'png', theme = 'klokantech-basic') { - super(); - this.address = address; - this.format = format; - this.theme = theme; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = this.address + 'styles/' + this.theme + '.json'; - const data = yield XHRUtils.get(address); - const meta = JSON.parse(data); - this.name = meta.name; - this.format = meta.format; - this.minZoom = meta.minZoom; - this.maxZoom = meta.maxZoom; - this.bounds = meta.bounds; - this.center = meta.center; - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = this.address + 'styles/' + this.theme + '/' + zoom + '/' + x + '/' + y + '.' + this.format; - }); - } + class OpenMapTilesProvider extends MapProvider { + constructor(address, format = 'png', theme = 'klokantech-basic') { + super(); + this.address = address; + this.format = format; + this.theme = theme; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = this.address + 'styles/' + this.theme + '.json'; + const data = yield XHRUtils.get(address); + const meta = JSON.parse(data); + this.name = meta.name; + this.format = meta.format; + this.minZoom = meta.minZoom; + this.maxZoom = meta.maxZoom; + this.bounds = meta.bounds; + this.center = meta.center; + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.address + 'styles/' + this.theme + '/' + zoom + '/' + x + '/' + y + '.' + this.format; + }); + } } - class DebugProvider extends MapProvider { - constructor() { - super(...arguments); - this.resolution = 256; - } - fetchTile(zoom, x, y) { - const canvas = CanvasUtils.createOffscreenCanvas(this.resolution, this.resolution); - const context = canvas.getContext('2d'); - const green = new three.Color(0x00ff00); - const red = new three.Color(0xff0000); - const color = green.lerpHSL(red, (zoom - this.minZoom) / (this.maxZoom - this.minZoom)); - context.fillStyle = color.getStyle(); - context.fillRect(0, 0, this.resolution, this.resolution); - context.fillStyle = '#000000'; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.font = 'bold ' + this.resolution * 0.1 + 'px arial'; - context.fillText('(' + zoom + ')', this.resolution / 2, this.resolution * 0.4); - context.fillText('(' + x + ', ' + y + ')', this.resolution / 2, this.resolution * 0.6); - return Promise.resolve(canvas); - } + class DebugProvider extends MapProvider { + constructor() { + super(...arguments); + this.resolution = 256; + } + fetchTile(zoom, x, y) { + const canvas = CanvasUtils.createOffscreenCanvas(this.resolution, this.resolution); + const context = canvas.getContext('2d'); + const green = new three.Color(0x00ff00); + const red = new three.Color(0xff0000); + const color = green.lerpHSL(red, (zoom - this.minZoom) / (this.maxZoom - this.minZoom)); + context.fillStyle = color.getStyle(); + context.fillRect(0, 0, this.resolution, this.resolution); + context.fillStyle = '#000000'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.font = 'bold ' + this.resolution * 0.1 + 'px arial'; + context.fillText('(' + zoom + ')', this.resolution / 2, this.resolution * 0.4); + context.fillText('(' + x + ', ' + y + ')', this.resolution / 2, this.resolution * 0.6); + return Promise.resolve(canvas); + } } - class HeightDebugProvider extends MapProvider { - constructor(provider) { - super(); - this.fromColor = new three.Color(0xff0000); - this.toColor = new three.Color(0x00ff00); - this.provider = provider; - } - fetchTile(zoom, x, y) { - return __awaiter(this, void 0, void 0, function* () { - const image = yield this.provider.fetchTile(zoom, x, y); - const resolution = 256; - const canvas = CanvasUtils.createOffscreenCanvas(resolution, resolution); - const context = canvas.getContext('2d'); - context.drawImage(image, 0, 0, resolution, resolution, 0, 0, resolution, resolution); - const imageData = context.getImageData(0, 0, resolution, resolution); - const data = imageData.data; - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; - const max = 1667721.6; - const color = this.fromColor.clone().lerpHSL(this.toColor, value / max); - data[i] = color.r * 255; - data[i + 1] = color.g * 255; - data[i + 2] = color.b * 255; - } - context.putImageData(imageData, 0, 0); - return canvas; - }); - } + class HeightDebugProvider extends MapProvider { + constructor(provider) { + super(); + this.fromColor = new three.Color(0xff0000); + this.toColor = new three.Color(0x00ff00); + this.provider = provider; + } + fetchTile(zoom, x, y) { + return __awaiter(this, void 0, void 0, function* () { + const image = yield this.provider.fetchTile(zoom, x, y); + const resolution = 256; + const canvas = CanvasUtils.createOffscreenCanvas(resolution, resolution); + const context = canvas.getContext('2d'); + context.drawImage(image, 0, 0, resolution, resolution, 0, 0, resolution, resolution); + const imageData = context.getImageData(0, 0, resolution, resolution); + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; + const max = 1667721.6; + const color = this.fromColor.clone().lerpHSL(this.toColor, value / max); + data[i] = color.r * 255; + data[i + 1] = color.g * 255; + data[i + 2] = color.b * 255; + } + context.putImageData(imageData, 0, 0); + return canvas; + }); + } } - class GeolocationUtils { - static get() { - return new Promise(function (resolve, reject) { - navigator.geolocation.getCurrentPosition(function (result) { - resolve(result); - }, reject); - }); - } + class GeolocationUtils { + static get() { + return new Promise(function (resolve, reject) { + navigator.geolocation.getCurrentPosition(function (result) { + resolve(result); + }, reject); + }); + } } - class CancelablePromise { - constructor(executor) { - this.fulfilled = false; - this.rejected = false; - this.called = false; - const resolve = (v) => { - this.fulfilled = true; - this.value = v; - if (typeof this.onResolve === 'function') { - this.onResolve(this.value); - this.called = true; - } - }; - const reject = (reason) => { - this.rejected = true; - this.value = reason; - if (typeof this.onReject === 'function') { - this.onReject(this.value); - this.called = true; - } - }; - try { - executor(resolve, reject); - } - catch (error) { - reject(error); - } - } - cancel() { - return false; - } - then(callback) { - this.onResolve = callback; - if (this.fulfilled && !this.called) { - this.called = true; - this.onResolve(this.value); - } - return this; - } - catch(callback) { - this.onReject = callback; - if (this.rejected && !this.called) { - this.called = true; - this.onReject(this.value); - } - return this; - } - finally(callback) { - return this; - } - static resolve(val) { - return new CancelablePromise(function executor(resolve, _reject) { - resolve(val); - }); - } - static reject(reason) { - return new CancelablePromise(function executor(resolve, reject) { - reject(reason); - }); - } - static all(promises) { - const fulfilledPromises = []; - const result = []; - function executor(resolve, reject) { - promises.forEach((promise, index) => { - return promise - .then((val) => { - fulfilledPromises.push(true); - result[index] = val; - if (fulfilledPromises.length === promises.length) { - return resolve(result); - } - }) - .catch((error) => { return reject(error); }); - }); - } - return new CancelablePromise(executor); - } + class CancelablePromise { + constructor(executor) { + this.fulfilled = false; + this.rejected = false; + this.called = false; + const resolve = (v) => { + this.fulfilled = true; + this.value = v; + if (typeof this.onResolve === 'function') { + this.onResolve(this.value); + this.called = true; + } + }; + const reject = (reason) => { + this.rejected = true; + this.value = reason; + if (typeof this.onReject === 'function') { + this.onReject(this.value); + this.called = true; + } + }; + try { + executor(resolve, reject); + } + catch (error) { + reject(error); + } + } + cancel() { + return false; + } + then(callback) { + this.onResolve = callback; + if (this.fulfilled && !this.called) { + this.called = true; + this.onResolve(this.value); + } + return this; + } + catch(callback) { + this.onReject = callback; + if (this.rejected && !this.called) { + this.called = true; + this.onReject(this.value); + } + return this; + } + finally(callback) { + return this; + } + static resolve(val) { + return new CancelablePromise(function executor(resolve, _reject) { + resolve(val); + }); + } + static reject(reason) { + return new CancelablePromise(function executor(resolve, reject) { + reject(reason); + }); + } + static all(promises) { + const fulfilledPromises = []; + const result = []; + function executor(resolve, reject) { + promises.forEach((promise, index) => { + return promise + .then((val) => { + fulfilledPromises.push(true); + result[index] = val; + if (fulfilledPromises.length === promises.length) { + return resolve(result); + } + }) + .catch((error) => { return reject(error); }); + }); + } + return new CancelablePromise(executor); + } } exports.BingMapsProvider = BingMapsProvider; @@ -2013,6 +2121,7 @@ exports.HeightDebugProvider = HeightDebugProvider; exports.HereMapsProvider = HereMapsProvider; exports.LODFrustum = LODFrustum; + exports.LODFrustumOrthographic = LODFrustumOrthographic; exports.LODRadial = LODRadial; exports.LODRaycast = LODRaycast; exports.MapBoxProvider = MapBoxProvider; diff --git a/build/geo-three.module.js b/build/geo-three.module.js index 2978b1b..02124c0 100644 --- a/build/geo-three.module.js +++ b/build/geo-three.module.js @@ -1,4 +1,4 @@ -import { Texture, RGBAFormat, LinearFilter, Mesh, REVISION, BufferGeometry, Float32BufferAttribute, Vector2, Vector3, MeshBasicMaterial, MeshPhongMaterial, Vector4, ShaderMaterial, Matrix4, Quaternion, TextureLoader, NearestFilter, Raycaster, DoubleSide, Uint32BufferAttribute, Frustum, Color } from 'three'; +import { Texture, RGBAFormat, LinearFilter, Mesh, REVISION, BufferGeometry, Float32BufferAttribute, Vector2, Vector3, MeshBasicMaterial, MeshPhongMaterial, Vector4, ShaderMaterial, Matrix4, Quaternion, TextureLoader, NearestFilter, Raycaster, DoubleSide, Uint32BufferAttribute, Frustum, Box3, Color } from 'three'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. @@ -27,1976 +27,2084 @@ function __awaiter(thisArg, _arguments, P, generator) { }); } -class MapProvider { - constructor() { - this.name = ''; - this.minZoom = 0; - this.maxZoom = 20; - this.bounds = []; - this.center = []; - } - fetchTile(zoom, x, y) { - return null; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { }); - } +class MapProvider { + constructor() { + this.name = ''; + this.minZoom = 0; + this.maxZoom = 20; + this.bounds = []; + this.center = []; + } + fetchTile(zoom, x, y) { + return null; + } + fetchTileBuffer(zoom, x, y) { + return Promise.resolve(null); + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { }); + } } -class OpenStreetMapsProvider extends MapProvider { - constructor(address = 'https://a.tile.openstreetmap.org/') { - super(); - this.address = address; - this.format = 'png'; - this.maxZoom = 19; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = this.address + zoom + '/' + x + '/' + y + '.' + this.format; - }); - } +class OpenStreetMapsProvider extends MapProvider { + constructor(address = 'https://a.tile.openstreetmap.org/') { + super(); + this.address = address; + this.format = 'png'; + this.maxZoom = 19; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.address + zoom + '/' + x + '/' + y + '.' + this.format; + }); + } } -class CanvasUtils { - static createOffscreenCanvas(width, height) { - if (typeof OffscreenCanvas !== 'undefined') { - return new OffscreenCanvas(width, height); - } - else { - let canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - } - } +class CanvasUtils { + static createOffscreenCanvas(width, height) { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(width, height); + } + else { + let canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + } } -class TextureUtils { - static createFillTexture(color = '#000000', width = 1, height = 1) { - const canvas = CanvasUtils.createOffscreenCanvas(width, height); - const context = canvas.getContext('2d'); - context.fillStyle = color; - context.fillRect(0, 0, width, height); - const texture = new Texture(canvas); - texture.format = RGBAFormat; - texture.magFilter = LinearFilter; - texture.minFilter = LinearFilter; - texture.generateMipmaps = false; - texture.needsUpdate = true; - return texture; - } +class TextureUtils { + static createFillTexture(color = '#000000', width = 1, height = 1) { + const canvas = CanvasUtils.createOffscreenCanvas(width, height); + const context = canvas.getContext('2d'); + context.fillStyle = color; + context.fillRect(0, 0, width, height); + const texture = new Texture(canvas); + texture.format = RGBAFormat; + texture.magFilter = LinearFilter; + texture.minFilter = LinearFilter; + texture.generateMipmaps = false; + texture.needsUpdate = true; + return texture; + } } -class QuadTreePosition { -} -QuadTreePosition.root = -1; -QuadTreePosition.topLeft = 0; -QuadTreePosition.topRight = 1; -QuadTreePosition.bottomLeft = 2; -QuadTreePosition.bottomRight = 3; -class MapNode extends Mesh { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = null, material = null) { - super(geometry, material); - this.mapView = null; - this.parentNode = null; - this.subdivided = false; - this.disposed = false; - this.nodesLoaded = 0; - this.childrenCache = null; - this.isMesh = true; - this.mapView = mapView; - this.parentNode = parentNode; - this.disposed = false; - this.location = location; - this.level = level; - this.x = x; - this.y = y; - this.initialize(); - } - initialize() { - return __awaiter(this, void 0, void 0, function* () { }); - } - createChildNodes() { } - subdivide() { - const maxZoom = this.mapView.maxZoom(); - if (this.children.length > 0 || this.level + 1 > maxZoom || this.parentNode !== null && this.parentNode.nodesLoaded < MapNode.childrens) { - return; - } - if (this.mapView.cacheTiles && this.childrenCache !== null) { - this.isMesh = false; - this.children = this.childrenCache; - this.nodesLoaded = this.childrenCache.length; - } - else { - this.createChildNodes(); - } - this.subdivided = true; - } - simplify() { - const minZoom = this.mapView.minZoom(); - if (this.level - 1 < minZoom) { - return; - } - if (this.mapView.cacheTiles) { - this.childrenCache = this.children; - } - else { - for (let i = 0; i < this.children.length; i++) { - this.children[i].dispose(); - } - } - this.subdivided = false; - this.isMesh = true; - this.children = []; - this.nodesLoaded = 0; - } - loadData() { - return __awaiter(this, void 0, void 0, function* () { - if (this.level < this.mapView.provider.minZoom || this.level > this.mapView.provider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.material.map = MapNode.defaultTexture; - this.material.needsUpdate = true; - return; - } - try { - const image = yield this.mapView.provider.fetchTile(this.level, this.x, this.y); - yield this.applyTexture(image); - } - catch (e) { - if (this.disposed) { - return; - } - console.warn('Geo-Three: Failed to load node tile data.', this); - this.material.map = MapNode.defaultTexture; - } - this.material.needsUpdate = true; - }); - } - applyTexture(image) { - return __awaiter(this, void 0, void 0, function* () { - if (this.disposed) { - return; - } - const texture = new Texture(image); - if (parseInt(REVISION) >= 152) { - texture.colorSpace = 'srgb'; - } - texture.generateMipmaps = false; - texture.format = RGBAFormat; - texture.magFilter = LinearFilter; - texture.minFilter = LinearFilter; - texture.needsUpdate = true; - this.material.map = texture; - }); - } - nodeReady() { - if (this.disposed) { - console.warn('Geo-Three: nodeReady() called for disposed node.', this); - this.dispose(); - return; - } - if (this.parentNode !== null) { - this.parentNode.nodesLoaded++; - if (this.parentNode.nodesLoaded === MapNode.childrens) { - if (this.parentNode.subdivided === true) { - this.parentNode.isMesh = false; - } - for (let i = 0; i < this.parentNode.children.length; i++) { - this.parentNode.children[i].visible = true; - } - } - if (this.parentNode.nodesLoaded > MapNode.childrens) { - console.error('Geo-Three: Loaded more children objects than expected.', this.parentNode.nodesLoaded, this); - } - } - else { - this.visible = true; - } - } - dispose() { - this.disposed = true; - const self = this; - try { - const material = self.material; - material.dispose(); - if (material.map && material.map !== MapNode.defaultTexture) { - material.map.dispose(); - } - } - catch (e) { } - try { - self.geometry.dispose(); - } - catch (e) { } - } -} -MapNode.defaultTexture = TextureUtils.createFillTexture(); -MapNode.baseGeometry = null; -MapNode.baseScale = null; +class QuadTreePosition { +} +QuadTreePosition.root = -1; +QuadTreePosition.topLeft = 0; +QuadTreePosition.topRight = 1; +QuadTreePosition.bottomLeft = 2; +QuadTreePosition.bottomRight = 3; +class MapNode extends Mesh { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = null, material = null) { + super(geometry, material); + this.mapView = null; + this.parentNode = null; + this.subdivided = false; + this.disposed = false; + this.nodesLoaded = 0; + this.childrenCache = null; + this.isMesh = true; + this.mapView = mapView; + this.parentNode = parentNode; + this.disposed = false; + this.location = location; + this.level = level; + this.x = x; + this.y = y; + this.initialize(); + } + initialize() { + return __awaiter(this, void 0, void 0, function* () { }); + } + createChildNodes() { } + subdivide() { + const maxZoom = this.mapView.maxZoom(); + if (this.children.length > 0 || this.level + 1 > maxZoom || this.parentNode !== null && this.parentNode.nodesLoaded < MapNode.childrens) { + return; + } + if (this.mapView.cacheTiles && this.childrenCache !== null) { + this.isMesh = false; + this.children = this.childrenCache; + this.nodesLoaded = this.childrenCache.length; + } + else { + this.createChildNodes(); + } + this.subdivided = true; + } + simplify() { + const minZoom = this.mapView.minZoom(); + if (this.level - 1 < minZoom) { + return; + } + if (this.mapView.cacheTiles) { + this.childrenCache = this.children; + } + else { + for (let i = 0; i < this.children.length; i++) { + this.children[i].dispose(); + } + } + this.subdivided = false; + this.isMesh = true; + this.children = []; + this.nodesLoaded = 0; + } + loadData() { + return __awaiter(this, void 0, void 0, function* () { + if (this.level < this.mapView.provider.minZoom || this.level > this.mapView.provider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.material.map = MapNode.defaultTexture; + this.material.needsUpdate = true; + return; + } + try { + const image = yield this.mapView.provider.fetchTile(this.level, this.x, this.y); + yield this.applyTexture(image); + } + catch (e) { + if (this.disposed) { + return; + } + console.warn('Geo-Three: Failed to load node tile data.', this); + this.material.map = MapNode.defaultTexture; + } + this.material.needsUpdate = true; + }); + } + applyTexture(image) { + return __awaiter(this, void 0, void 0, function* () { + if (this.disposed) { + return; + } + const texture = new Texture(image); + if (parseInt(REVISION) >= 152) { + texture.colorSpace = 'srgb'; + } + texture.generateMipmaps = false; + texture.format = RGBAFormat; + texture.magFilter = LinearFilter; + texture.minFilter = LinearFilter; + texture.needsUpdate = true; + this.material.map = texture; + }); + } + nodeReady() { + if (this.disposed) { + console.warn('Geo-Three: nodeReady() called for disposed node.', this); + this.dispose(); + return; + } + if (this.parentNode !== null) { + this.parentNode.nodesLoaded++; + if (this.parentNode.nodesLoaded === MapNode.childrens) { + if (this.parentNode.subdivided === true) { + this.parentNode.isMesh = false; + } + for (let i = 0; i < this.parentNode.children.length; i++) { + this.parentNode.children[i].visible = true; + } + } + if (this.parentNode.nodesLoaded > MapNode.childrens) { + console.error('Geo-Three: Loaded more children objects than expected.', this.parentNode.nodesLoaded, this); + } + } + else { + this.visible = true; + } + } + dispose() { + this.disposed = true; + const self = this; + try { + const material = self.material; + material.dispose(); + if (material.map && material.map !== MapNode.defaultTexture) { + material.map.dispose(); + } + } + catch (e) { } + try { + self.geometry.dispose(); + } + catch (e) { } + } +} +MapNode.defaultTexture = TextureUtils.createFillTexture(); +MapNode.baseGeometry = null; +MapNode.baseScale = null; MapNode.childrens = 4; -class MapNodeGeometry extends BufferGeometry { - constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0) { - super(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); - if (skirt) { - MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); - } - this.setIndex(indices); - this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); - } - static buildPlane(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, indices, vertices, normals, uvs) { - const widthHalf = width / 2; - const heightHalf = height / 2; - const gridX = widthSegments + 1; - const gridZ = heightSegments + 1; - const segmentWidth = width / widthSegments; - const segmentHeight = height / heightSegments; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - vertices.push(x, 0, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 1 - iz / heightSegments); - } - } - for (let iz = 0; iz < heightSegments; iz++) { - for (let ix = 0; ix < widthSegments; ix++) { - const a = ix + gridX * iz; - const b = ix + gridX * (iz + 1); - const c = ix + 1 + gridX * (iz + 1); - const d = ix + 1 + gridX * iz; - indices.push(a, b, d, b, c, d); - } - } - } - static buildSkirt(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirtDepth, indices, vertices, normals, uvs) { - const widthHalf = width / 2; - const heightHalf = height / 2; - const gridX = widthSegments + 1; - const gridZ = heightSegments + 1; - const segmentWidth = width / widthSegments; - const segmentHeight = height / heightSegments; - let start = vertices.length / 3; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - const z = -heightHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 1); - } - for (let ix = 0; ix < widthSegments; ix++) { - const a = ix; - const d = ix + 1; - const b = ix + start; - const c = ix + start + 1; - indices.push(d, b, a, d, c, b); - } - start = vertices.length / 3; - for (let ix = 0; ix < gridX; ix++) { - const x = ix * segmentWidth - widthHalf; - const z = heightSegments * segmentHeight - heightHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(ix / widthSegments, 0); - } - let offset = gridX * gridZ - widthSegments - 1; - for (let ix = 0; ix < widthSegments; ix++) { - const a = offset + ix; - const d = offset + ix + 1; - const b = ix + start; - const c = ix + start + 1; - indices.push(a, b, d, b, c, d); - } - start = vertices.length / 3; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - const x = -widthHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(0, 1 - iz / heightSegments); - } - for (let iz = 0; iz < heightSegments; iz++) { - const a = iz * gridZ; - const d = (iz + 1) * gridZ; - const b = iz + start; - const c = iz + start + 1; - indices.push(a, b, d, b, c, d); - } - start = vertices.length / 3; - for (let iz = 0; iz < gridZ; iz++) { - const z = iz * segmentHeight - heightHalf; - const x = widthSegments * segmentWidth - widthHalf; - vertices.push(x, -skirtDepth, z); - normals.push(0, 1, 0); - uvs.push(1.0, 1 - iz / heightSegments); - } - for (let iz = 0; iz < heightSegments; iz++) { - const a = iz * gridZ + heightSegments; - const d = (iz + 1) * gridZ + heightSegments; - const b = iz + start; - const c = iz + start + 1; - indices.push(d, b, a, d, c, b); - } - } +class MapNodeGeometry extends BufferGeometry { + constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0) { + super(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); + if (skirt) { + MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); + } + this.setIndex(indices); + this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); + } + static buildPlane(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, indices, vertices, normals, uvs) { + const widthHalf = width / 2; + const heightHalf = height / 2; + const gridX = widthSegments + 1; + const gridZ = heightSegments + 1; + const segmentWidth = width / widthSegments; + const segmentHeight = height / heightSegments; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + vertices.push(x, 0, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 1 - iz / heightSegments); + } + } + for (let iz = 0; iz < heightSegments; iz++) { + for (let ix = 0; ix < widthSegments; ix++) { + const a = ix + gridX * iz; + const b = ix + gridX * (iz + 1); + const c = ix + 1 + gridX * (iz + 1); + const d = ix + 1 + gridX * iz; + indices.push(a, b, d, b, c, d); + } + } + } + static buildSkirt(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirtDepth, indices, vertices, normals, uvs) { + const widthHalf = width / 2; + const heightHalf = height / 2; + const gridX = widthSegments + 1; + const gridZ = heightSegments + 1; + const segmentWidth = width / widthSegments; + const segmentHeight = height / heightSegments; + let start = vertices.length / 3; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + const z = -heightHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 1); + } + for (let ix = 0; ix < widthSegments; ix++) { + const a = ix; + const d = ix + 1; + const b = ix + start; + const c = ix + start + 1; + indices.push(d, b, a, d, c, b); + } + start = vertices.length / 3; + for (let ix = 0; ix < gridX; ix++) { + const x = ix * segmentWidth - widthHalf; + const z = heightSegments * segmentHeight - heightHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(ix / widthSegments, 0); + } + let offset = gridX * gridZ - widthSegments - 1; + for (let ix = 0; ix < widthSegments; ix++) { + const a = offset + ix; + const d = offset + ix + 1; + const b = ix + start; + const c = ix + start + 1; + indices.push(a, b, d, b, c, d); + } + start = vertices.length / 3; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + const x = -widthHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(0, 1 - iz / heightSegments); + } + for (let iz = 0; iz < heightSegments; iz++) { + const a = iz * gridZ; + const d = (iz + 1) * gridZ; + const b = iz + start; + const c = iz + start + 1; + indices.push(a, b, d, b, c, d); + } + start = vertices.length / 3; + for (let iz = 0; iz < gridZ; iz++) { + const z = iz * segmentHeight - heightHalf; + const x = widthSegments * segmentWidth - widthHalf; + vertices.push(x, -skirtDepth, z); + normals.push(0, 1, 0); + uvs.push(1.0, 1 - iz / heightSegments); + } + for (let iz = 0; iz < heightSegments; iz++) { + const a = iz * gridZ + heightSegments; + const d = (iz + 1) * gridZ + heightSegments; + const b = iz + start; + const c = iz + start + 1; + indices.push(d, b, a, d, c, b); + } + } } -class Geolocation { - constructor(latitude, longitude) { - this.latitude = latitude; - this.longitude = longitude; - } +class Geolocation { + constructor(latitude, longitude) { + this.latitude = latitude; + this.longitude = longitude; + } } -class UnitsUtils { - static datumsToSpherical(latitude, longitude) { - const x = longitude * UnitsUtils.EARTH_ORIGIN / 180.0; - let y = Math.log(Math.tan((90 + latitude) * Math.PI / 360.0)) / (Math.PI / 180.0); - y = y * UnitsUtils.EARTH_ORIGIN / 180.0; - return new Vector2(x, y); - } - static sphericalToDatums(x, y) { - const longitude = x / UnitsUtils.EARTH_ORIGIN * 180.0; - let latitude = y / UnitsUtils.EARTH_ORIGIN * 180.0; - latitude = 180.0 / Math.PI * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); - return new Geolocation(latitude, longitude); - } - static quadtreeToDatums(zoom, x, y) { - const n = Math.pow(2.0, zoom); - const longitude = x / n * 360.0 - 180.0; - const latitudeRad = Math.atan(Math.sinh(Math.PI * (1.0 - 2.0 * y / n))); - const latitude = 180.0 * (latitudeRad / Math.PI); - return new Geolocation(latitude, longitude); - } - static vectorToDatums(dir) { - const radToDeg = 180 / Math.PI; - const latitude = Math.atan2(dir.y, Math.sqrt(Math.pow(dir.x, 2) + Math.pow(-dir.z, 2))) * radToDeg; - const longitude = Math.atan2(-dir.z, dir.x) * radToDeg; - return new Geolocation(latitude, longitude); - } - static datumsToVector(latitude, longitude) { - const degToRad = Math.PI / 180; - const rotX = longitude * degToRad; - const rotY = latitude * degToRad; - var cos = Math.cos(rotY); - return new Vector3(-Math.cos(rotX + Math.PI) * cos, Math.sin(rotY), Math.sin(rotX + Math.PI) * cos); - } - static mapboxAltitude(color) { - return (color.r * 255.0 * 65536.0 + color.g * 255.0 * 256.0 + color.b * 255.0) * 0.1 - 10000.0; - } - static getTileSize(zoom) { - const maxExtent = UnitsUtils.WEB_MERCATOR_MAX_EXTENT; - const numTiles = Math.pow(2, zoom); - return 2 * maxExtent / numTiles; - } - static tileBounds(zoom, x, y) { - const tileSize = UnitsUtils.getTileSize(zoom); - const minX = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * tileSize; - const minY = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - (y + 1) * tileSize; - return [minX, tileSize, minY, tileSize]; - } - static webMercatorToLatitude(zoom, y) { - const yMerc = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - y * UnitsUtils.getTileSize(zoom); - return Math.atan(Math.sinh(yMerc / UnitsUtils.EARTH_RADIUS)); - } - static webMercatorToLongitude(zoom, x) { - const xMerc = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * UnitsUtils.getTileSize(zoom); - return xMerc / UnitsUtils.EARTH_RADIUS; - } -} -UnitsUtils.EARTH_RADIUS = 6371008; -UnitsUtils.EARTH_RADIUS_A = 6378137.0; -UnitsUtils.EARTH_RADIUS_B = 6356752.314245; -UnitsUtils.EARTH_PERIMETER = 2 * Math.PI * UnitsUtils.EARTH_RADIUS; -UnitsUtils.EARTH_ORIGIN = UnitsUtils.EARTH_PERIMETER / 2.0; +class UnitsUtils { + static datumsToSpherical(latitude, longitude) { + const x = longitude * UnitsUtils.EARTH_ORIGIN / 180.0; + let y = Math.log(Math.tan((90 + latitude) * Math.PI / 360.0)) / (Math.PI / 180.0); + y = y * UnitsUtils.EARTH_ORIGIN / 180.0; + return new Vector2(x, y); + } + static sphericalToDatums(x, y) { + const longitude = x / UnitsUtils.EARTH_ORIGIN * 180.0; + let latitude = y / UnitsUtils.EARTH_ORIGIN * 180.0; + latitude = 180.0 / Math.PI * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); + return new Geolocation(latitude, longitude); + } + static quadtreeToDatums(zoom, x, y) { + const n = Math.pow(2.0, zoom); + const longitude = x / n * 360.0 - 180.0; + const latitudeRad = Math.atan(Math.sinh(Math.PI * (1.0 - 2.0 * y / n))); + const latitude = 180.0 * (latitudeRad / Math.PI); + return new Geolocation(latitude, longitude); + } + static vectorToDatums(dir) { + const radToDeg = 180 / Math.PI; + const latitude = Math.atan2(dir.y, Math.sqrt(Math.pow(dir.x, 2) + Math.pow(-dir.z, 2))) * radToDeg; + const longitude = Math.atan2(-dir.z, dir.x) * radToDeg; + return new Geolocation(latitude, longitude); + } + static datumsToVector(latitude, longitude) { + const degToRad = Math.PI / 180; + const rotX = longitude * degToRad; + const rotY = latitude * degToRad; + var cos = Math.cos(rotY); + return new Vector3(-Math.cos(rotX + Math.PI) * cos, Math.sin(rotY), Math.sin(rotX + Math.PI) * cos); + } + static mapboxAltitude(color) { + return (color.r * 255.0 * 65536.0 + color.g * 255.0 * 256.0 + color.b * 255.0) * 0.1 - 10000.0; + } + static getTileSize(zoom) { + const maxExtent = UnitsUtils.WEB_MERCATOR_MAX_EXTENT; + const numTiles = Math.pow(2, zoom); + return 2 * maxExtent / numTiles; + } + static tileBounds(zoom, x, y) { + const tileSize = UnitsUtils.getTileSize(zoom); + const minX = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * tileSize; + const minY = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - (y + 1) * tileSize; + return [minX, tileSize, minY, tileSize]; + } + static webMercatorToLatitude(zoom, y) { + const yMerc = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - y * UnitsUtils.getTileSize(zoom); + return Math.atan(Math.sinh(yMerc / UnitsUtils.EARTH_RADIUS)); + } + static webMercatorToLongitude(zoom, x) { + const xMerc = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * UnitsUtils.getTileSize(zoom); + return xMerc / UnitsUtils.EARTH_RADIUS; + } +} +UnitsUtils.EARTH_RADIUS = 6371008; +UnitsUtils.EARTH_RADIUS_A = 6378137.0; +UnitsUtils.EARTH_RADIUS_B = 6356752.314245; +UnitsUtils.EARTH_PERIMETER = 2 * Math.PI * UnitsUtils.EARTH_RADIUS; +UnitsUtils.EARTH_ORIGIN = UnitsUtils.EARTH_PERIMETER / 2.0; UnitsUtils.WEB_MERCATOR_MAX_EXTENT = 20037508.34; -class MapPlaneNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - super(parentNode, mapView, location, level, x, y, MapPlaneNode.geometry, new MeshBasicMaterial({ wireframe: false })); - this.matrixAutoUpdate = false; - this.isMesh = true; - this.visible = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - this.nodeReady(); - }); - } - createChildNodes() { - const level = this.level + 1; - const x = this.x * 2; - const y = this.y * 2; - const Constructor = Object.getPrototypeOf(this).constructor; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } -} -MapPlaneNode.geometry = new MapNodeGeometry(1, 1, 1, 1, false); -MapPlaneNode.baseGeometry = MapPlaneNode.geometry; +class MapPlaneNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + super(parentNode, mapView, location, level, x, y, MapPlaneNode.geometry, new MeshBasicMaterial({ wireframe: false })); + this.matrixAutoUpdate = false; + this.isMesh = true; + this.visible = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + this.nodeReady(); + }); + } + createChildNodes() { + const level = this.level + 1; + const x = this.x * 2; + const y = this.y * 2; + const Constructor = Object.getPrototypeOf(this).constructor; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } +} +MapPlaneNode.geometry = new MapNodeGeometry(1, 1, 1, 1, false); +MapPlaneNode.baseGeometry = MapPlaneNode.geometry; MapPlaneNode.baseScale = new Vector3(UnitsUtils.EARTH_PERIMETER, 1.0, UnitsUtils.EARTH_PERIMETER); -class MapNodeHeightGeometry extends BufferGeometry { - constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0, imageData = null, calculateNormals = true) { - super(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); - const data = imageData.data; - for (let i = 0, j = 0; i < data.length && j < vertices.length; i += 4, j += 3) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; - vertices[j + 1] = value; - } - if (skirt) { - MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); - } - this.setIndex(indices); - this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); - if (calculateNormals) { - this.computeNormals(widthSegments, heightSegments); - } - } - computeNormals(widthSegments, heightSegments) { - const positionAttribute = this.getAttribute('position'); - if (positionAttribute !== undefined) { - let normalAttribute = this.getAttribute('normal'); - const normalLength = heightSegments * widthSegments; - for (let i = 0; i < normalLength; i++) { - normalAttribute.setXYZ(i, 0, 0, 0); - } - const pA = new Vector3(), pB = new Vector3(), pC = new Vector3(); - const nA = new Vector3(), nB = new Vector3(), nC = new Vector3(); - const cb = new Vector3(), ab = new Vector3(); - const indexLength = heightSegments * widthSegments * 6; - for (let i = 0; i < indexLength; i += 3) { - const vA = this.index.getX(i + 0); - const vB = this.index.getX(i + 1); - const vC = this.index.getX(i + 2); - pA.fromBufferAttribute(positionAttribute, vA); - pB.fromBufferAttribute(positionAttribute, vB); - pC.fromBufferAttribute(positionAttribute, vC); - cb.subVectors(pC, pB); - ab.subVectors(pA, pB); - cb.cross(ab); - nA.fromBufferAttribute(normalAttribute, vA); - nB.fromBufferAttribute(normalAttribute, vB); - nC.fromBufferAttribute(normalAttribute, vC); - nA.add(cb); - nB.add(cb); - nC.add(cb); - normalAttribute.setXYZ(vA, nA.x, nA.y, nA.z); - normalAttribute.setXYZ(vB, nB.x, nB.y, nB.z); - normalAttribute.setXYZ(vC, nC.x, nC.y, nC.z); - } - this.normalizeNormals(); - normalAttribute.needsUpdate = true; - } - } +class MapNodeHeightGeometry extends BufferGeometry { + constructor(width = 1.0, height = 1.0, widthSegments = 1.0, heightSegments = 1.0, skirt = false, skirtDepth = 10.0, imageData = null, calculateNormals = true) { + super(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + MapNodeGeometry.buildPlane(width, height, widthSegments, heightSegments, indices, vertices, normals, uvs); + const data = imageData.data; + for (let i = 0, j = 0; i < data.length && j < vertices.length; i += 4, j += 3) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; + vertices[j + 1] = value; + } + if (skirt) { + MapNodeGeometry.buildSkirt(width, height, widthSegments, heightSegments, skirtDepth, indices, vertices, normals, uvs); + } + this.setIndex(indices); + this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); + if (calculateNormals) { + this.computeNormals(widthSegments, heightSegments); + } + } + computeNormals(widthSegments, heightSegments) { + const positionAttribute = this.getAttribute('position'); + if (positionAttribute !== undefined) { + let normalAttribute = this.getAttribute('normal'); + const normalLength = heightSegments * widthSegments; + for (let i = 0; i < normalLength; i++) { + normalAttribute.setXYZ(i, 0, 0, 0); + } + const pA = new Vector3(), pB = new Vector3(), pC = new Vector3(); + const nA = new Vector3(), nB = new Vector3(), nC = new Vector3(); + const cb = new Vector3(), ab = new Vector3(); + const indexLength = heightSegments * widthSegments * 6; + for (let i = 0; i < indexLength; i += 3) { + const vA = this.index.getX(i + 0); + const vB = this.index.getX(i + 1); + const vC = this.index.getX(i + 2); + pA.fromBufferAttribute(positionAttribute, vA); + pB.fromBufferAttribute(positionAttribute, vB); + pC.fromBufferAttribute(positionAttribute, vC); + cb.subVectors(pC, pB); + ab.subVectors(pA, pB); + cb.cross(ab); + nA.fromBufferAttribute(normalAttribute, vA); + nB.fromBufferAttribute(normalAttribute, vB); + nC.fromBufferAttribute(normalAttribute, vC); + nA.add(cb); + nB.add(cb); + nC.add(cb); + normalAttribute.setXYZ(vA, nA.x, nA.y, nA.z); + normalAttribute.setXYZ(vB, nB.x, nB.y, nB.z); + normalAttribute.setXYZ(vC, nC.x, nC.y, nC.z); + } + this.normalizeNormals(); + normalAttribute.needsUpdate = true; + } + } } -class MapHeightNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = MapHeightNode.geometry, material = new MeshPhongMaterial({ wireframe: false, color: 0xffffff })) { - super(parentNode, mapView, location, level, x, y, geometry, material); - this.heightLoaded = false; - this.textureLoaded = false; - this.geometrySize = 16; - this.geometryNormals = false; - this.isMesh = true; - this.visible = false; - this.matrixAutoUpdate = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - yield this.loadHeightGeometry(); - this.nodeReady(); - }); - } - loadData() { - const _super = Object.create(null, { - loadData: { get: () => super.loadData } - }); - return __awaiter(this, void 0, void 0, function* () { - yield _super.loadData.call(this); - this.textureLoaded = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.geometry = MapPlaneNode.baseGeometry; - return; - } - try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); - const context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); - } - catch (e) { - if (this.disposed) { - return; - } - this.geometry = MapPlaneNode.baseGeometry; - } - this.heightLoaded = true; - }); - } - createChildNodes() { - const level = this.level + 1; - const Constructor = Object.getPrototypeOf(this).constructor; - const x = this.x * 2; - const y = this.y * 2; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, -0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(-0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - node.scale.set(0.5, 1.0, 0.5); - node.position.set(0.25, 0, 0.25); - this.add(node); - node.updateMatrix(); - node.updateMatrixWorld(true); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } -} -MapHeightNode.tileSize = 256; -MapHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); -MapHeightNode.baseGeometry = MapPlaneNode.geometry; +class MapHeightNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, geometry = MapHeightNode.geometry, material = new MeshPhongMaterial({ wireframe: false, color: 0xffffff })) { + super(parentNode, mapView, location, level, x, y, geometry, material); + this.heightLoaded = false; + this.textureLoaded = false; + this.geometrySize = 16; + this.geometryNormals = false; + this.isMesh = true; + this.visible = false; + this.matrixAutoUpdate = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + yield this.loadHeightGeometry(); + this.nodeReady(); + }); + } + loadData() { + const _super = Object.create(null, { + loadData: { get: () => super.loadData } + }); + return __awaiter(this, void 0, void 0, function* () { + yield _super.loadData.call(this); + this.textureLoaded = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.geometry = MapPlaneNode.baseGeometry; + return; + } + try { + let imageData; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + if (this.disposed) { + return; + } + this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); + } + catch (e) { + if (this.disposed) { + return; + } + this.geometry = MapPlaneNode.baseGeometry; + } + this.heightLoaded = true; + }); + } + createChildNodes() { + const level = this.level + 1; + const Constructor = Object.getPrototypeOf(this).constructor; + const x = this.x * 2; + const y = this.y * 2; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, -0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(-0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + node.scale.set(0.5, 1.0, 0.5); + node.position.set(0.25, 0, 0.25); + this.add(node); + node.updateMatrix(); + node.updateMatrixWorld(true); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } +} +MapHeightNode.tileSize = 256; +MapHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); +MapHeightNode.baseGeometry = MapPlaneNode.geometry; MapHeightNode.baseScale = new Vector3(UnitsUtils.EARTH_PERIMETER, 1, UnitsUtils.EARTH_PERIMETER); -class MapSphereNodeGeometry extends BufferGeometry { - constructor(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength) { - super(); - const thetaEnd = thetaStart + thetaLength; - let index = 0; - const grid = []; - const vertex = new Vector3(); - const normal = new Vector3(); - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - for (let iy = 0; iy <= heightSegments; iy++) { - const verticesRow = []; - const v = iy / heightSegments; - for (let ix = 0; ix <= widthSegments; ix++) { - const u = ix / widthSegments; - vertex.x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); - vertex.y = radius * Math.cos(thetaStart + v * thetaLength); - vertex.z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); - vertices.push(vertex.x, vertex.y, vertex.z); - normal.set(vertex.x, vertex.y, vertex.z).normalize(); - normals.push(normal.x, normal.y, normal.z); - uvs.push(u, 1 - v); - verticesRow.push(index++); - } - grid.push(verticesRow); - } - for (let iy = 0; iy < heightSegments; iy++) { - for (let ix = 0; ix < widthSegments; ix++) { - const a = grid[iy][ix + 1]; - const b = grid[iy][ix]; - const c = grid[iy + 1][ix]; - const d = grid[iy + 1][ix + 1]; - if (iy !== 0 || thetaStart > 0) { - indices.push(a, b, d); - } - if (iy !== heightSegments - 1 || thetaEnd < Math.PI) { - indices.push(b, c, d); - } - } - } - this.setIndex(indices); - this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); - this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); - this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); - } +class MapSphereNodeGeometry extends BufferGeometry { + constructor(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength) { + super(); + const thetaEnd = thetaStart + thetaLength; + let index = 0; + const grid = []; + const vertex = new Vector3(); + const normal = new Vector3(); + const indices = []; + const vertices = []; + const normals = []; + const uvs = []; + for (let iy = 0; iy <= heightSegments; iy++) { + const verticesRow = []; + const v = iy / heightSegments; + for (let ix = 0; ix <= widthSegments; ix++) { + const u = ix / widthSegments; + vertex.x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); + vertex.y = radius * Math.cos(thetaStart + v * thetaLength); + vertex.z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); + vertices.push(vertex.x, vertex.y, vertex.z); + normal.set(vertex.x, vertex.y, vertex.z).normalize(); + normals.push(normal.x, normal.y, normal.z); + uvs.push(u, 1 - v); + verticesRow.push(index++); + } + grid.push(verticesRow); + } + for (let iy = 0; iy < heightSegments; iy++) { + for (let ix = 0; ix < widthSegments; ix++) { + const a = grid[iy][ix + 1]; + const b = grid[iy][ix]; + const c = grid[iy + 1][ix]; + const d = grid[iy + 1][ix + 1]; + if (iy !== 0 || thetaStart > 0) { + indices.push(a, b, d); + } + if (iy !== heightSegments - 1 || thetaEnd < Math.PI) { + indices.push(b, c, d); + } + } + } + this.setIndex(indices); + this.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + this.setAttribute('normal', new Float32BufferAttribute(normals, 3)); + this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)); + } } -class MapSphereNode extends MapNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - let bounds = UnitsUtils.tileBounds(level, x, y); - const vertexShader = ` - varying vec3 vPosition; - - void main() { - vPosition = position; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `; - const fragmentShader = ` - #define PI 3.1415926538 - varying vec3 vPosition; - uniform sampler2D uTexture; - uniform vec4 webMercatorBounds; - - void main() { - // this could also be a constant, but for some reason using a constant causes more visible tile gaps at high zoom - float radius = length(vPosition); - - float latitude = asin(vPosition.y / radius); - float longitude = atan(-vPosition.z, vPosition.x); - - float web_mercator_x = radius * longitude; - float web_mercator_y = radius * log(tan(PI / 4.0 + latitude / 2.0)); - float y = (web_mercator_y - webMercatorBounds.z) / webMercatorBounds.w; - float x = (web_mercator_x - webMercatorBounds.x) / webMercatorBounds.y; - - vec4 color = texture2D(uTexture, vec2(x, y)); - gl_FragColor = color; - ${parseInt(REVISION) < 152 ? '' : ` - #include - #include ${parseInt(REVISION) >= 154 ? '' : ''} - `} - } - `; - let vBounds = new Vector4(...bounds); - const material = new ShaderMaterial({ - uniforms: { uTexture: { value: new Texture() }, webMercatorBounds: { value: vBounds } }, - vertexShader: vertexShader, - fragmentShader: fragmentShader - }); - super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), material); - this.applyScaleNode(); - this.matrixAutoUpdate = false; - this.isMesh = true; - this.visible = false; - } - initialize() { - const _super = Object.create(null, { - initialize: { get: () => super.initialize } - }); - return __awaiter(this, void 0, void 0, function* () { - _super.initialize.call(this); - yield this.loadData(); - this.nodeReady(); - }); - } - static createGeometry(zoom, x, y) { - const range = Math.pow(2, zoom); - const max = 40; - const segments = Math.floor(MapSphereNode.segments * (max / (zoom + 1)) / max); - const lon1 = x > 0 ? UnitsUtils.webMercatorToLongitude(zoom, x) + Math.PI : 0; - const lon2 = x < range - 1 ? UnitsUtils.webMercatorToLongitude(zoom, x + 1) + Math.PI : 2 * Math.PI; - const phiStart = lon1; - const phiLength = lon2 - lon1; - const lat1 = y > 0 ? UnitsUtils.webMercatorToLatitude(zoom, y) : Math.PI / 2; - const lat2 = y < range - 1 ? UnitsUtils.webMercatorToLatitude(zoom, y + 1) : -Math.PI / 2; - const thetaLength = lat1 - lat2; - const thetaStart = Math.PI - (lat1 + Math.PI / 2); - return new MapSphereNodeGeometry(1, segments, segments, phiStart, phiLength, thetaStart, thetaLength); - } - applyTexture(image) { - return __awaiter(this, void 0, void 0, function* () { - const textureLoader = new TextureLoader(); - const texture = textureLoader.load(image.src, function () { - if (parseInt(REVISION) >= 152) { - texture.colorSpace = 'srgb'; - } - }); - this.material.uniforms.uTexture.value = texture; - this.material.uniforms.uTexture.needsUpdate = true; - }); - } - applyScaleNode() { - this.geometry.computeBoundingBox(); - const box = this.geometry.boundingBox.clone(); - const center = box.getCenter(new Vector3()); - const matrix = new Matrix4(); - matrix.compose(new Vector3(-center.x, -center.y, -center.z), new Quaternion(), new Vector3(UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS)); - this.geometry.applyMatrix4(matrix); - this.position.copy(center); - this.updateMatrix(); - this.updateMatrixWorld(); - } - updateMatrix() { - this.matrix.setPosition(this.position); - this.matrixWorldNeedsUpdate = true; - } - updateMatrixWorld(force = false) { - if (this.matrixWorldNeedsUpdate || force) { - this.matrixWorld.copy(this.matrix); - this.matrixWorldNeedsUpdate = false; - } - } - createChildNodes() { - const level = this.level + 1; - const x = this.x * 2; - const y = this.y * 2; - const Constructor = Object.getPrototypeOf(this).constructor; - let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); - this.add(node); - node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); - this.add(node); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - super.raycast(raycaster, intersects); - } - } -} -MapSphereNode.baseGeometry = new MapSphereNodeGeometry(UnitsUtils.EARTH_RADIUS, 64, 64, 0, 2 * Math.PI, 0, Math.PI); -MapSphereNode.baseScale = new Vector3(1, 1, 1); +class MapSphereNode extends MapNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + let bounds = UnitsUtils.tileBounds(level, x, y); + const vertexShader = ` + varying vec3 vPosition; + + void main() { + vPosition = position; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `; + const fragmentShader = ` + #define PI 3.1415926538 + varying vec3 vPosition; + uniform sampler2D uTexture; + uniform vec4 webMercatorBounds; + + void main() { + // this could also be a constant, but for some reason using a constant causes more visible tile gaps at high zoom + float radius = length(vPosition); + + float latitude = asin(vPosition.y / radius); + float longitude = atan(-vPosition.z, vPosition.x); + + float web_mercator_x = radius * longitude; + float web_mercator_y = radius * log(tan(PI / 4.0 + latitude / 2.0)); + float y = (web_mercator_y - webMercatorBounds.z) / webMercatorBounds.w; + float x = (web_mercator_x - webMercatorBounds.x) / webMercatorBounds.y; + + vec4 color = texture2D(uTexture, vec2(x, y)); + gl_FragColor = color; + ${parseInt(REVISION) < 152 ? '' : ` + #include + #include ${parseInt(REVISION) >= 154 ? '' : ''} + `} + } + `; + let vBounds = new Vector4(...bounds); + const material = new ShaderMaterial({ + uniforms: { uTexture: { value: new Texture() }, webMercatorBounds: { value: vBounds } }, + vertexShader: vertexShader, + fragmentShader: fragmentShader + }); + super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), material); + this.applyScaleNode(); + this.matrixAutoUpdate = false; + this.isMesh = true; + this.visible = false; + } + initialize() { + const _super = Object.create(null, { + initialize: { get: () => super.initialize } + }); + return __awaiter(this, void 0, void 0, function* () { + _super.initialize.call(this); + yield this.loadData(); + this.nodeReady(); + }); + } + static createGeometry(zoom, x, y) { + const range = Math.pow(2, zoom); + const max = 40; + const segments = Math.floor(MapSphereNode.segments * (max / (zoom + 1)) / max); + const lon1 = x > 0 ? UnitsUtils.webMercatorToLongitude(zoom, x) + Math.PI : 0; + const lon2 = x < range - 1 ? UnitsUtils.webMercatorToLongitude(zoom, x + 1) + Math.PI : 2 * Math.PI; + const phiStart = lon1; + const phiLength = lon2 - lon1; + const lat1 = y > 0 ? UnitsUtils.webMercatorToLatitude(zoom, y) : Math.PI / 2; + const lat2 = y < range - 1 ? UnitsUtils.webMercatorToLatitude(zoom, y + 1) : -Math.PI / 2; + const thetaLength = lat1 - lat2; + const thetaStart = Math.PI - (lat1 + Math.PI / 2); + return new MapSphereNodeGeometry(1, segments, segments, phiStart, phiLength, thetaStart, thetaLength); + } + applyTexture(image) { + return __awaiter(this, void 0, void 0, function* () { + const textureLoader = new TextureLoader(); + const texture = textureLoader.load(image.src, function () { + if (parseInt(REVISION) >= 152) { + texture.colorSpace = 'srgb'; + } + }); + this.material.uniforms.uTexture.value = texture; + this.material.uniforms.uTexture.needsUpdate = true; + }); + } + applyScaleNode() { + this.geometry.computeBoundingBox(); + const box = this.geometry.boundingBox.clone(); + const center = box.getCenter(new Vector3()); + const matrix = new Matrix4(); + matrix.compose(new Vector3(-center.x, -center.y, -center.z), new Quaternion(), new Vector3(UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS, UnitsUtils.EARTH_RADIUS)); + this.geometry.applyMatrix4(matrix); + this.position.copy(center); + this.updateMatrix(); + this.updateMatrixWorld(); + } + updateMatrix() { + this.matrix.setPosition(this.position); + this.matrixWorldNeedsUpdate = true; + } + updateMatrixWorld(force = false) { + if (this.matrixWorldNeedsUpdate || force) { + this.matrixWorld.copy(this.matrix); + this.matrixWorldNeedsUpdate = false; + } + } + createChildNodes() { + const level = this.level + 1; + const x = this.x * 2; + const y = this.y * 2; + const Constructor = Object.getPrototypeOf(this).constructor; + let node = new Constructor(this, this.mapView, QuadTreePosition.topLeft, level, x, y); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.topRight, level, x + 1, y); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomLeft, level, x, y + 1); + this.add(node); + node = new Constructor(this, this.mapView, QuadTreePosition.bottomRight, level, x + 1, y + 1); + this.add(node); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + super.raycast(raycaster, intersects); + } + } +} +MapSphereNode.baseGeometry = new MapSphereNodeGeometry(UnitsUtils.EARTH_RADIUS, 64, 64, 0, 2 * Math.PI, 0, Math.PI); +MapSphereNode.baseScale = new Vector3(1, 1, 1); MapSphereNode.segments = 80; -class MapHeightNodeShader extends MapHeightNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { - const material = MapHeightNodeShader.prepareMaterial(new MeshPhongMaterial({ map: MapNode.defaultTexture, color: 0xFFFFFF })); - super(parentNode, mapView, location, level, x, y, MapHeightNodeShader.geometry, material); - this.frustumCulled = false; - } - static prepareMaterial(material) { - material.userData = { heightMap: { value: MapHeightNodeShader.defaultHeightTexture } }; - material.onBeforeCompile = (shader) => { - for (const i in material.userData) { - shader.uniforms[i] = material.userData[i]; - } - shader.vertexShader = - ` - uniform sampler2D heightMap; - ` + shader.vertexShader; - shader.vertexShader = shader.vertexShader.replace('#include ', ` - #include - - // Calculate height of the title - vec4 _theight = texture2D(heightMap, vUv); - float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; - vec3 _transformed = position + _height * normal; - - // Vertex position based on height - gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); - `); - }; - return material; - } - loadData() { - const _super = Object.create(null, { - loadData: { get: () => super.loadData } - }); - return __awaiter(this, void 0, void 0, function* () { - yield _super.loadData.call(this); - this.textureLoaded = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { - console.warn('Geo-Three: Loading tile outside of provider range.', this); - this.material.map = MapHeightNodeShader.defaultTexture; - this.material.needsUpdate = true; - return; - } - try { - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - const texture = new Texture(image); - texture.generateMipmaps = false; - texture.format = RGBAFormat; - texture.magFilter = NearestFilter; - texture.minFilter = NearestFilter; - texture.needsUpdate = true; - this.material.userData.heightMap.value = texture; - } - catch (e) { - if (this.disposed) { - return; - } - console.error('Geo-Three: Failed to load node tile height data.', this); - this.material.userData.heightMap.value = MapHeightNodeShader.defaultHeightTexture; - } - this.material.needsUpdate = true; - this.heightLoaded = true; - }); - } - raycast(raycaster, intersects) { - if (this.isMesh === true) { - this.geometry = MapPlaneNode.geometry; - super.raycast(raycaster, intersects); - this.geometry = MapHeightNodeShader.geometry; - } - } - dispose() { - super.dispose(); - if (this.material.userData.heightMap.value && this.material.userData.heightMap.value !== MapHeightNodeShader.defaultHeightTexture) { - this.material.userData.heightMap.value.dispose(); - } - } -} -MapHeightNodeShader.defaultHeightTexture = TextureUtils.createFillTexture('#0186C0'); -MapHeightNodeShader.geometrySize = 256; -MapHeightNodeShader.geometry = new MapNodeGeometry(1.0, 1.0, MapHeightNodeShader.geometrySize, MapHeightNodeShader.geometrySize, true); -MapHeightNodeShader.baseGeometry = MapPlaneNode.geometry; +class MapHeightNodeShader extends MapHeightNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0) { + const material = MapHeightNodeShader.prepareMaterial(new MeshPhongMaterial({ map: MapNode.defaultTexture, color: 0xFFFFFF })); + super(parentNode, mapView, location, level, x, y, MapHeightNodeShader.geometry, material); + this.frustumCulled = false; + } + static prepareMaterial(material) { + material.userData = { heightMap: { value: MapHeightNodeShader.defaultHeightTexture } }; + material.onBeforeCompile = (shader) => { + for (const i in material.userData) { + shader.uniforms[i] = material.userData[i]; + } + shader.vertexShader = + ` + uniform sampler2D heightMap; + ` + shader.vertexShader; + shader.vertexShader = shader.vertexShader.replace('#include ', ` + #include + + // Calculate height of the title + vec4 _theight = texture2D(heightMap, vMapUv); + float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; + vec3 _transformed = position + _height * normal; + + // Vertex position based on height + gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); + `); + }; + return material; + } + loadData() { + const _super = Object.create(null, { + loadData: { get: () => super.loadData } + }); + return __awaiter(this, void 0, void 0, function* () { + yield _super.loadData.call(this); + this.textureLoaded = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + if (this.level < this.mapView.heightProvider.minZoom || this.level > this.mapView.heightProvider.maxZoom) { + console.warn('Geo-Three: Loading tile outside of provider range.', this); + this.material.map = MapHeightNodeShader.defaultTexture; + this.material.needsUpdate = true; + return; + } + try { + let texture; + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + texture = new Texture(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + texture = new Texture(image); + } + if (this.disposed) { + return; + } + texture.generateMipmaps = false; + texture.format = RGBAFormat; + texture.magFilter = NearestFilter; + texture.minFilter = NearestFilter; + texture.needsUpdate = true; + this.material.userData.heightMap.value = texture; + } + catch (e) { + if (this.disposed) { + return; + } + console.error('Geo-Three: Failed to load node tile height data.', this); + this.material.userData.heightMap.value = MapHeightNodeShader.defaultHeightTexture; + } + this.material.needsUpdate = true; + this.heightLoaded = true; + }); + } + raycast(raycaster, intersects) { + if (this.isMesh === true) { + this.geometry = MapPlaneNode.geometry; + super.raycast(raycaster, intersects); + this.geometry = MapHeightNodeShader.geometry; + } + } + dispose() { + super.dispose(); + if (this.material.userData.heightMap.value && this.material.userData.heightMap.value !== MapHeightNodeShader.defaultHeightTexture) { + this.material.userData.heightMap.value.dispose(); + } + } +} +MapHeightNodeShader.defaultHeightTexture = TextureUtils.createFillTexture('#0186C0'); +MapHeightNodeShader.geometrySize = 256; +MapHeightNodeShader.geometry = new MapNodeGeometry(1.0, 1.0, MapHeightNodeShader.geometrySize, MapHeightNodeShader.geometrySize, true); +MapHeightNodeShader.baseGeometry = MapPlaneNode.geometry; MapHeightNodeShader.baseScale = new Vector3(UnitsUtils.EARTH_PERIMETER, 1, UnitsUtils.EARTH_PERIMETER); -class LODRaycast { - constructor() { - this.subdivisionRays = 1; - this.thresholdUp = 0.6; - this.thresholdDown = 0.15; - this.raycaster = new Raycaster(); - this.mouse = new Vector2(); - this.powerDistance = false; - this.scaleDistance = true; - } - updateLOD(view, camera, renderer, scene) { - const intersects = []; - for (let t = 0; t < this.subdivisionRays; t++) { - this.mouse.set(Math.random() * 2 - 1, Math.random() * 2 - 1); - this.raycaster.setFromCamera(this.mouse, camera); - let myIntersects = []; - this.raycaster.intersectObjects(view.children, true, myIntersects); - if (myIntersects.length > 0) { - intersects.push(myIntersects[0]); - } - } - for (let i = 0; i < intersects.length; i++) { - const node = intersects[i].object; - let distance = intersects[i].distance; - if (this.powerDistance) { - distance = Math.pow(distance * 2, node.level); - } - if (this.scaleDistance) { - const matrix = node.matrixWorld.elements; - const vector = new Vector3(matrix[0], matrix[1], matrix[2]); - distance = vector.length() / distance; - } - if (distance > this.thresholdUp) { - node.subdivide(); - } - else if (distance < this.thresholdDown && node.parentNode) { - node.parentNode.simplify(); - } - } - } +class LODRaycast { + constructor() { + this.subdivisionRays = 1; + this.thresholdUp = 0.6; + this.thresholdDown = 0.15; + this.raycaster = new Raycaster(); + this.mouse = new Vector2(); + this.powerDistance = false; + this.scaleDistance = true; + } + updateLOD(view, camera, renderer, scene) { + const intersects = []; + for (let t = 0; t < this.subdivisionRays; t++) { + this.mouse.set(Math.random() * 2 - 1, Math.random() * 2 - 1); + this.raycaster.setFromCamera(this.mouse, camera); + let myIntersects = []; + this.raycaster.intersectObjects(view.children, true, myIntersects); + if (myIntersects.length > 0) { + intersects.push(myIntersects[0]); + } + } + for (let i = 0; i < intersects.length; i++) { + const node = intersects[i].object; + let distance = intersects[i].distance; + if (this.powerDistance) { + distance = Math.pow(distance * 2, node.level); + } + if (this.scaleDistance) { + const matrix = node.matrixWorld.elements; + const vector = new Vector3(matrix[0], matrix[1], matrix[2]); + distance = vector.length() / distance; + } + if (distance > this.thresholdUp) { + node.subdivide(); + } + else if (distance < this.thresholdDown && node.parentNode) { + node.parentNode.simplify(); + } + } + } } -class Martini { - constructor(gridSize = 257) { - this.gridSize = gridSize; - const tileSize = gridSize - 1; - if (tileSize & tileSize - 1) { - throw new Error(`Expected grid size to be 2^n+1, got ${gridSize}.`); - } - this.numTriangles = tileSize * tileSize * 2 - 2; - this.numParentTriangles = this.numTriangles - tileSize * tileSize; - this.indices = new Uint32Array(this.gridSize * this.gridSize); - this.coords = new Uint16Array(this.numTriangles * 4); - for (let i = 0; i < this.numTriangles; i++) { - let id = i + 2; - let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; - if (id & 1) { - bx = by = cx = tileSize; - } - else { - ax = ay = cy = tileSize; - } - while ((id >>= 1) > 1) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (id & 1) { - bx = ax; - by = ay; - ax = cx; - ay = cy; - } - else { - ax = bx; - ay = by; - bx = cx; - by = cy; - } - cx = mx; - cy = my; - } - const k = i * 4; - this.coords[k + 0] = ax; - this.coords[k + 1] = ay; - this.coords[k + 2] = bx; - this.coords[k + 3] = by; - } - } - createTile(terrain) { - return new Tile(terrain, this); - } -} -class Tile { - constructor(terrain, martini) { - const size = martini.gridSize; - if (terrain.length !== size * size) { - throw new Error(`Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`); - } - this.terrain = terrain; - this.martini = martini; - this.errors = new Float32Array(terrain.length); - this.update(); - } - update() { - const { numTriangles, numParentTriangles, coords, gridSize: size } = this.martini; - const { terrain, errors } = this; - for (let i = numTriangles - 1; i >= 0; i--) { - const k = i * 4; - const ax = coords[k + 0]; - const ay = coords[k + 1]; - const bx = coords[k + 2]; - const by = coords[k + 3]; - const mx = ax + bx >> 1; - const my = ay + by >> 1; - const cx = mx + my - ay; - const cy = my + ax - mx; - const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2; - const middleIndex = my * size + mx; - const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]); - errors[middleIndex] = Math.max(errors[middleIndex], middleError); - if (i < numParentTriangles) { - const leftChildIndex = (ay + cy >> 1) * size + (ax + cx >> 1); - const rightChildIndex = (by + cy >> 1) * size + (bx + cx >> 1); - errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]); - } - } - } - getMesh(maxError = 0, withSkirts = false) { - const { gridSize: size, indices } = this.martini; - const { errors } = this; - let numVertices = 0; - let numTriangles = 0; - const max = size - 1; - let aIndex, bIndex, cIndex = 0; - const leftSkirtIndices = []; - const rightSkirtIndices = []; - const bottomSkirtIndices = []; - const topSkirtIndices = []; - indices.fill(0); - function countElements(ax, ay, bx, by, cx, cy) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { - countElements(cx, cy, ax, ay, mx, my); - countElements(bx, by, cx, cy, mx, my); - } - else { - aIndex = ay * size + ax; - bIndex = by * size + bx; - cIndex = cy * size + cx; - if (indices[aIndex] === 0) { - if (withSkirts) { - if (ax === 0) { - leftSkirtIndices.push(numVertices); - } - else if (ax === max) { - rightSkirtIndices.push(numVertices); - } - if (ay === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (ay === max) { - topSkirtIndices.push(numVertices); - } - } - indices[aIndex] = ++numVertices; - } - if (indices[bIndex] === 0) { - if (withSkirts) { - if (bx === 0) { - leftSkirtIndices.push(numVertices); - } - else if (bx === max) { - rightSkirtIndices.push(numVertices); - } - if (by === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (by === max) { - topSkirtIndices.push(numVertices); - } - } - indices[bIndex] = ++numVertices; - } - if (indices[cIndex] === 0) { - if (withSkirts) { - if (cx === 0) { - leftSkirtIndices.push(numVertices); - } - else if (cx === max) { - rightSkirtIndices.push(numVertices); - } - if (cy === 0) { - bottomSkirtIndices.push(numVertices); - } - else if (cy === max) { - topSkirtIndices.push(numVertices); - } - } - indices[cIndex] = ++numVertices; - } - numTriangles++; - } - } - countElements(0, 0, max, max, max, 0); - countElements(max, max, 0, 0, 0, max); - let numTotalVertices = numVertices * 2; - let numTotalTriangles = numTriangles * 3; - if (withSkirts) { - numTotalVertices += (leftSkirtIndices.length + rightSkirtIndices.length + bottomSkirtIndices.length + topSkirtIndices.length) * 2; - numTotalTriangles += ((leftSkirtIndices.length - 1) * 2 + (rightSkirtIndices.length - 1) * 2 + (bottomSkirtIndices.length - 1) * 2 + (topSkirtIndices.length - 1) * 2) * 3; - } - const vertices = new Uint16Array(numTotalVertices); - const triangles = new Uint32Array(numTotalTriangles); - let triIndex = 0; - function processTriangle(ax, ay, bx, by, cx, cy) { - const mx = ax + bx >> 1; - const my = ay + by >> 1; - if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { - processTriangle(cx, cy, ax, ay, mx, my); - processTriangle(bx, by, cx, cy, mx, my); - } - else { - const a = indices[ay * size + ax] - 1; - const b = indices[by * size + bx] - 1; - const c = indices[cy * size + cx] - 1; - vertices[2 * a] = ax; - vertices[2 * a + 1] = ay; - vertices[2 * b] = bx; - vertices[2 * b + 1] = by; - vertices[2 * c] = cx; - vertices[2 * c + 1] = cy; - triangles[triIndex++] = a; - triangles[triIndex++] = b; - triangles[triIndex++] = c; - } - } - processTriangle(0, 0, max, max, max, 0); - processTriangle(max, max, 0, 0, 0, max); - if (withSkirts) { - leftSkirtIndices.sort((a, b) => { return vertices[2 * a + 1] - vertices[2 * b + 1]; }); - rightSkirtIndices.sort((a, b) => { return vertices[2 * b + 1] - vertices[2 * a + 1]; }); - bottomSkirtIndices.sort((a, b) => { return vertices[2 * b] - vertices[2 * a]; }); - topSkirtIndices.sort((a, b) => { return vertices[2 * a] - vertices[2 * b]; }); - let skirtIndex = numVertices * 2; - function constructSkirt(skirt) { - const skirtLength = skirt.length; - for (let i = 0; i < skirtLength - 1; i++) { - const currIndex = skirt[i]; - const nextIndex = skirt[i + 1]; - const currentSkirt = skirtIndex / 2; - const nextSkirt = (skirtIndex + 2) / 2; - vertices[skirtIndex++] = vertices[2 * currIndex]; - vertices[skirtIndex++] = vertices[2 * currIndex + 1]; - triangles[triIndex++] = currIndex; - triangles[triIndex++] = currentSkirt; - triangles[triIndex++] = nextIndex; - triangles[triIndex++] = currentSkirt; - triangles[triIndex++] = nextSkirt; - triangles[triIndex++] = nextIndex; - } - vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1]]; - vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1] + 1]; - } - constructSkirt(leftSkirtIndices); - constructSkirt(rightSkirtIndices); - constructSkirt(bottomSkirtIndices); - constructSkirt(topSkirtIndices); - } - return { vertices: vertices, triangles: triangles, numVerticesWithoutSkirts: numVertices }; - } +class Martini { + constructor(gridSize = 257) { + this.gridSize = gridSize; + const tileSize = gridSize - 1; + if (tileSize & tileSize - 1) { + throw new Error(`Expected grid size to be 2^n+1, got ${gridSize}.`); + } + this.numTriangles = tileSize * tileSize * 2 - 2; + this.numParentTriangles = this.numTriangles - tileSize * tileSize; + this.indices = new Uint32Array(this.gridSize * this.gridSize); + this.coords = new Uint16Array(this.numTriangles * 4); + for (let i = 0; i < this.numTriangles; i++) { + let id = i + 2; + let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; + if (id & 1) { + bx = by = cx = tileSize; + } + else { + ax = ay = cy = tileSize; + } + while ((id >>= 1) > 1) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (id & 1) { + bx = ax; + by = ay; + ax = cx; + ay = cy; + } + else { + ax = bx; + ay = by; + bx = cx; + by = cy; + } + cx = mx; + cy = my; + } + const k = i * 4; + this.coords[k + 0] = ax; + this.coords[k + 1] = ay; + this.coords[k + 2] = bx; + this.coords[k + 3] = by; + } + } + createTile(terrain) { + return new Tile(terrain, this); + } +} +class Tile { + constructor(terrain, martini) { + const size = martini.gridSize; + if (terrain.length !== size * size) { + throw new Error(`Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`); + } + this.terrain = terrain; + this.martini = martini; + this.errors = new Float32Array(terrain.length); + this.update(); + } + update() { + const { numTriangles, numParentTriangles, coords, gridSize: size } = this.martini; + const { terrain, errors } = this; + for (let i = numTriangles - 1; i >= 0; i--) { + const k = i * 4; + const ax = coords[k + 0]; + const ay = coords[k + 1]; + const bx = coords[k + 2]; + const by = coords[k + 3]; + const mx = ax + bx >> 1; + const my = ay + by >> 1; + const cx = mx + my - ay; + const cy = my + ax - mx; + const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2; + const middleIndex = my * size + mx; + const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]); + errors[middleIndex] = Math.max(errors[middleIndex], middleError); + if (i < numParentTriangles) { + const leftChildIndex = (ay + cy >> 1) * size + (ax + cx >> 1); + const rightChildIndex = (by + cy >> 1) * size + (bx + cx >> 1); + errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]); + } + } + } + getMesh(maxError = 0, withSkirts = false) { + const { gridSize: size, indices } = this.martini; + const { errors } = this; + let numVertices = 0; + let numTriangles = 0; + const max = size - 1; + let aIndex, bIndex, cIndex = 0; + const leftSkirtIndices = []; + const rightSkirtIndices = []; + const bottomSkirtIndices = []; + const topSkirtIndices = []; + indices.fill(0); + function countElements(ax, ay, bx, by, cx, cy) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { + countElements(cx, cy, ax, ay, mx, my); + countElements(bx, by, cx, cy, mx, my); + } + else { + aIndex = ay * size + ax; + bIndex = by * size + bx; + cIndex = cy * size + cx; + if (indices[aIndex] === 0) { + if (withSkirts) { + if (ax === 0) { + leftSkirtIndices.push(numVertices); + } + else if (ax === max) { + rightSkirtIndices.push(numVertices); + } + if (ay === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (ay === max) { + topSkirtIndices.push(numVertices); + } + } + indices[aIndex] = ++numVertices; + } + if (indices[bIndex] === 0) { + if (withSkirts) { + if (bx === 0) { + leftSkirtIndices.push(numVertices); + } + else if (bx === max) { + rightSkirtIndices.push(numVertices); + } + if (by === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (by === max) { + topSkirtIndices.push(numVertices); + } + } + indices[bIndex] = ++numVertices; + } + if (indices[cIndex] === 0) { + if (withSkirts) { + if (cx === 0) { + leftSkirtIndices.push(numVertices); + } + else if (cx === max) { + rightSkirtIndices.push(numVertices); + } + if (cy === 0) { + bottomSkirtIndices.push(numVertices); + } + else if (cy === max) { + topSkirtIndices.push(numVertices); + } + } + indices[cIndex] = ++numVertices; + } + numTriangles++; + } + } + countElements(0, 0, max, max, max, 0); + countElements(max, max, 0, 0, 0, max); + let numTotalVertices = numVertices * 2; + let numTotalTriangles = numTriangles * 3; + if (withSkirts) { + numTotalVertices += (leftSkirtIndices.length + rightSkirtIndices.length + bottomSkirtIndices.length + topSkirtIndices.length) * 2; + numTotalTriangles += ((leftSkirtIndices.length - 1) * 2 + (rightSkirtIndices.length - 1) * 2 + (bottomSkirtIndices.length - 1) * 2 + (topSkirtIndices.length - 1) * 2) * 3; + } + const vertices = new Uint16Array(numTotalVertices); + const triangles = new Uint32Array(numTotalTriangles); + let triIndex = 0; + function processTriangle(ax, ay, bx, by, cx, cy) { + const mx = ax + bx >> 1; + const my = ay + by >> 1; + if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) { + processTriangle(cx, cy, ax, ay, mx, my); + processTriangle(bx, by, cx, cy, mx, my); + } + else { + const a = indices[ay * size + ax] - 1; + const b = indices[by * size + bx] - 1; + const c = indices[cy * size + cx] - 1; + vertices[2 * a] = ax; + vertices[2 * a + 1] = ay; + vertices[2 * b] = bx; + vertices[2 * b + 1] = by; + vertices[2 * c] = cx; + vertices[2 * c + 1] = cy; + triangles[triIndex++] = a; + triangles[triIndex++] = b; + triangles[triIndex++] = c; + } + } + processTriangle(0, 0, max, max, max, 0); + processTriangle(max, max, 0, 0, 0, max); + if (withSkirts) { + leftSkirtIndices.sort((a, b) => { return vertices[2 * a + 1] - vertices[2 * b + 1]; }); + rightSkirtIndices.sort((a, b) => { return vertices[2 * b + 1] - vertices[2 * a + 1]; }); + bottomSkirtIndices.sort((a, b) => { return vertices[2 * b] - vertices[2 * a]; }); + topSkirtIndices.sort((a, b) => { return vertices[2 * a] - vertices[2 * b]; }); + let skirtIndex = numVertices * 2; + function constructSkirt(skirt) { + const skirtLength = skirt.length; + for (let i = 0; i < skirtLength - 1; i++) { + const currIndex = skirt[i]; + const nextIndex = skirt[i + 1]; + const currentSkirt = skirtIndex / 2; + const nextSkirt = (skirtIndex + 2) / 2; + vertices[skirtIndex++] = vertices[2 * currIndex]; + vertices[skirtIndex++] = vertices[2 * currIndex + 1]; + triangles[triIndex++] = currIndex; + triangles[triIndex++] = currentSkirt; + triangles[triIndex++] = nextIndex; + triangles[triIndex++] = currentSkirt; + triangles[triIndex++] = nextSkirt; + triangles[triIndex++] = nextIndex; + } + vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1]]; + vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1] + 1]; + } + constructSkirt(leftSkirtIndices); + constructSkirt(rightSkirtIndices); + constructSkirt(bottomSkirtIndices); + constructSkirt(topSkirtIndices); + } + return { vertices: vertices, triangles: triangles, numVerticesWithoutSkirts: numVertices }; + } } -class MapMartiniHeightNode extends MapHeightNode { - constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, { elevationDecoder = null, meshMaxError = 10, exageration = 1 } = {}) { - super(parentNode, mapView, location, level, x, y, MapMartiniHeightNode.geometry, MapMartiniHeightNode.prepareMaterial(new MeshPhongMaterial({ - map: MapMartiniHeightNode.emptyTexture, - color: 0xFFFFFF, - side: DoubleSide - }), level, exageration)); - this.elevationDecoder = { - rScaler: 256, - gScaler: 1, - bScaler: 1 / 256, - offset: -32768 - }; - this.exageration = 1.0; - this.meshMaxError = 10; - if (elevationDecoder) { - this.elevationDecoder = elevationDecoder; - } - this.meshMaxError = meshMaxError; - this.exageration = exageration; - this.frustumCulled = false; - } - static prepareMaterial(material, level, exageration = 1.0) { - material.userData = { - heightMap: { value: MapMartiniHeightNode.emptyTexture }, - drawNormals: { value: 0 }, - drawBlack: { value: 0 }, - zoomlevel: { value: level }, - computeNormals: { value: 1 }, - drawTexture: { value: 1 } - }; - material.onBeforeCompile = (shader) => { - for (let i in material.userData) { - shader.uniforms[i] = material.userData[i]; - } - shader.vertexShader = - ` - uniform bool computeNormals; - uniform float zoomlevel; - uniform sampler2D heightMap; - ` + shader.vertexShader; - shader.fragmentShader = - ` - uniform bool drawNormals; - uniform bool drawTexture; - uniform bool drawBlack; - ` + shader.fragmentShader; - shader.fragmentShader = shader.fragmentShader.replace('#include ', ` - if(drawBlack) { - gl_FragColor = vec4( 0.0,0.0,0.0, 1.0 ); - } else if(drawNormals) { - gl_FragColor = vec4( ( 0.5 * vNormal + 0.5 ), 1.0 ); - } else if (!drawTexture) { - gl_FragColor = vec4( 0.0,0.0,0.0, 0.0 ); - }`); - shader.vertexShader = shader.vertexShader.replace('#include ', ` - #include - - // queried pixels: - // +-----------+ - // | | | | - // | a | b | c | - // | | | | - // +-----------+ - // | | | | - // | d | e | f | - // | | | | - // +-----------+ - // | | | | - // | g | h | i | - // | | | | - // +-----------+ - - if (computeNormals) { - float e = getElevation(vUv, 0.0); - ivec2 size = textureSize(heightMap, 0); - float offset = 1.0 / float(size.x); - float a = getElevation(vUv + vec2(-offset, -offset), 0.0); - float b = getElevation(vUv + vec2(0, -offset), 0.0); - float c = getElevation(vUv + vec2(offset, -offset), 0.0); - float d = getElevation(vUv + vec2(-offset, 0), 0.0); - float f = getElevation(vUv + vec2(offset, 0), 0.0); - float g = getElevation(vUv + vec2(-offset, offset), 0.0); - float h = getElevation(vUv + vec2(0, offset), 0.0); - float i = getElevation(vUv + vec2(offset,offset), 0.0); - - - float normalLength = 500.0 / zoomlevel; - - vec3 v0 = vec3(0.0, 0.0, 0.0); - vec3 v1 = vec3(0.0, normalLength, 0.0); - vec3 v2 = vec3(normalLength, 0.0, 0.0); - v0.z = (e + d + g + h) / 4.0; - v1.z = (e+ b + a + d) / 4.0; - v2.z = (e+ h + i + f) / 4.0; - vNormal = (normalize(cross(v2 - v0, v1 - v0))).rbg; - } - `); - }; - return material; - } - static getTerrain(imageData, tileSize, elevation) { - const { rScaler, bScaler, gScaler, offset } = elevation; - const gridSize = tileSize + 1; - const terrain = new Float32Array(gridSize * gridSize); - for (let i = 0, y = 0; y < tileSize; y++) { - for (let x = 0; x < tileSize; x++, i++) { - const k = i * 4; - const r = imageData[k + 0]; - const g = imageData[k + 1]; - const b = imageData[k + 2]; - terrain[i + y] = r * rScaler + g * gScaler + b * bScaler + offset; - } - } - for (let i = gridSize * (gridSize - 1), x = 0; x < gridSize - 1; x++, i++) { - terrain[i] = terrain[i - gridSize]; - } - for (let i = gridSize - 1, y = 0; y < gridSize; y++, i += gridSize) { - terrain[i] = terrain[i - 1]; - } - return terrain; - } - static getMeshAttributes(vertices, terrain, tileSize, bounds, exageration) { - const gridSize = tileSize + 1; - const numOfVerticies = vertices.length / 2; - const positions = new Float32Array(numOfVerticies * 3); - const texCoords = new Float32Array(numOfVerticies * 2); - const [minX, minY, maxX, maxY] = bounds || [0, 0, tileSize, tileSize]; - const xScale = (maxX - minX) / tileSize; - const yScale = (maxY - minY) / tileSize; - for (let i = 0; i < numOfVerticies; i++) { - const x = vertices[i * 2]; - const y = vertices[i * 2 + 1]; - const pixelIdx = y * gridSize + x; - positions[3 * i + 0] = x * xScale + minX; - positions[3 * i + 1] = -terrain[pixelIdx] * exageration; - positions[3 * i + 2] = -y * yScale + maxY; - texCoords[2 * i + 0] = x / tileSize; - texCoords[2 * i + 1] = y / tileSize; - } - return { - position: { value: positions, size: 3 }, - uv: { value: texCoords, size: 2 } - }; - } - processHeight(image) { - return __awaiter(this, void 0, void 0, function* () { - const tileSize = image.width; - const gridSize = tileSize + 1; - var canvas = CanvasUtils.createOffscreenCanvas(tileSize, tileSize); - var context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, tileSize, tileSize, 0, 0, canvas.width, canvas.height); - var imageData = context.getImageData(0, 0, canvas.width, canvas.height); - var data = imageData.data; - const terrain = MapMartiniHeightNode.getTerrain(data, tileSize, this.elevationDecoder); - const martini = new Martini(gridSize); - const tile = martini.createTile(terrain); - const { vertices, triangles } = tile.getMesh(typeof this.meshMaxError === 'function' ? this.meshMaxError(this.level) : this.meshMaxError); - const attributes = MapMartiniHeightNode.getMeshAttributes(vertices, terrain, tileSize, [-0.5, -0.5, 0.5, 0.5], this.exageration); - this.geometry = new BufferGeometry(); - this.geometry.setIndex(new Uint32BufferAttribute(triangles, 1)); - this.geometry.setAttribute('position', new Float32BufferAttribute(attributes.position.value, attributes.position.size)); - this.geometry.setAttribute('uv', new Float32BufferAttribute(attributes.uv.value, attributes.uv.size)); - this.geometry.rotateX(Math.PI); - var texture = new Texture(image); - texture.generateMipmaps = false; - texture.format = RGBAFormat; - texture.magFilter = NearestFilter; - texture.minFilter = NearestFilter; - texture.needsUpdate = true; - this.material.userData.heightMap.value = texture; - this.material.map = texture; - this.material.needsUpdate = true; - }); - } - loadHeightGeometry() { - return __awaiter(this, void 0, void 0, function* () { - if (this.mapView.heightProvider === null) { - throw new Error('GeoThree: MapView.heightProvider provider is null.'); - } - const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - if (this.disposed) { - return; - } - this.processHeight(image); - this.heightLoaded = true; - this.nodeReady(); - }); - } -} -MapMartiniHeightNode.geometrySize = 16; -MapMartiniHeightNode.emptyTexture = new Texture(); -MapMartiniHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); +class MapMartiniHeightNode extends MapHeightNode { + constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0, { elevationDecoder = null, meshMaxError = 10, exageration = 1 } = {}) { + super(parentNode, mapView, location, level, x, y, MapMartiniHeightNode.geometry, MapMartiniHeightNode.prepareMaterial(new MeshPhongMaterial({ + map: MapMartiniHeightNode.emptyTexture, + color: 0xFFFFFF, + side: DoubleSide + }), level, exageration)); + this.elevationDecoder = { + rScaler: 256, + gScaler: 1, + bScaler: 1 / 256, + offset: -32768 + }; + this.exageration = 1.0; + this.meshMaxError = 10; + if (elevationDecoder) { + this.elevationDecoder = elevationDecoder; + } + this.meshMaxError = meshMaxError; + this.exageration = exageration; + this.frustumCulled = false; + } + static prepareMaterial(material, level, exageration = 1.0) { + material.userData = { + heightMap: { value: MapMartiniHeightNode.emptyTexture }, + drawNormals: { value: 0 }, + drawBlack: { value: 0 }, + zoomlevel: { value: level }, + computeNormals: { value: 1 }, + drawTexture: { value: 1 } + }; + material.onBeforeCompile = (shader) => { + for (let i in material.userData) { + shader.uniforms[i] = material.userData[i]; + } + shader.vertexShader = + ` + uniform bool computeNormals; + uniform float zoomlevel; + uniform sampler2D heightMap; + ` + shader.vertexShader; + shader.fragmentShader = + ` + uniform bool drawNormals; + uniform bool drawTexture; + uniform bool drawBlack; + ` + shader.fragmentShader; + shader.fragmentShader = shader.fragmentShader.replace('#include ', ` + if(drawBlack) { + gl_FragColor = vec4( 0.0,0.0,0.0, 1.0 ); + } else if(drawNormals) { + gl_FragColor = vec4( ( 0.5 * vNormal + 0.5 ), 1.0 ); + } else if (!drawTexture) { + gl_FragColor = vec4( 0.0,0.0,0.0, 0.0 ); + }`); + shader.vertexShader = shader.vertexShader.replace('#include ', ` + #include + + // queried pixels: + // +-----------+ + // | | | | + // | a | b | c | + // | | | | + // +-----------+ + // | | | | + // | d | e | f | + // | | | | + // +-----------+ + // | | | | + // | g | h | i | + // | | | | + // +-----------+ + + if (computeNormals) { + float e = getElevation(vUv, 0.0); + ivec2 size = textureSize(heightMap, 0); + float offset = 1.0 / float(size.x); + float a = getElevation(vUv + vec2(-offset, -offset), 0.0); + float b = getElevation(vUv + vec2(0, -offset), 0.0); + float c = getElevation(vUv + vec2(offset, -offset), 0.0); + float d = getElevation(vUv + vec2(-offset, 0), 0.0); + float f = getElevation(vUv + vec2(offset, 0), 0.0); + float g = getElevation(vUv + vec2(-offset, offset), 0.0); + float h = getElevation(vUv + vec2(0, offset), 0.0); + float i = getElevation(vUv + vec2(offset,offset), 0.0); + + + float normalLength = 500.0 / zoomlevel; + + vec3 v0 = vec3(0.0, 0.0, 0.0); + vec3 v1 = vec3(0.0, normalLength, 0.0); + vec3 v2 = vec3(normalLength, 0.0, 0.0); + v0.z = (e + d + g + h) / 4.0; + v1.z = (e+ b + a + d) / 4.0; + v2.z = (e+ h + i + f) / 4.0; + vNormal = (normalize(cross(v2 - v0, v1 - v0))).rbg; + } + `); + }; + return material; + } + static getTerrain(imageData, tileSize, elevation) { + const { rScaler, bScaler, gScaler, offset } = elevation; + const gridSize = tileSize + 1; + const terrain = new Float32Array(gridSize * gridSize); + for (let i = 0, y = 0; y < tileSize; y++) { + for (let x = 0; x < tileSize; x++, i++) { + const k = i * 4; + const r = imageData[k + 0]; + const g = imageData[k + 1]; + const b = imageData[k + 2]; + terrain[i + y] = r * rScaler + g * gScaler + b * bScaler + offset; + } + } + for (let i = gridSize * (gridSize - 1), x = 0; x < gridSize - 1; x++, i++) { + terrain[i] = terrain[i - gridSize]; + } + for (let i = gridSize - 1, y = 0; y < gridSize; y++, i += gridSize) { + terrain[i] = terrain[i - 1]; + } + return terrain; + } + static getMeshAttributes(vertices, terrain, tileSize, bounds, exageration) { + const gridSize = tileSize + 1; + const numOfVerticies = vertices.length / 2; + const positions = new Float32Array(numOfVerticies * 3); + const texCoords = new Float32Array(numOfVerticies * 2); + const [minX, minY, maxX, maxY] = bounds || [0, 0, tileSize, tileSize]; + const xScale = (maxX - minX) / tileSize; + const yScale = (maxY - minY) / tileSize; + for (let i = 0; i < numOfVerticies; i++) { + const x = vertices[i * 2]; + const y = vertices[i * 2 + 1]; + const pixelIdx = y * gridSize + x; + positions[3 * i + 0] = x * xScale + minX; + positions[3 * i + 1] = -terrain[pixelIdx] * exageration; + positions[3 * i + 2] = -y * yScale + maxY; + texCoords[2 * i + 0] = x / tileSize; + texCoords[2 * i + 1] = y / tileSize; + } + return { + position: { value: positions, size: 3 }, + uv: { value: texCoords, size: 2 } + }; + } + processHeight(image) { + return __awaiter(this, void 0, void 0, function* () { + const tileSize = image.width; + const gridSize = tileSize + 1; + var canvas = CanvasUtils.createOffscreenCanvas(tileSize, tileSize); + var context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, tileSize, tileSize, 0, 0, canvas.width, canvas.height); + var imageData = context.getImageData(0, 0, canvas.width, canvas.height); + var data = imageData.data; + const terrain = MapMartiniHeightNode.getTerrain(data, tileSize, this.elevationDecoder); + const martini = new Martini(gridSize); + const tile = martini.createTile(terrain); + const { vertices, triangles } = tile.getMesh(typeof this.meshMaxError === 'function' ? this.meshMaxError(this.level) : this.meshMaxError); + const attributes = MapMartiniHeightNode.getMeshAttributes(vertices, terrain, tileSize, [-0.5, -0.5, 0.5, 0.5], this.exageration); + this.geometry = new BufferGeometry(); + this.geometry.setIndex(new Uint32BufferAttribute(triangles, 1)); + this.geometry.setAttribute('position', new Float32BufferAttribute(attributes.position.value, attributes.position.size)); + this.geometry.setAttribute('uv', new Float32BufferAttribute(attributes.uv.value, attributes.uv.size)); + this.geometry.rotateX(Math.PI); + var texture = new Texture(image); + texture.generateMipmaps = false; + texture.format = RGBAFormat; + texture.magFilter = NearestFilter; + texture.minFilter = NearestFilter; + texture.needsUpdate = true; + this.material.userData.heightMap.value = texture; + this.material.map = texture; + this.material.needsUpdate = true; + }); + } + loadHeightGeometry() { + return __awaiter(this, void 0, void 0, function* () { + if (this.mapView.heightProvider === null) { + throw new Error('GeoThree: MapView.heightProvider provider is null.'); + } + const tileBuffer = yield this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { + const bitmap = yield createImageBitmap(new Blob([tileBuffer])); + if (this.disposed) { + return; + } + yield this.processHeight(bitmap); + } + else { + const image = yield this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + if (this.disposed) { + return; + } + yield this.processHeight(image); + } + this.heightLoaded = true; + this.nodeReady(); + }); + } +} +MapMartiniHeightNode.geometrySize = 16; +MapMartiniHeightNode.emptyTexture = new Texture(); +MapMartiniHeightNode.geometry = new MapNodeGeometry(1, 1, 1, 1); MapMartiniHeightNode.tileSize = 256; -class MapView extends Mesh { - constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null) { - super(undefined, new MeshBasicMaterial({ transparent: true, opacity: 0.0, depthWrite: false, colorWrite: false })); - this.lod = null; - this.provider = null; - this.heightProvider = null; - this.root = null; - this.cacheTiles = false; - this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { - this.lod.updateLOD(this, camera, renderer, scene); - }; - this.lod = new LODRaycast(); - this.provider = provider; - this.heightProvider = heightProvider; - this.setRoot(root); - this.preSubdivide(); - } - setRoot(root) { - if (typeof root === 'number') { - if (!MapView.mapModes.has(root)) { - throw new Error('Map mode ' + root + ' does is not registered.'); - } - const rootConstructor = MapView.mapModes.get(root); - root = new rootConstructor(null, this); - } - if (this.root !== null) { - this.remove(this.root); - this.root = null; - } - this.root = root; - if (this.root !== null) { - this.geometry = this.root.constructor.baseGeometry; - this.scale.copy(this.root.constructor.baseScale); - this.root.mapView = this; - this.add(this.root); - this.root.initialize(); - } - } - preSubdivide() { - var _a, _b; - function subdivide(node, depth) { - if (depth <= 0) { - return; - } - node.subdivide(); - for (let i = 0; i < node.children.length; i++) { - if (node.children[i] instanceof MapNode) { - const child = node.children[i]; - subdivide(child, depth - 1); - } - } - } - const minZoom = Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); - if (minZoom > 0) { - subdivide(this.root, minZoom); - } - } - setProvider(provider) { - if (provider !== this.provider) { - this.provider = provider; - this.clear(); - } - } - setHeightProvider(heightProvider) { - if (heightProvider !== this.heightProvider) { - this.heightProvider = heightProvider; - this.clear(); - } - } - clear() { - this.traverse(function (children) { - if (children.childrenCache) { - children.childrenCache = null; - } - if (children.initialize) { - children.initialize(); - } - }); - return this; - } - minZoom() { - var _a, _b; - return Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); - } - maxZoom() { - var _a, _b; - return Math.min(this.provider.maxZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.maxZoom) !== null && _b !== void 0 ? _b : Infinity); - } - getMetaData() { - this.provider.getMetaData(); - } - raycast(raycaster, intersects) { - return false; - } -} -MapView.PLANAR = 200; -MapView.SPHERICAL = 201; -MapView.HEIGHT = 202; -MapView.HEIGHT_SHADER = 203; -MapView.MARTINI = 204; -MapView.mapModes = new Map([ - [MapView.PLANAR, MapPlaneNode], - [MapView.SPHERICAL, MapSphereNode], - [MapView.HEIGHT, MapHeightNode], - [MapView.HEIGHT_SHADER, MapHeightNodeShader], - [MapView.MARTINI, MapMartiniHeightNode] +class MapView extends Mesh { + constructor(root = MapView.PLANAR, provider = new OpenStreetMapsProvider(), heightProvider = null, lod = new LODRaycast()) { + super(undefined, new MeshBasicMaterial({ transparent: true, opacity: 0.0, depthWrite: false, colorWrite: false })); + this.lod = null; + this.provider = null; + this.heightProvider = null; + this.root = null; + this.cacheTiles = false; + this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { + this.lod.updateLOD(this, camera, renderer, scene); + }; + this.lod = lod; + this.provider = provider; + this.heightProvider = heightProvider; + this.setRoot(root); + this.preSubdivide(); + } + setRoot(root) { + if (typeof root === 'number') { + if (!MapView.mapModes.has(root)) { + throw new Error('Map mode ' + root + ' does is not registered.'); + } + const rootConstructor = MapView.mapModes.get(root); + root = new rootConstructor(null, this); + } + if (this.root !== null) { + this.remove(this.root); + this.root = null; + } + this.root = root; + if (this.root !== null) { + this.geometry = this.root.constructor.baseGeometry; + this.scale.copy(this.root.constructor.baseScale); + this.root.mapView = this; + this.add(this.root); + this.root.initialize(); + } + } + preSubdivide() { + var _a, _b; + function subdivide(node, depth) { + if (depth <= 0) { + return; + } + node.subdivide(); + for (let i = 0; i < node.children.length; i++) { + if (node.children[i] instanceof MapNode) { + const child = node.children[i]; + subdivide(child, depth - 1); + } + } + } + const minZoom = Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); + if (minZoom > 0) { + subdivide(this.root, minZoom); + } + } + setProvider(provider) { + if (provider !== this.provider) { + this.provider = provider; + this.clear(); + } + } + setHeightProvider(heightProvider) { + if (heightProvider !== this.heightProvider) { + this.heightProvider = heightProvider; + this.clear(); + } + } + clear() { + this.traverse(function (children) { + if (children.childrenCache) { + children.childrenCache = null; + } + if (children.initialize) { + children.initialize(); + } + }); + return this; + } + minZoom() { + var _a, _b; + return Math.max(this.provider.minZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.minZoom) !== null && _b !== void 0 ? _b : -Infinity); + } + maxZoom() { + var _a, _b; + return Math.min(this.provider.maxZoom, (_b = (_a = this.heightProvider) === null || _a === void 0 ? void 0 : _a.maxZoom) !== null && _b !== void 0 ? _b : Infinity); + } + getMetaData() { + this.provider.getMetaData(); + } + raycast(raycaster, intersects) { + return false; + } +} +MapView.PLANAR = 200; +MapView.SPHERICAL = 201; +MapView.HEIGHT = 202; +MapView.HEIGHT_SHADER = 203; +MapView.MARTINI = 204; +MapView.mapModes = new Map([ + [MapView.PLANAR, MapPlaneNode], + [MapView.SPHERICAL, MapSphereNode], + [MapView.HEIGHT, MapHeightNode], + [MapView.HEIGHT_SHADER, MapHeightNodeShader], + [MapView.MARTINI, MapMartiniHeightNode] ]); -const pov$1 = new Vector3(); -const position$1 = new Vector3(); -class LODRadial { - constructor(subdivideDistance = 50, simplifyDistance = 300) { - this.subdivideDistance = subdivideDistance; - this.simplifyDistance = simplifyDistance; - } - updateLOD(view, camera, renderer, scene) { - camera.getWorldPosition(pov$1); - view.children[0].traverse((node) => { - node.getWorldPosition(position$1); - let distance = pov$1.distanceTo(position$1); - distance /= Math.pow(2, view.provider.maxZoom - node.level); - if (distance < this.subdivideDistance) { - node.subdivide(); - } - else if (distance > this.simplifyDistance && node.parentNode) { - node.parentNode.simplify(); - } - }); - } +const pov$2 = new Vector3(); +const position$2 = new Vector3(); +class LODRadial { + constructor(subdivideDistance = 50, simplifyDistance = 300) { + this.subdivideDistance = subdivideDistance; + this.simplifyDistance = simplifyDistance; + } + updateLOD(view, camera, renderer, scene) { + camera.getWorldPosition(pov$2); + view.children[0].traverse((node) => { + node.getWorldPosition(position$2); + let distance = pov$2.distanceTo(position$2); + distance /= Math.pow(2, view.provider.maxZoom - node.level); + if (distance < this.subdivideDistance) { + node.subdivide(); + } + else if (distance > this.simplifyDistance && node.parentNode) { + node.parentNode.simplify(); + } + }); + } +} + +const projection$1 = new Matrix4(); +const pov$1 = new Vector3(); +const frustum$1 = new Frustum(); +const position$1 = new Vector3(); +class LODFrustum extends LODRadial { + constructor(subdivideDistance = 120, simplifyDistance = 400) { + super(subdivideDistance, simplifyDistance); + this.testCenter = true; + this.pointOnly = false; + } + updateLOD(view, camera, renderer, scene) { + projection$1.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum$1.setFromProjectionMatrix(projection$1); + camera.getWorldPosition(pov$1); + view.children[0].traverse((node) => { + node.getWorldPosition(position$1); + let distance = pov$1.distanceTo(position$1); + distance /= Math.pow(2, view.provider.maxZoom - node.level); + const inFrustum = this.pointOnly ? frustum$1.containsPoint(position$1) : frustum$1.intersectsObject(node); + if (distance < this.subdivideDistance && inFrustum) { + node.subdivide(); + } + else if (distance > this.simplifyDistance && node.parentNode) { + node.parentNode.simplify(); + } + }); + } } -const projection = new Matrix4(); -const pov = new Vector3(); -const frustum = new Frustum(); -const position = new Vector3(); -class LODFrustum extends LODRadial { - constructor(subdivideDistance = 120, simplifyDistance = 400) { - super(subdivideDistance, simplifyDistance); - this.testCenter = true; - this.pointOnly = false; - } - updateLOD(view, camera, renderer, scene) { - projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); - frustum.setFromProjectionMatrix(projection); - camera.getWorldPosition(pov); - view.children[0].traverse((node) => { - node.getWorldPosition(position); - let distance = pov.distanceTo(position); - distance /= Math.pow(2, view.provider.maxZoom - node.level); - const inFrustum = this.pointOnly ? frustum.containsPoint(position) : frustum.intersectsObject(node); - if (distance < this.subdivideDistance && inFrustum) { - node.subdivide(); - } - else if (distance > this.simplifyDistance && node.parentNode) { - node.parentNode.simplify(); - } - }); - } +const projection = new Matrix4(); +const pov = new Vector3(); +const frustum = new Frustum(); +const position = new Vector3(); +const zoomLevelPixelRatios = [ + 78271.484, 39135.742, 19567.871, 9783.936, 4891.968, 2445.984, 1222.992, + 611.496, 305.748, 152.874, 76.437, 38.218, 19.109, 9.555, 4.777, 2.389, 1.194, + 0.597, 0.299, 0.149, 0.075, 0.037, 0.019 +]; +class LODFrustumOrthographic extends LODFrustum { + updateLOD(view, camera, renderer, scene) { + const isOrthographic = camera.isOrthographicCamera; + if (!isOrthographic) { + super.updateLOD(view, camera, renderer, scene); + return; + } + projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum.setFromProjectionMatrix(projection); + camera.getWorldPosition(pov); + view.children[0].traverse((obj) => { + var _a; + const node = obj; + node.getWorldPosition(position); + const nodeBox = new Box3().setFromObject(node); + let distance = nodeBox.distanceToPoint(pov); + distance /= Math.pow(2, (view.provider.maxZoom - node.level)); + const inFrustum = frustum.intersectsObject(node); + if (inFrustum) { + const metresPerPixel = 1 / camera.zoom; + let closestZoomLevel = 0; + let minDifference = Number.POSITIVE_INFINITY; + for (let i = 0; i < zoomLevelPixelRatios.length; i++) { + const difference = Math.abs(zoomLevelPixelRatios[i] - metresPerPixel); + if (difference < minDifference) { + minDifference = difference; + closestZoomLevel = i; + } + } + if (node.level < closestZoomLevel) { + if (!(node.children.length > 0)) { + node.subdivide(); + } + } + else if (node.level > closestZoomLevel) { + (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.simplify(); + } + } + }); + } } -class XHRUtils { - static get(url) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise(function (resolve, reject) { - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.open('GET', url, true); - xhr.onload = function () { - resolve(xhr.response); - }; - xhr.onerror = reject; - xhr.send(null); - }); - }); - } - static getRaw(url) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - xhr.responseType = 'arraybuffer'; - xhr.open('GET', url, true); - xhr.onload = function () { - resolve(xhr.response); - }; - xhr.onerror = reject; - xhr.send(null); - }); - }); - } - static request(url, type, header, body, onLoad, onError, onProgress) { - function parseResponse(response) { - try { - return JSON.parse(response); - } - catch (e) { - return response; - } - } - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.open(type, url, true); - if (header !== null && header !== undefined) { - for (const i in header) { - xhr.setRequestHeader(i, header[i]); - } - } - if (onLoad !== undefined) { - xhr.onload = function (event) { - onLoad(parseResponse(xhr.response), xhr); - }; - } - if (onError !== undefined) { - xhr.onerror = onError; - } - if (onProgress !== undefined) { - xhr.onprogress = onProgress; - } - xhr.send(body !== undefined ? body : null); - return xhr; - } +class XHRUtils { + static get(url) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise(function (resolve, reject) { + const xhr = new XMLHttpRequest(); + xhr.overrideMimeType('text/plain'); + xhr.open('GET', url, true); + xhr.onload = function () { + resolve(xhr.response); + }; + xhr.onerror = reject; + xhr.send(null); + }); + }); + } + static getRaw(url) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + xhr.open('GET', url, true); + xhr.onload = function () { + resolve(xhr.response); + }; + xhr.onerror = reject; + xhr.send(null); + }); + }); + } + static request(url, type, header, body, onLoad, onError, onProgress) { + function parseResponse(response) { + try { + return JSON.parse(response); + } + catch (e) { + return response; + } + } + const xhr = new XMLHttpRequest(); + xhr.overrideMimeType('text/plain'); + xhr.open(type, url, true); + if (header !== null && header !== undefined) { + for (const i in header) { + xhr.setRequestHeader(i, header[i]); + } + } + if (onLoad !== undefined) { + xhr.onload = function (event) { + onLoad(parseResponse(xhr.response), xhr); + }; + } + if (onError !== undefined) { + xhr.onerror = onError; + } + if (onProgress !== undefined) { + xhr.onprogress = onProgress; + } + xhr.send(body !== undefined ? body : null); + return xhr; + } } -class BingMapsProvider extends MapProvider { - constructor(apiKey = '', type = BingMapsProvider.AERIAL) { - super(); - this.maxZoom = 19; - this.minZoom = 1; - this.format = 'jpeg'; - this.mapSize = 512; - this.subdomain = 't1'; - this.meta = null; - this.apiKey = apiKey; - this.type = type; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = BingMapsProvider.ADDRESS + '/REST/V1/Imagery/Metadata/RoadOnDemand?output=json&include=ImageryProviders&key=' + this.apiKey; - const data = yield XHRUtils.get(address); - this.meta = JSON.parse(data); - }); - } - static quadKey(zoom, x, y) { - let quad = ''; - for (let i = zoom; i > 0; i--) { - const mask = 1 << i - 1; - let cell = 0; - if ((x & mask) !== 0) { - cell++; - } - if ((y & mask) !== 0) { - cell += 2; - } - quad += cell; - } - return quad; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'http://ecn.' + this.subdomain + '.tiles.virtualearth.net/tiles/' + this.type + BingMapsProvider.quadKey(zoom, x, y) + '.jpeg?g=1173'; - }); - } -} -BingMapsProvider.ADDRESS = 'https://dev.virtualearth.net'; -BingMapsProvider.AERIAL = 'a'; -BingMapsProvider.ROAD = 'r'; -BingMapsProvider.AERIAL_LABELS = 'h'; -BingMapsProvider.OBLIQUE = 'o'; +class BingMapsProvider extends MapProvider { + constructor(apiKey = '', type = BingMapsProvider.AERIAL) { + super(); + this.maxZoom = 19; + this.minZoom = 1; + this.format = 'jpeg'; + this.mapSize = 512; + this.subdomain = 't1'; + this.meta = null; + this.apiKey = apiKey; + this.type = type; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = BingMapsProvider.ADDRESS + '/REST/V1/Imagery/Metadata/RoadOnDemand?output=json&include=ImageryProviders&key=' + this.apiKey; + const data = yield XHRUtils.get(address); + this.meta = JSON.parse(data); + }); + } + static quadKey(zoom, x, y) { + let quad = ''; + for (let i = zoom; i > 0; i--) { + const mask = 1 << i - 1; + let cell = 0; + if ((x & mask) !== 0) { + cell++; + } + if ((y & mask) !== 0) { + cell += 2; + } + quad += cell; + } + return quad; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'http://ecn.' + this.subdomain + '.tiles.virtualearth.net/tiles/' + this.type + BingMapsProvider.quadKey(zoom, x, y) + '.jpeg?g=1173'; + }); + } +} +BingMapsProvider.ADDRESS = 'https://dev.virtualearth.net'; +BingMapsProvider.AERIAL = 'a'; +BingMapsProvider.ROAD = 'r'; +BingMapsProvider.AERIAL_LABELS = 'h'; +BingMapsProvider.OBLIQUE = 'o'; BingMapsProvider.OBLIQUE_LABELS = 'b'; -class GoogleMapsProvider extends MapProvider { - constructor(apiToken) { - super(); - this.sessionToken = null; - this.orientation = 0; - this.format = 'png'; - this.mapType = 'roadmap'; - this.overlay = false; - this.apiToken = apiToken !== undefined ? apiToken : ''; - this.createSession(); - } - createSession() { - const address = 'https://tile.googleapis.com/v1/createSession?key=' + this.apiToken; - const data = JSON.stringify({ - mapType: this.mapType, - language: 'en-EN', - region: 'en', - layerTypes: ['layerRoadmap', 'layerStreetview'], - overlay: this.overlay, - scale: 'scaleFactor1x' - }); - XHRUtils.request(address, 'POST', { 'Content-Type': 'text/json' }, data, (response, xhr) => { - this.sessionToken = response.session; - }, function (xhr) { - throw new Error('Unable to create a google maps session.'); - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://tile.googleapis.com/v1/2dtiles/' + zoom + '/' + x + '/' + y + '?session=' + this.sessionToken + '&orientation=' + this.orientation + '&key=' + this.apiToken; - }); - } +class GoogleMapsProvider extends MapProvider { + constructor(apiToken) { + super(); + this.sessionToken = null; + this.orientation = 0; + this.format = 'png'; + this.mapType = 'roadmap'; + this.overlay = false; + this.apiToken = apiToken !== undefined ? apiToken : ''; + this.createSession(); + } + createSession() { + const address = 'https://tile.googleapis.com/v1/createSession?key=' + this.apiToken; + const data = JSON.stringify({ + mapType: this.mapType, + language: 'en-EN', + region: 'en', + layerTypes: ['layerRoadmap', 'layerStreetview'], + overlay: this.overlay, + scale: 'scaleFactor1x' + }); + XHRUtils.request(address, 'POST', { 'Content-Type': 'text/json' }, data, (response, xhr) => { + this.sessionToken = response.session; + }, function (xhr) { + throw new Error('Unable to create a google maps session.'); + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://tile.googleapis.com/v1/2dtiles/' + zoom + '/' + x + '/' + y + '?session=' + this.sessionToken + '&orientation=' + this.orientation + '&key=' + this.apiToken; + }); + } } -class HereMapsProvider extends MapProvider { - constructor(appId = '', appCode = '', style = 'base', scheme = 'normal.day', format = 'png', size = 512) { - super(); - this.appId = appId; - this.appCode = appCode; - this.style = style; - this.scheme = scheme; - this.format = format; - this.size = size; - this.version = 'newest'; - this.server = 1; - } - nextServer() { - this.server = this.server % 4 === 0 ? 1 : this.server + 1; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { }); - } - fetchTile(zoom, x, y) { - this.nextServer(); - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://' + this.server + '.' + this.style + '.maps.api.here.com/maptile/2.1/maptile/' + - this.version + '/' + this.scheme + '/' + zoom + '/' + x + '/' + y + '/' + - this.size + '/' + this.format + '?app_id=' + this.appId + '&app_code=' + this.appCode; - }); - } -} +class HereMapsProvider extends MapProvider { + constructor(appId = '', appCode = '', style = 'base', scheme = 'normal.day', format = 'png', size = 512) { + super(); + this.appId = appId; + this.appCode = appCode; + this.style = style; + this.scheme = scheme; + this.format = format; + this.size = size; + this.version = 'newest'; + this.server = 1; + } + nextServer() { + this.server = this.server % 4 === 0 ? 1 : this.server + 1; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { }); + } + fetchTile(zoom, x, y) { + this.nextServer(); + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://' + this.server + '.' + this.style + '.maps.api.here.com/maptile/2.1/maptile/' + + this.version + '/' + this.scheme + '/' + zoom + '/' + x + '/' + y + '/' + + this.size + '/' + this.format + '?app_id=' + this.appId + '&app_code=' + this.appCode; + }); + } +} HereMapsProvider.PATH = '/maptile/2.1/'; -class MapBoxProvider extends MapProvider { - constructor(apiToken = '', id = '', mode = MapBoxProvider.STYLE, format = 'png', useHDPI = false, version = 'v4') { - super(); - this.apiToken = apiToken; - this.format = format; - this.useHDPI = useHDPI; - this.mode = mode; - this.mapId = id; - this.style = id; - this.version = version; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = MapBoxProvider.ADDRESS + this.version + '/' + this.mapId + '.json?access_token=' + this.apiToken; - const data = yield XHRUtils.get(address); - const meta = JSON.parse(data); - this.name = meta.name; - this.minZoom = meta.minZoom; - this.maxZoom = meta.maxZoom; - this.bounds = meta.bounds; - this.center = meta.center; - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - if (this.mode === MapBoxProvider.STYLE) { - image.src = MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; - } - else { - image.src = MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; - } - }); - } -} -MapBoxProvider.ADDRESS = 'https://api.mapbox.com/'; -MapBoxProvider.STYLE = 100; +class MapBoxProvider extends MapProvider { + constructor(apiToken = '', id = '', mode = MapBoxProvider.STYLE, format = 'png', useHDPI = false, version = 'v4') { + super(); + this.apiToken = apiToken; + this.format = format; + this.useHDPI = useHDPI; + this.mode = mode; + this.mapId = id; + this.style = id; + this.version = version; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = MapBoxProvider.ADDRESS + this.version + '/' + this.mapId + '.json?access_token=' + this.apiToken; + const data = yield XHRUtils.get(address); + const meta = JSON.parse(data); + this.name = meta.name; + this.minZoom = meta.minZoom; + this.maxZoom = meta.maxZoom; + this.bounds = meta.bounds; + this.center = meta.center; + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.getTileUrl(zoom, x, y); + }); + } + fetchTileBuffer(zoom, x, y) { + return __awaiter(this, void 0, void 0, function* () { + const response = yield fetch(this.getTileUrl(zoom, x, y)); + if (!response.ok) { + throw new Error('MapBoxProvider: Failed to fetch tile buffer. Status ' + response.status); + } + return response.arrayBuffer(); + }); + } + getTileUrl(zoom, x, y) { + if (this.mode === MapBoxProvider.STYLE) { + return MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; + } + else { + return MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; + } + } +} +MapBoxProvider.ADDRESS = 'https://api.mapbox.com/'; +MapBoxProvider.STYLE = 100; MapBoxProvider.MAP_ID = 101; -class MapTilerProvider extends MapProvider { - constructor(apiKey, category, style, format) { - super(); - this.apiKey = apiKey !== undefined ? apiKey : ''; - this.format = format !== undefined ? format : 'png'; - this.category = category !== undefined ? category : 'maps'; - this.style = style !== undefined ? style : 'satellite'; - this.resolution = 512; - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = 'https://api.maptiler.com/' + this.category + '/' + this.style + '/' + zoom + '/' + x + '/' + y + '.' + this.format + '?key=' + this.apiKey; - }); - } +class MapTilerProvider extends MapProvider { + constructor(apiKey, category, style, format) { + super(); + this.apiKey = apiKey !== undefined ? apiKey : ''; + this.format = format !== undefined ? format : 'png'; + this.category = category !== undefined ? category : 'maps'; + this.style = style !== undefined ? style : 'satellite'; + this.resolution = 512; + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = 'https://api.maptiler.com/' + this.category + '/' + this.style + '/' + zoom + '/' + x + '/' + y + '.' + this.format + '?key=' + this.apiKey; + }); + } } -class OpenMapTilesProvider extends MapProvider { - constructor(address, format = 'png', theme = 'klokantech-basic') { - super(); - this.address = address; - this.format = format; - this.theme = theme; - } - getMetaData() { - return __awaiter(this, void 0, void 0, function* () { - const address = this.address + 'styles/' + this.theme + '.json'; - const data = yield XHRUtils.get(address); - const meta = JSON.parse(data); - this.name = meta.name; - this.format = meta.format; - this.minZoom = meta.minZoom; - this.maxZoom = meta.maxZoom; - this.bounds = meta.bounds; - this.center = meta.center; - }); - } - fetchTile(zoom, x, y) { - return new Promise((resolve, reject) => { - const image = document.createElement('img'); - image.onload = function () { - resolve(image); - }; - image.onerror = function () { - reject(); - }; - image.crossOrigin = 'Anonymous'; - image.src = this.address + 'styles/' + this.theme + '/' + zoom + '/' + x + '/' + y + '.' + this.format; - }); - } +class OpenMapTilesProvider extends MapProvider { + constructor(address, format = 'png', theme = 'klokantech-basic') { + super(); + this.address = address; + this.format = format; + this.theme = theme; + } + getMetaData() { + return __awaiter(this, void 0, void 0, function* () { + const address = this.address + 'styles/' + this.theme + '.json'; + const data = yield XHRUtils.get(address); + const meta = JSON.parse(data); + this.name = meta.name; + this.format = meta.format; + this.minZoom = meta.minZoom; + this.maxZoom = meta.maxZoom; + this.bounds = meta.bounds; + this.center = meta.center; + }); + } + fetchTile(zoom, x, y) { + return new Promise((resolve, reject) => { + const image = document.createElement('img'); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(); + }; + image.crossOrigin = 'Anonymous'; + image.src = this.address + 'styles/' + this.theme + '/' + zoom + '/' + x + '/' + y + '.' + this.format; + }); + } } -class DebugProvider extends MapProvider { - constructor() { - super(...arguments); - this.resolution = 256; - } - fetchTile(zoom, x, y) { - const canvas = CanvasUtils.createOffscreenCanvas(this.resolution, this.resolution); - const context = canvas.getContext('2d'); - const green = new Color(0x00ff00); - const red = new Color(0xff0000); - const color = green.lerpHSL(red, (zoom - this.minZoom) / (this.maxZoom - this.minZoom)); - context.fillStyle = color.getStyle(); - context.fillRect(0, 0, this.resolution, this.resolution); - context.fillStyle = '#000000'; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.font = 'bold ' + this.resolution * 0.1 + 'px arial'; - context.fillText('(' + zoom + ')', this.resolution / 2, this.resolution * 0.4); - context.fillText('(' + x + ', ' + y + ')', this.resolution / 2, this.resolution * 0.6); - return Promise.resolve(canvas); - } +class DebugProvider extends MapProvider { + constructor() { + super(...arguments); + this.resolution = 256; + } + fetchTile(zoom, x, y) { + const canvas = CanvasUtils.createOffscreenCanvas(this.resolution, this.resolution); + const context = canvas.getContext('2d'); + const green = new Color(0x00ff00); + const red = new Color(0xff0000); + const color = green.lerpHSL(red, (zoom - this.minZoom) / (this.maxZoom - this.minZoom)); + context.fillStyle = color.getStyle(); + context.fillRect(0, 0, this.resolution, this.resolution); + context.fillStyle = '#000000'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.font = 'bold ' + this.resolution * 0.1 + 'px arial'; + context.fillText('(' + zoom + ')', this.resolution / 2, this.resolution * 0.4); + context.fillText('(' + x + ', ' + y + ')', this.resolution / 2, this.resolution * 0.6); + return Promise.resolve(canvas); + } } -class HeightDebugProvider extends MapProvider { - constructor(provider) { - super(); - this.fromColor = new Color(0xff0000); - this.toColor = new Color(0x00ff00); - this.provider = provider; - } - fetchTile(zoom, x, y) { - return __awaiter(this, void 0, void 0, function* () { - const image = yield this.provider.fetchTile(zoom, x, y); - const resolution = 256; - const canvas = CanvasUtils.createOffscreenCanvas(resolution, resolution); - const context = canvas.getContext('2d'); - context.drawImage(image, 0, 0, resolution, resolution, 0, 0, resolution, resolution); - const imageData = context.getImageData(0, 0, resolution, resolution); - const data = imageData.data; - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; - const max = 1667721.6; - const color = this.fromColor.clone().lerpHSL(this.toColor, value / max); - data[i] = color.r * 255; - data[i + 1] = color.g * 255; - data[i + 2] = color.b * 255; - } - context.putImageData(imageData, 0, 0); - return canvas; - }); - } +class HeightDebugProvider extends MapProvider { + constructor(provider) { + super(); + this.fromColor = new Color(0xff0000); + this.toColor = new Color(0x00ff00); + this.provider = provider; + } + fetchTile(zoom, x, y) { + return __awaiter(this, void 0, void 0, function* () { + const image = yield this.provider.fetchTile(zoom, x, y); + const resolution = 256; + const canvas = CanvasUtils.createOffscreenCanvas(resolution, resolution); + const context = canvas.getContext('2d'); + context.drawImage(image, 0, 0, resolution, resolution, 0, 0, resolution, resolution); + const imageData = context.getImageData(0, 0, resolution, resolution); + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const value = (r * 65536 + g * 256 + b) * 0.1 - 1e4; + const max = 1667721.6; + const color = this.fromColor.clone().lerpHSL(this.toColor, value / max); + data[i] = color.r * 255; + data[i + 1] = color.g * 255; + data[i + 2] = color.b * 255; + } + context.putImageData(imageData, 0, 0); + return canvas; + }); + } } -class GeolocationUtils { - static get() { - return new Promise(function (resolve, reject) { - navigator.geolocation.getCurrentPosition(function (result) { - resolve(result); - }, reject); - }); - } +class GeolocationUtils { + static get() { + return new Promise(function (resolve, reject) { + navigator.geolocation.getCurrentPosition(function (result) { + resolve(result); + }, reject); + }); + } } -class CancelablePromise { - constructor(executor) { - this.fulfilled = false; - this.rejected = false; - this.called = false; - const resolve = (v) => { - this.fulfilled = true; - this.value = v; - if (typeof this.onResolve === 'function') { - this.onResolve(this.value); - this.called = true; - } - }; - const reject = (reason) => { - this.rejected = true; - this.value = reason; - if (typeof this.onReject === 'function') { - this.onReject(this.value); - this.called = true; - } - }; - try { - executor(resolve, reject); - } - catch (error) { - reject(error); - } - } - cancel() { - return false; - } - then(callback) { - this.onResolve = callback; - if (this.fulfilled && !this.called) { - this.called = true; - this.onResolve(this.value); - } - return this; - } - catch(callback) { - this.onReject = callback; - if (this.rejected && !this.called) { - this.called = true; - this.onReject(this.value); - } - return this; - } - finally(callback) { - return this; - } - static resolve(val) { - return new CancelablePromise(function executor(resolve, _reject) { - resolve(val); - }); - } - static reject(reason) { - return new CancelablePromise(function executor(resolve, reject) { - reject(reason); - }); - } - static all(promises) { - const fulfilledPromises = []; - const result = []; - function executor(resolve, reject) { - promises.forEach((promise, index) => { - return promise - .then((val) => { - fulfilledPromises.push(true); - result[index] = val; - if (fulfilledPromises.length === promises.length) { - return resolve(result); - } - }) - .catch((error) => { return reject(error); }); - }); - } - return new CancelablePromise(executor); - } +class CancelablePromise { + constructor(executor) { + this.fulfilled = false; + this.rejected = false; + this.called = false; + const resolve = (v) => { + this.fulfilled = true; + this.value = v; + if (typeof this.onResolve === 'function') { + this.onResolve(this.value); + this.called = true; + } + }; + const reject = (reason) => { + this.rejected = true; + this.value = reason; + if (typeof this.onReject === 'function') { + this.onReject(this.value); + this.called = true; + } + }; + try { + executor(resolve, reject); + } + catch (error) { + reject(error); + } + } + cancel() { + return false; + } + then(callback) { + this.onResolve = callback; + if (this.fulfilled && !this.called) { + this.called = true; + this.onResolve(this.value); + } + return this; + } + catch(callback) { + this.onReject = callback; + if (this.rejected && !this.called) { + this.called = true; + this.onReject(this.value); + } + return this; + } + finally(callback) { + return this; + } + static resolve(val) { + return new CancelablePromise(function executor(resolve, _reject) { + resolve(val); + }); + } + static reject(reason) { + return new CancelablePromise(function executor(resolve, reject) { + reject(reason); + }); + } + static all(promises) { + const fulfilledPromises = []; + const result = []; + function executor(resolve, reject) { + promises.forEach((promise, index) => { + return promise + .then((val) => { + fulfilledPromises.push(true); + result[index] = val; + if (fulfilledPromises.length === promises.length) { + return resolve(result); + } + }) + .catch((error) => { return reject(error); }); + }); + } + return new CancelablePromise(executor); + } } -export { BingMapsProvider, CancelablePromise, CanvasUtils, DebugProvider, Geolocation, GeolocationUtils, GoogleMapsProvider, HeightDebugProvider, HereMapsProvider, LODFrustum, LODRadial, LODRaycast, MapBoxProvider, MapHeightNode, MapHeightNodeShader, MapNode, MapNodeGeometry, MapNodeHeightGeometry, MapPlaneNode, MapProvider, MapSphereNode, MapSphereNodeGeometry, MapTilerProvider, MapView, OpenMapTilesProvider, OpenStreetMapsProvider, QuadTreePosition, TextureUtils, UnitsUtils, XHRUtils }; +export { BingMapsProvider, CancelablePromise, CanvasUtils, DebugProvider, Geolocation, GeolocationUtils, GoogleMapsProvider, HeightDebugProvider, HereMapsProvider, LODFrustum, LODFrustumOrthographic, LODRadial, LODRaycast, MapBoxProvider, MapHeightNode, MapHeightNodeShader, MapNode, MapNodeGeometry, MapNodeHeightGeometry, MapPlaneNode, MapProvider, MapSphereNode, MapSphereNodeGeometry, MapTilerProvider, MapView, OpenMapTilesProvider, OpenStreetMapsProvider, QuadTreePosition, TextureUtils, UnitsUtils, XHRUtils }; diff --git a/build/lod/LODFrustumOrthographic.d.ts b/build/lod/LODFrustumOrthographic.d.ts new file mode 100644 index 0000000..5f0b9e8 --- /dev/null +++ b/build/lod/LODFrustumOrthographic.d.ts @@ -0,0 +1,6 @@ +import { type Camera, type Object3D, type WebGLRenderer } from 'three'; +import { LODFrustum } from './LODFrustum'; +import { MapView } from '../MapView'; +export declare class LODFrustumOrthographic extends LODFrustum { + updateLOD(view: MapView, camera: Camera, renderer: WebGLRenderer, scene: Object3D): void; +} diff --git a/build/nodes/MapMartiniHeightNode.d.ts b/build/nodes/MapMartiniHeightNode.d.ts index b9299c2..5774443 100644 --- a/build/nodes/MapMartiniHeightNode.d.ts +++ b/build/nodes/MapMartiniHeightNode.d.ts @@ -28,6 +28,6 @@ export declare class MapMartiniHeightNode extends MapHeightNode { size: number; }; }; - processHeight(image: HTMLImageElement): Promise; + processHeight(image: HTMLImageElement | ImageBitmap): Promise; loadHeightGeometry(): Promise; } diff --git a/build/providers/MapBoxProvider.d.ts b/build/providers/MapBoxProvider.d.ts index c5ab10a..239dfd6 100644 --- a/build/providers/MapBoxProvider.d.ts +++ b/build/providers/MapBoxProvider.d.ts @@ -13,4 +13,6 @@ export declare class MapBoxProvider extends MapProvider { constructor(apiToken?: string, id?: string, mode?: number, format?: string, useHDPI?: boolean, version?: string); getMetaData(): Promise; fetchTile(zoom: number, x: number, y: number): Promise; + fetchTileBuffer(zoom: number, x: number, y: number): Promise; + private getTileUrl; } diff --git a/build/providers/MapProvider.d.ts b/build/providers/MapProvider.d.ts index bdb7eb3..c09f225 100644 --- a/build/providers/MapProvider.d.ts +++ b/build/providers/MapProvider.d.ts @@ -5,5 +5,6 @@ export declare abstract class MapProvider { bounds: number[]; center: number[]; fetchTile(zoom: number, x: number, y: number): Promise; + fetchTileBuffer(zoom: number, x: number, y: number): Promise; getMetaData(): Promise; } diff --git a/build/utils/PNGDecoder.d.ts b/build/utils/PNGDecoder.d.ts new file mode 100644 index 0000000..8110f63 --- /dev/null +++ b/build/utils/PNGDecoder.d.ts @@ -0,0 +1,8 @@ +export declare class PNGDecoder { + private static readonly SIGNATURE; + static decode(buffer: ArrayBuffer): Promise; + private static decompress; + private static reconstruct; + private static paeth; + static scaleImageData(src: ImageData, dstWidth: number, dstHeight: number): ImageData; +} diff --git a/source/nodes/MapHeightNode.ts b/source/nodes/MapHeightNode.ts index a7b9194..a022d95 100644 --- a/source/nodes/MapHeightNode.ts +++ b/source/nodes/MapHeightNode.ts @@ -121,20 +121,47 @@ export class MapHeightNode extends MapNode try { - const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - - if (this.disposed) + let imageData: ImageData; + + const tileBuffer = await this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { - return; + // Use the browser to decode the image from the raw bytes, bypassing the element + // to avoid fingerprinting noise introduced by Firefox Enhanced Tracking Protection + const bitmap = await createImageBitmap(new Blob([tileBuffer])); + + if (this.disposed) + { + return; + } + + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d') as CanvasRenderingContext2D; + context.imageSmoothingEnabled = false; + context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + } + else + { + // Fallback: load image via HTML element and use canvas for pixel read + const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + + if (this.disposed) + { + return; + } + + const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); + const context = canvas.getContext('2d') as CanvasRenderingContext2D; + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); } - const canvas = CanvasUtils.createOffscreenCanvas(this.geometrySize + 1, this.geometrySize + 1); - - const context = canvas.getContext('2d') as CanvasRenderingContext2D; - context.imageSmoothingEnabled = false; - context.drawImage(image, 0, 0, MapHeightNode.tileSize, MapHeightNode.tileSize, 0, 0, canvas.width, canvas.height); - - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + if (this.disposed) + { + return; + } this.geometry = new MapNodeHeightGeometry(1, 1, this.geometrySize, this.geometrySize, true, 10.0, imageData, true); } diff --git a/source/nodes/MapHeightNodeShader.ts b/source/nodes/MapHeightNodeShader.ts index 8791157..ee9d06e 100644 --- a/source/nodes/MapHeightNodeShader.ts +++ b/source/nodes/MapHeightNodeShader.ts @@ -125,14 +125,40 @@ export class MapHeightNodeShader extends MapHeightNode try { - const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + let texture: Texture; + + const tileBuffer = await this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) + { + // Use the browser to decode the image from the raw bytes, bypassing the element + // to avoid fingerprinting noise introduced by Firefox Enhanced Tracking Protection + const bitmap = await createImageBitmap(new Blob([tileBuffer])); + + if (this.disposed) + { + return; + } + + texture = new Texture(bitmap as any); + } + else + { + // Fallback: load image via HTML element + const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); + + if (this.disposed) + { + return; + } + + texture = new Texture(image as any); + } if (this.disposed) { return; } - const texture = new Texture(image as any); texture.generateMipmaps = false; texture.format = RGBAFormat; texture.magFilter = NearestFilter; diff --git a/source/nodes/MapMartiniHeightNode.ts b/source/nodes/MapMartiniHeightNode.ts index e9f7095..b2c9a01 100644 --- a/source/nodes/MapMartiniHeightNode.ts +++ b/source/nodes/MapMartiniHeightNode.ts @@ -283,9 +283,9 @@ export class MapMartiniHeightNode extends MapHeightNode /** * Process the height texture received from the tile data provider. * - * @param image - Image element received by the tile provider. + * @param image - Image element or ImageBitmap received by the tile provider. */ - public async processHeight(image: HTMLImageElement): Promise + public async processHeight(image: HTMLImageElement | ImageBitmap): Promise { const tileSize = image.width; const gridSize = tileSize + 1; @@ -311,7 +311,7 @@ export class MapMartiniHeightNode extends MapHeightNode this.geometry.setAttribute('uv', new Float32BufferAttribute( attributes.uv.value, attributes.uv.size)); this.geometry.rotateX(Math.PI); - var texture = new Texture(image); + var texture = new Texture(image as any); texture.generateMipmaps = false; texture.format = RGBAFormat; texture.magFilter = NearestFilter; @@ -335,14 +335,32 @@ export class MapMartiniHeightNode extends MapHeightNode throw new Error('GeoThree: MapView.heightProvider provider is null.'); } - const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - - if (this.disposed) + const tileBuffer = await this.mapView.heightProvider.fetchTileBuffer(this.level, this.x, this.y); + if (tileBuffer !== null) { - return; + // Use the browser to decode the image from the raw bytes, bypassing the element + // to avoid fingerprinting noise introduced by Firefox Enhanced Tracking Protection + const bitmap = await createImageBitmap(new Blob([tileBuffer])); + + if (this.disposed) + { + return; + } + + await this.processHeight(bitmap); } + else + { + // Fallback: load image via HTML element and use canvas for pixel read + const image = await this.mapView.heightProvider.fetchTile(this.level, this.x, this.y); - this.processHeight(image); + if (this.disposed) + { + return; + } + + await this.processHeight(image); + } this.heightLoaded = true; this.nodeReady(); diff --git a/source/providers/MapBoxProvider.ts b/source/providers/MapBoxProvider.ts index a975465..415345d 100644 --- a/source/providers/MapBoxProvider.ts +++ b/source/providers/MapBoxProvider.ts @@ -139,15 +139,37 @@ export class MapBoxProvider extends MapProvider reject(); }; image.crossOrigin = 'Anonymous'; - - if (this.mode === MapBoxProvider.STYLE) - { - image.src = MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; - } - else - { - image.src = MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; - } + image.src = this.getTileUrl(zoom, x, y); }); } + + public async fetchTileBuffer(zoom: number, x: number, y: number): Promise + { + const response = await fetch(this.getTileUrl(zoom, x, y)); + if (!response.ok) + { + throw new Error('MapBoxProvider: Failed to fetch tile buffer. Status ' + response.status); + } + return response.arrayBuffer(); + } + + /** + * Build the URL for a tile given its zoom, x, and y coordinates. + * + * @param zoom - Zoom level. + * @param x - Tile x. + * @param y - Tile y. + * @returns URL string for the tile. + */ + private getTileUrl(zoom: number, x: number, y: number): string + { + if (this.mode === MapBoxProvider.STYLE) + { + return MapBoxProvider.ADDRESS + 'styles/v1/' + this.style + '/tiles/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x?access_token=' : '?access_token=') + this.apiToken; + } + else + { + return MapBoxProvider.ADDRESS + 'v4/' + this.mapId + '/' + zoom + '/' + x + '/' + y + (this.useHDPI ? '@2x.' : '.') + this.format + '?access_token=' + this.apiToken; + } + } } diff --git a/source/providers/MapProvider.ts b/source/providers/MapProvider.ts index 6531bd2..a86d4ca 100644 --- a/source/providers/MapProvider.ts +++ b/source/providers/MapProvider.ts @@ -49,6 +49,24 @@ export abstract class MapProvider return null; } + /** + * Get a tile as a raw ArrayBuffer for the x, y, zoom based on the provider configuration. + * + * Returns null by default. Providers that support direct binary fetching (e.g. MapBoxProvider) + * override this method. When available this should be preferred over fetchTile() for height + * data because it allows PNG decoding without canvas, avoiding fingerprinting noise introduced + * by Firefox Enhanced Tracking Protection. + * + * @param zoom - Zoom level. + * @param x - Tile x. + * @param y - Tile y. + * @returns Promise with the raw tile data as ArrayBuffer, or null if not supported. + */ + public fetchTileBuffer(zoom: number, x: number, y: number): Promise + { + return Promise.resolve(null); + } + /** * Get map meta data from server if supported. *