terriajs
Version:
Geospatial data visualization platform.
250 lines • 10.2 kB
JavaScript
import Point from "@mapbox/point-geometry";
import arcGisPbfDecode from "arcgis-pbf-parser";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import Resource from "terriajs-cesium/Source/Core/Resource";
import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo";
import { isGeometryCollection, isLine, isMultiLineString, isMultiPolygon, isPoint, isPolygon } from "../../../Core/GeoJson";
import { PROTOMAPS_TILE_BUFFER } from "../../ImageryProvider/ProtomapsImageryProvider";
import { GEOJSON_SOURCE_LAYER_NAME, geomTypeMap } from "./ProtomapsGeojsonSource";
export class ProtomapsArcGisPbfSource {
baseResource;
tilingScheme;
objectIdField;
outFields;
maxRecordCountFactor;
featuresPerTileRequest;
maxTiledFeatures;
enablePickFeatures;
supportsQuantization;
constructor(options) {
this.baseResource = new Resource(options.url);
this.baseResource.appendForwardSlash();
if (options.token) {
this.baseResource.setQueryParameters({
token: options.token
});
}
this.tilingScheme = options.tilingScheme;
this.objectIdField = options.objectIdField;
this.outFields = options.outFields;
this.maxRecordCountFactor = options.maxRecordCountFactor;
this.featuresPerTileRequest = options.featuresPerTileRequest;
this.maxTiledFeatures = options.maxTiledFeatures;
this.enablePickFeatures = options.enablePickFeatures;
this.supportsQuantization = options.supportsQuantization;
}
async get(c, tileSizePixels,
// TODO add support for canceling requests (through cesium)
_request) {
const rect = this.tilingScheme.tileXYToNativeRectangle(c.x, c.y, c.z);
const tileExtent = {
xmin: rect.west,
ymin: rect.south,
xmax: rect.east,
ymax: rect.north
};
const tileWidthNative = tileExtent.xmax - tileExtent.xmin;
const nativePixelSize = tileWidthNative / tileSizePixels;
const tileExtentWithBuffer = {
xmin: rect.west - PROTOMAPS_TILE_BUFFER * nativePixelSize,
ymin: rect.south - PROTOMAPS_TILE_BUFFER * nativePixelSize,
xmax: rect.east + PROTOMAPS_TILE_BUFFER * nativePixelSize,
ymax: rect.north + PROTOMAPS_TILE_BUFFER * nativePixelSize
};
const arcGisFeatures = [];
let offset = 0;
let fetching = true;
while (fetching) {
const tileResource = this.baseResource.getDerivedResource({
// Not sure how to handle request here - as we are making multiple requests
// request: request
});
tileResource.setQueryParameters({
f: "pbf",
resultType: "tile",
inSR: "102100",
geometry: JSON.stringify(tileExtentWithBuffer),
geometryType: "esriGeometryEnvelope",
outFields: Array.from(new Set([this.objectIdField, ...this.outFields])).join(","),
where: "1=1",
maxRecordCountFactor: this.maxRecordCountFactor,
resultRecordCount: this.featuresPerTileRequest,
outSR: "102100", // Fetch in Web Mercator
spatialRel: "esriSpatialRelIntersects",
maxAllowableOffset: nativePixelSize,
outSpatialReference: "102100",
precision: "8",
resultOffset: offset
});
if (this.supportsQuantization) {
tileResource.setQueryParameters({
quantizationParameters: JSON.stringify({
extent: tileExtentWithBuffer,
spatialReference: { wkid: 102100, latestWkid: 3857 },
mode: "view",
originPosition: "upperLeft",
tolerance: nativePixelSize
})
});
}
const arrayBuffer = await tileResource.fetchArrayBuffer();
if (!arrayBuffer) {
console.error("No data for URL: " + tileResource.url);
fetching = false;
continue;
}
const arcGisResponse = arcGisPbfDecode(new Uint8Array(arrayBuffer));
arcGisFeatures.push(...arcGisResponse.featureCollection.features);
if (arcGisResponse.featureCollection.features.length === 0) {
fetching = false;
continue;
}
// If we have reached the max number of features per tile request, we need to fetch more
if (arcGisResponse.featureCollection.features.length >=
this.featuresPerTileRequest) {
offset = offset + this.featuresPerTileRequest;
}
else {
fetching = false;
}
if (offset >= this.maxTiledFeatures) {
console.warn(`ArcGisPbfSource: maxTiledFeatures exceeded`);
fetching = false;
}
}
const protomapsFeatures = [];
for (const f of arcGisFeatures) {
processFeature(f, tileExtentWithBuffer, tileSizePixels).forEach((pf) => protomapsFeatures.push(pf));
}
const result = new Map();
result.set(GEOJSON_SOURCE_LAYER_NAME, protomapsFeatures);
return result;
}
async pickFeatures(_x, _y, level, longitude, latitude) {
if (!this.enablePickFeatures) {
return [];
}
const features = [];
const pickFeatureResource = this.baseResource.getDerivedResource({
url: "query"
});
// Get rough meters per pixel (at equator) for given zoom level
const zoomMeters = 156543 / Math.pow(2, level);
pickFeatureResource.setQueryParameters({
f: "geojson",
sr: "4326", // Fetch in WGS84
geometryType: "esriGeometryPoint",
geometry: JSON.stringify({
x: CesiumMath.toDegrees(longitude),
y: CesiumMath.toDegrees(latitude),
spatialReference: { wkid: 4326 }
}),
outFields: "*",
// We don't need geometry in response, as we will create a "highlight imagery provider" for the feature, that uses the same ArcGisPbfSource
returnGeometry: false,
outSR: "4326",
spatialRel: "esriSpatialRelIntersects",
units: "esriSRUnit_Meter",
distance: zoomMeters * 4 // 4 pixels wide (in meters)
});
const json = (await pickFeatureResource.fetchJson());
if (json.features) {
for (const f of json.features) {
const featureInfo = new ImageryLayerFeatureInfo();
featureInfo.data = f;
featureInfo.properties = f.properties;
featureInfo.configureDescriptionFromProperties(f.properties);
featureInfo.configureNameFromProperties(f.properties);
features.push(featureInfo);
}
}
return features;
}
}
/* Process GeoJSON (in Web Mercator) features into Protomaps features **/
function processFeature(feature, tileExtentWithBuffer, tileWidthPixels) {
const tileWidthWithBuffer = tileExtentWithBuffer.xmax - tileExtentWithBuffer.xmin;
const tileWidthWithBufferPixels = tileWidthPixels + 2 * PROTOMAPS_TILE_BUFFER;
const geomType = geomTypeMap(feature.geometry?.type);
if (geomType === null) {
return [];
}
const transformedGeom = [];
let numVertices = 0;
// Calculate bbox
const bbox = {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
};
let points;
if (isPoint(feature)) {
points = [[feature.geometry.coordinates]];
}
else if (isMultiLineString(feature)) {
points = feature.geometry.coordinates;
}
else if (isPolygon(feature)) {
points = feature.geometry.coordinates;
}
else if (isMultiPolygon(feature) &&
feature.geometry.coordinates.length > 0) {
return feature.geometry.coordinates.flatMap((polygon) => processFeature({
type: "Feature",
properties: feature.properties,
geometry: { type: "Polygon", coordinates: polygon }
}, tileExtentWithBuffer, tileWidthPixels));
}
else if (isLine(feature)) {
points = [feature.geometry.coordinates];
}
else if (isGeometryCollection(feature)) {
return feature.geometry.geometries.flatMap((geometry) => processFeature({
type: "Feature",
properties: feature.properties,
geometry
}, tileExtentWithBuffer, tileWidthPixels));
}
else {
return [];
}
for (const g1 of points) {
const transformedG1 = [];
for (const g2 of g1) {
const transformedG2 = [
((g2[0] - tileExtentWithBuffer.xmin) / tileWidthWithBuffer) *
tileWidthWithBufferPixels -
PROTOMAPS_TILE_BUFFER,
(1 - (g2[1] - tileExtentWithBuffer.ymin) / tileWidthWithBuffer) *
tileWidthWithBufferPixels -
PROTOMAPS_TILE_BUFFER
];
if (bbox.minX > transformedG2[0]) {
bbox.minX = transformedG2[0];
}
if (bbox.maxX < transformedG2[0]) {
bbox.maxX = transformedG2[0];
}
if (bbox.minY > transformedG2[1]) {
bbox.minY = transformedG2[1];
}
if (bbox.maxY < transformedG2[1]) {
bbox.maxY = transformedG2[1];
}
transformedG1.push(new Point(transformedG2[0], transformedG2[1]));
numVertices++;
}
transformedGeom.push(transformedG1);
}
return [
{
props: feature.properties ?? {},
bbox,
geomType,
geom: transformedGeom,
numVertices
}
];
}
//# sourceMappingURL=ProtomapsArcGisPbfSource.js.map