terriajs
Version:
Geospatial data visualization platform.
232 lines • 9.38 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 bbox from "@turf/bbox";
import booleanIntersects from "@turf/boolean-intersects";
import circle from "@turf/circle";
import { featureCollection } from "@turf/helpers";
import geojsonvt from "geojson-vt";
import { cloneDeep } from "lodash-es";
import { makeObservable, observable, runInAction } from "mobx";
import { GeomType } from "protomaps-leaflet";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo";
import { toFeatureCollection } from "../../../Core/GeoJson";
import { LAYER_NAME_PROP, PROTOMAPS_DEFAULT_TILE_SIZE, PROTOMAPS_TILE_BUFFER } from "../../ImageryProvider/ProtomapsImageryProvider";
/** Extent (of coordinates) of tiles generated by geojson-vt */
export const GEOJSON_VT_EXTENT = 4096;
/** Layer name to use with geojson-vt
* This must be used in PaintRules/LabelRules (eg `dataLayer: "layer"`)
*/
export const GEOJSON_SOURCE_LAYER_NAME = "layer";
/** Protomaps Geojson source
* This source uses geojson-vt to tile geojson data
* It is designed to be used with ProtomapsImageryProvider
*/
export class ProtomapsGeojsonSource {
/** Data object from Options */
data;
/** Resolved geojsonObject (if applicable) */
geojsonObject;
/** Geojson-vt tileIndex (if applicable) */
tileIndex;
constructor(url) {
makeObservable(this);
this.data = url;
if (typeof url !== "string") {
this.geojsonObject = url;
}
}
/** Fetch geoJSON data (if required) and tile with geojson-vt */
async fetchData() {
let result;
if (typeof this.data === "string") {
result =
toFeatureCollection(await (await fetch(this.data)).json()) ??
featureCollection([]);
}
else {
result = this.data;
}
runInAction(() => (this.geojsonObject = result));
return geojsonvt(result, {
buffer: (PROTOMAPS_TILE_BUFFER / PROTOMAPS_DEFAULT_TILE_SIZE) *
GEOJSON_VT_EXTENT,
extent: GEOJSON_VT_EXTENT,
maxZoom: 24
});
}
async get(c, tileSize) {
if (!this.tileIndex) {
this.tileIndex = this.fetchData();
}
// request a particular tile
const tile = (await this.tileIndex).getTile(c.z, c.x, c.y);
const result = new Map();
if (tile && tile.features && tile.features.length > 0) {
result.set(GEOJSON_SOURCE_LAYER_NAME, geojsonVtTileToProtomapsFeatures(tile.features, tileSize));
}
return result;
}
async pickFeatures(_x, _y, level, longitude, latitude) {
if (!this.geojsonObject)
return [];
const featureInfos = [];
// Get rough meters per pixel (at equator) for given zoom level
const zoomMeters = 156543 / Math.pow(2, level);
// Create circle with 10 pixel radius to pick features
const buffer = circle([CesiumMath.toDegrees(longitude), CesiumMath.toDegrees(latitude)], 10 * zoomMeters, {
steps: 10,
units: "meters"
});
// Create wrappedBuffer with only positive coordinates - this is needed for features which overlap antemeridian
const wrappedBuffer = cloneDeep(buffer);
wrappedBuffer.geometry.coordinates.forEach((ring) => ring.forEach((point) => {
point[0] = point[0] < 0 ? point[0] + 360 : point[0];
}));
const bufferBbox = bbox(buffer);
// Get array of all features
const geojsonFeatures = this.geojsonObject.features;
const pickedFeatures = [];
for (let index = 0; index < geojsonFeatures.length; index++) {
const feature = geojsonFeatures[index];
if (!feature.bbox) {
feature.bbox = bbox(feature);
}
// Filter by bounding box and then intersection with buffer (to minimize calls to booleanIntersects)
if (Math.max(feature.bbox[0],
// Wrap buffer bbox if necessary
feature.bbox[0] > 180 ? bufferBbox[0] + 360 : bufferBbox[0]) <=
Math.min(feature.bbox[2],
// Wrap buffer bbox if necessary
feature.bbox[2] > 180 ? bufferBbox[2] + 360 : bufferBbox[2]) &&
Math.max(feature.bbox[1], bufferBbox[1]) <=
Math.min(feature.bbox[3], bufferBbox[3])) {
// If we have longitudes greater than 180 - used wrappedBuffer
if (feature.bbox[0] > 180 || feature.bbox[2] > 180) {
if (booleanIntersects(feature, wrappedBuffer))
pickedFeatures.push(feature);
}
else if (booleanIntersects(feature, buffer))
pickedFeatures.push(feature);
}
}
// Convert pickedFeatures to ImageryLayerFeatureInfos
pickedFeatures.forEach((f) => {
const featureInfo = new ImageryLayerFeatureInfo();
featureInfo.data = f;
featureInfo.properties = Object.assign({ [LAYER_NAME_PROP]: GEOJSON_SOURCE_LAYER_NAME }, f.properties ?? {});
if (f.geometry.type === "Point" &&
typeof f.geometry.coordinates[0] === "number" &&
typeof f.geometry.coordinates[1] === "number") {
featureInfo.position = Cartographic.fromDegrees(f.geometry.coordinates[0], f.geometry.coordinates[1]);
}
featureInfo.configureDescriptionFromProperties(f.properties);
featureInfo.configureNameFromProperties(f.properties);
featureInfos.push(featureInfo);
});
return featureInfos;
}
}
__decorate([
observable.ref
], ProtomapsGeojsonSource.prototype, "geojsonObject", void 0);
export const geomTypeMap = (type) => {
switch (type) {
case "Point":
case "MultiPoint":
return GeomType.Point;
case "LineString":
case "MultiLineString":
return GeomType.Line;
case "Polygon":
case "MultiPolygon":
return GeomType.Polygon;
default:
return null;
}
};
export function geojsonVtTileToProtomapsFeatures(features, tileSize) {
const scale = tileSize / GEOJSON_VT_EXTENT;
return features
.map((f) => {
let transformedGeom;
let numVertices;
// Calculate bbox
const bbox = {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
};
// Multi-polygon
if (Array.isArray(f.geometry[0][0])) {
// Note: the type is incorrect here
const geom = f.geometry;
transformedGeom = geom.map((g1) => g1.map((g2) => {
const x = g2[0] * scale;
const y = g2[1] * scale;
if (bbox.minX > x) {
bbox.minX = x;
}
if (bbox.maxX < x) {
bbox.maxX = x;
}
if (bbox.minY > y) {
bbox.minY = y;
}
if (bbox.maxY < y) {
bbox.maxY = y;
}
return new Point(x, y);
}));
numVertices = transformedGeom.reduce((count, current) => count + current.length, 0);
}
// Other feature types
else {
const geom = f.geometry;
transformedGeom = [
geom.map((g1) => {
const x = g1[0] * scale;
const y = g1[1] * scale;
if (bbox.minX > x) {
bbox.minX = x;
}
if (bbox.maxX < x) {
bbox.maxX = x;
}
if (bbox.minY > y) {
bbox.minY = y;
}
if (bbox.maxY < y) {
bbox.maxY = y;
}
return new Point(x, y);
})
];
numVertices = transformedGeom.length;
}
if (f.type === 0)
return null;
const geomType = {
[1]: GeomType.Point,
[2]: GeomType.Line,
[3]: GeomType.Polygon
}[f.type];
const feature = {
props: { ...(f.tags ?? {}) },
bbox,
geomType,
geom: transformedGeom,
numVertices
};
return feature;
})
.filter((f) => f !== null);
}
//# sourceMappingURL=ProtomapsGeojsonSource.js.map