terriajs
Version:
Geospatial data visualization platform.
327 lines • 16.2 kB
JavaScript
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