UNPKG

terriajs

Version:

Geospatial data visualization platform.

327 lines 16.2 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import Point from "@mapbox/point-geometry"; import { isEmpty } from "lodash-es"; import { action, makeObservable } from "mobx"; import { Labelers, LineSymbolizer, PmtilesSource, TileCache, View, ZxySource, paint } from "protomaps-leaflet"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; import Credit from "terriajs-cesium/Source/Core/Credit"; import CesiumEvent from "terriajs-cesium/Source/Core/Event"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme"; import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; import TerriaError from "../../Core/TerriaError"; import isDefined from "../../Core/isDefined"; import { FEATURE_ID_PROP as GEOJSON_FEATURE_ID_PROP } from "../../ModelMixins/GeojsonMixin"; import { ProtomapsArcGisPbfSource } from "../Vector/Protomaps/ProtomapsArcGisPbfSource"; import { GEOJSON_SOURCE_LAYER_NAME, ProtomapsGeojsonSource } from "../Vector/Protomaps/ProtomapsGeojsonSource"; export const LAYER_NAME_PROP = "__LAYERNAME"; /** Tile size in pixels (for canvas and geojson-vt) */ export const PROTOMAPS_DEFAULT_TILE_SIZE = 256; /** Buffer (in pixels) used when rendering (and generating - through geojson-vt) vector tiles */ export const PROTOMAPS_TILE_BUFFER = 32; /** Tile cache tile size for protomaps-leaflet */ const TILE_CACHE_TILE_SIZE = 1024; export default class ProtomapsImageryProvider { terria; // Imagery provider properties tilingScheme; tileWidth; tileHeight; minimumLevel; /** This is used to fail requests for levels below softMinimumLevel, as setting minimumLevel to higher than 0 (with no rectangle provided), will result in many tiles being requested. */ softMinimumLevel; maximumLevel; rectangle; errorEvent = new CesiumEvent(); ready = true; credit; /** This is only used for Terria feature picking - as we track ImageryProvider feature picking by url (See PickedFeatures/Cesium._attachProviderCoordHooks). This URL is never called. * This is set using the `id` property in the constructor options */ url; // Set values to please poor cesium types defaultNightAlpha = undefined; defaultDayAlpha = undefined; hasAlphaChannel = true; defaultAlpha = undefined; defaultBrightness = undefined; defaultContrast = undefined; defaultGamma = undefined; defaultHue = undefined; defaultSaturation = undefined; defaultMagnificationFilter = undefined; defaultMinificationFilter = undefined; proxy = undefined; readyPromise = Promise.resolve(true); tileDiscardPolicy = undefined; // Protomaps properties /** Data object from constructor options (this is transformed into `source`) */ data; labelers; view; processPickedFeatures; maximumNativeZoom; idProperty; source; paintRules; labelRules; constructor(options) { makeObservable(this); this.data = options.data; this.terria = options.terria; this.tilingScheme = new WebMercatorTilingScheme(); this.tileWidth = PROTOMAPS_DEFAULT_TILE_SIZE; this.tileHeight = PROTOMAPS_DEFAULT_TILE_SIZE; // Note we leave minimumLevel at 0, and then we fail requests for levels below softMinimumLevel (see softMinimumLevel) this.minimumLevel = 0; this.softMinimumLevel = options.minimumZoom; this.maximumLevel = options.maximumZoom ?? 24; this.maximumNativeZoom = options.maximumNativeZoom ?? this.maximumLevel; this.rectangle = isDefined(options.rectangle) ? Rectangle.intersection(options.rectangle, this.tilingScheme.rectangle) || this.tilingScheme.rectangle : this.tilingScheme.rectangle; this.errorEvent = new CesiumEvent(); this.url = options.id; this.ready = true; this.credit = typeof options.credit === "string" ? new Credit(options.credit) : options.credit; // Protomaps this.paintRules = options.paintRules; this.labelRules = options.labelRules; this.idProperty = options.idProperty ?? "FID"; // Generate protomaps source based on this.data // - URL of pmtiles, geojson or pbf files if (typeof this.data === "string") { // Note: the `base` passed to URL is not relevant as we only use the // parsed path. It still needs to be passed so that URL won't throw an // error when passing relative URLs like /proxy/something. const path = new URL(this.data, "http://example").pathname; if (path.endsWith(".pmtiles")) { this.source = new PmtilesSource(this.data, false); const cache = new TileCache(this.source, TILE_CACHE_TILE_SIZE); this.view = new View(cache, this.maximumNativeZoom, 2); } else if (path.endsWith(".json") || path.endsWith(".geojson")) { this.source = new ProtomapsGeojsonSource(this.data); } else { this.source = new ZxySource(this.data, false); const cache = new TileCache(this.source, TILE_CACHE_TILE_SIZE); this.view = new View(cache, this.maximumNativeZoom, 2); } } // Source object else if (this.data instanceof ProtomapsGeojsonSource || this.data instanceof PmtilesSource || this.data instanceof ZxySource || this.data instanceof ProtomapsArcGisPbfSource) { this.source = this.data; } // - GeoJsonObject object else { this.source = new ProtomapsGeojsonSource(this.data); } const labelersCanvasContext = document .createElement("canvas") .getContext("2d"); if (!labelersCanvasContext) throw TerriaError.from("Failed to create labelersCanvasContext"); this.labelers = new Labelers(labelersCanvasContext, this.labelRules, 16, () => undefined); this.processPickedFeatures = options.processPickedFeatures; } getTileCredits(_x, _y, _level) { return []; } async requestImage(x, y, level, request) { const canvas = document.createElement("canvas"); canvas.width = this.tileWidth; canvas.height = this.tileHeight; return await this.requestImageForCanvas(x, y, level, canvas, request); } async requestImageForCanvas(x, y, level, canvas, request) { if (isDefined(this.softMinimumLevel) && level < this.softMinimumLevel) throw TerriaError.from(`Level: ${level} is below softMinimumLevel (=${this.softMinimumLevel})`); const coords = { z: level, x, y }; // Adapted from https://github.com/protomaps/protomaps.js/blob/master/src/frontends/leaflet.ts let tile; // Get PreparedTile from source or view // Here we need a little bit of extra logic for the ProtomapsGeojsonSource if (this.source instanceof ProtomapsGeojsonSource || this.source instanceof ProtomapsArcGisPbfSource) { const data = await this.source.get(coords, this.tileHeight, request); tile = { data: data, z: coords.z, dataTile: coords, scale: 1, origin: new Point(coords.x * PROTOMAPS_DEFAULT_TILE_SIZE, coords.y * PROTOMAPS_DEFAULT_TILE_SIZE), dim: this.tileWidth }; } else if (this.view) { tile = await this.view.getDisplayTile(coords); } else { throw TerriaError.from(`Failed to get tile - no view or appropriate source in ProtomapsImageryProvider`); } const tileMap = new Map().set("", [tile]); this.labelers.add(coords.z, tileMap); const labelData = this.labelers.getIndex(tile.z); const bbox = { minX: PROTOMAPS_DEFAULT_TILE_SIZE * coords.x - PROTOMAPS_TILE_BUFFER, minY: PROTOMAPS_DEFAULT_TILE_SIZE * coords.y - PROTOMAPS_TILE_BUFFER, maxX: PROTOMAPS_DEFAULT_TILE_SIZE * (coords.x + 1) + PROTOMAPS_TILE_BUFFER, maxY: PROTOMAPS_DEFAULT_TILE_SIZE * (coords.y + 1) + PROTOMAPS_TILE_BUFFER }; const origin = new Point(PROTOMAPS_DEFAULT_TILE_SIZE * coords.x, PROTOMAPS_DEFAULT_TILE_SIZE * coords.y); const ctx = canvas.getContext("2d"); if (!ctx) throw TerriaError.from("Failed to get canvas context"); ctx.setTransform(this.tileWidth / PROTOMAPS_DEFAULT_TILE_SIZE, 0, 0, this.tileHeight / PROTOMAPS_DEFAULT_TILE_SIZE, 0, 0); ctx.clearRect(0, 0, PROTOMAPS_DEFAULT_TILE_SIZE, PROTOMAPS_DEFAULT_TILE_SIZE); if (labelData) paint(ctx, coords.z, tileMap, labelData, this.paintRules, bbox, origin, false, ""); return canvas; } async pickFeatures(x, y, level, longitude, latitude) { const featureInfos = []; // If view is set - this means we are using actual vector tiles (that is not GeoJson object) // So we use this.view.queryFeatures if (this.view) { try { // Make sure tile is loaded, this will use cache if tile is already loaded await this.view.getDisplayTile({ x, y, z: level }); } catch (e) { TerriaError.from(e, "Error while picking features").log(); return []; } // Get list of vector tile layers which are rendered const renderedLayers = [...this.paintRules, ...this.labelRules].map((r) => r.dataLayer); this.view .queryFeatures(CesiumMath.toDegrees(longitude), CesiumMath.toDegrees(latitude), level, 1) .forEach((f) => { // Only create FeatureInfo for visible features with properties if (!f.feature.props || isEmpty(f.feature.props) || !renderedLayers.includes(f.layerName)) return; const featureInfo = new ImageryLayerFeatureInfo(); // Add Layer name property featureInfo.properties = Object.assign({ [LAYER_NAME_PROP]: f.layerName }, f.feature.props ?? {}); featureInfo.position = new Cartographic(longitude, latitude); featureInfo.configureDescriptionFromProperties(f.feature.props); featureInfo.configureNameFromProperties(f.feature.props); featureInfos.push(featureInfo); }); // No view is set and we have geoJSON object // So we pick features manually } else if (this.source instanceof ProtomapsGeojsonSource || this.source instanceof ProtomapsArcGisPbfSource) { featureInfos.push(...(await this.source.pickFeatures(x, y, level, longitude, latitude))); } if (this.processPickedFeatures) { return await this.processPickedFeatures(featureInfos); } return featureInfos; } clone(options) { let data = options?.data; // To clone data/source, we want to minimize any unnecessary processing if (!data) { // These can be passed straight in without processing if (typeof this.data === "string" || this.data instanceof PmtilesSource || this.data instanceof ProtomapsArcGisPbfSource) { data = this.data; // We can't just clone ZxySource objects, so just pass in URL } else if (this.data instanceof ZxySource) { data = this.data.url; // If ProtomapsGeojsonSource was passed into data, create new one and copy over tileIndex } else if (this.data instanceof ProtomapsGeojsonSource) { if (this.data.geojsonObject) { data = new ProtomapsGeojsonSource(this.data.geojsonObject); // Copy over tileIndex so it doesn't have to be re-processed data.tileIndex = this.data.tileIndex; } // If GeoJson FeatureCollection was passed into data (this.data), and the source is ProtomapsGeojsonSource // create a ProtomapsGeojsonSource with the GeoJson and copy over tileIndex } else if (this.source instanceof ProtomapsGeojsonSource) { data = new ProtomapsGeojsonSource(this.data); // Copy over tileIndex so it doesn't have to be re-processed data.tileIndex = this.source.tileIndex; } } if (!data) return; return new ProtomapsImageryProvider({ terria: options?.terria ?? this.terria, id: options?.id ?? this.url, data, // Note we use softMinimumLevel here, the imagery provider minimum level is always 0 minimumZoom: options?.minimumZoom ?? this.softMinimumLevel, maximumZoom: options?.maximumZoom ?? this.maximumLevel, maximumNativeZoom: options?.maximumNativeZoom ?? this.maximumNativeZoom, rectangle: options?.rectangle ?? this.rectangle, credit: options?.credit ?? this.credit, paintRules: options?.paintRules ?? this.paintRules, labelRules: options?.labelRules ?? this.labelRules, processPickedFeatures: options?.processPickedFeatures ?? this.processPickedFeatures }); } /** Clones ImageryProvider, and sets paintRules to highlight picked features */ createHighlightImageryProvider(feature) { // Depending on this.source, feature IDs might be FID (for actual vector tile sources) or they will use GEOJSON_FEATURE_ID_PROP let featureProp; // Similarly, feature layer name will be LAYER_NAME_PROP for mvt, whereas GeoJSON features will use the constant GEOJSON_SOURCE_LAYER_NAME let layerName; if (this.source instanceof ProtomapsGeojsonSource) { featureProp = GEOJSON_FEATURE_ID_PROP; layerName = GEOJSON_SOURCE_LAYER_NAME; } else if (this.source instanceof ProtomapsArcGisPbfSource) { featureProp = this.source.objectIdField; layerName = GEOJSON_SOURCE_LAYER_NAME; } else { featureProp = this.idProperty; layerName = feature.properties?.[LAYER_NAME_PROP]?.getValue(); } const featureId = feature.properties?.[featureProp]?.getValue(); if (isDefined(featureId) && isDefined(layerName)) { return this.clone({ labelRules: [], paintRules: [ { dataLayer: layerName, symbolizer: new LineSymbolizer({ color: this.terria.baseMapContrastColor, width: 4 }), minzoom: 0, maxzoom: Infinity, filter: (_zoom, feature) => feature.props?.[featureProp] === featureId } ] }); } return; } } __decorate([ action ], ProtomapsImageryProvider.prototype, "createHighlightImageryProvider", null); //# sourceMappingURL=ProtomapsImageryProvider.js.map