itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
212 lines (208 loc) • 8.18 kB
JavaScript
import { Coordinates } from '@itowns/geographic';
import { FeatureCollection, FEATURE_TYPES } from "../Core/Feature.js";
import { deprecatedParsingOptionsToNewOne } from "../Core/Deprecated/Undeprecator.js";
function readCRS(json) {
if (json.crs) {
if (json.crs.type.toLowerCase() == 'epsg') {
return `EPSG:${json.crs.properties.code}`;
} else if (json.crs.type.toLowerCase() == 'name') {
if (json.crs.properties.name.toLowerCase().includes('epsg:')) {
// OGC CRS URN: urn:ogc:def:crs:authority:version:code => EPSG:[...]:code
// legacy identifier: authority:code => EPSG:code
const codeStart = json.crs.properties.name.lastIndexOf(':');
if (codeStart > 0) {
return `EPSG:${json.crs.properties.name.substr(codeStart + 1)}`;
}
}
throw new Error(`Unsupported CRS authority '${json.crs.properties.name}'`);
}
throw new Error(`Unsupported CRS type '${json.crs}'`);
}
// assume default crs
return 'EPSG:4326';
}
const coord = new Coordinates('EPSG:4978', 0, 0, 0);
const last = new Coordinates('EPSG:4978', 0, 0, 0);
const first = new Coordinates('EPSG:4978', 0, 0, 0);
// filter with the first point
const firstPtIsOut = (extent, aCoords, crs) => {
coord.crs = crs;
coord.setFromArray(aCoords[0]);
return !extent.isPointInside(coord);
};
const toFeature = {
populateGeometry(crsIn, coordinates, geometry, feature) {
geometry.startSubGeometry(coordinates.length, feature);
coord.crs = crsIn;
// coordinates is a list of pair [[x1, y1], [x2, y2], ..., [xn, yn]]
// or list of triplet [[x1, y1, z1], [x2, y2, z2], ..., [xn, yn, zn]]
for (const triplet of coordinates) {
coord.setFromValues(triplet[0], triplet[1], triplet[2]);
geometry.pushCoordinates(feature, coord);
}
geometry.updateExtent();
},
// compute clockwise polygon
populateGeometryWithCCW(crsIn, coordinates, geometry, feature) {
geometry.startSubGeometry(coordinates.length, feature);
coord.crs = crsIn;
let sum = 0;
first.setFromValues(coordinates[0][0], coordinates[0][1], coordinates[0][2]);
last.copy(first);
for (let i = 0; i < coordinates.length; i++) {
coord.setFromValues(coordinates[i][0], coordinates[i][1], coordinates[i][2]);
sum += (last.x - coord.x) * (last.y + coord.y);
last.copy(coord);
geometry.pushCoordinates(feature, coord);
}
sum += (last.x - first.x) * (last.y + first.y);
geometry.getLastSubGeometry().ccw = sum < 0;
geometry.updateExtent();
},
point(feature, crsIn, coordsIn, collection, properties) {
this.default(feature, crsIn, [coordsIn], collection, properties);
},
default(feature, crsIn, coordsIn, collection, properties) {
if (collection.filterExtent && firstPtIsOut(collection.filterExtent, coordsIn, crsIn)) {
return;
}
const geometry = feature.bindNewGeometry();
geometry.properties = properties;
this.populateGeometry(crsIn, coordsIn, geometry, feature);
feature.updateExtent(geometry);
},
polygon(feature, crsIn, coordsIn, collection, properties) {
// filtering
if (collection.filterExtent && firstPtIsOut(collection.filterExtent, coordsIn[0], crsIn)) {
return;
}
const geometry = feature.bindNewGeometry();
geometry.properties = properties;
// Then read contour and holes
for (let i = 0; i < coordsIn.length; i++) {
this.populateGeometryWithCCW(crsIn, coordsIn[i], geometry, feature);
}
feature.updateExtent(geometry);
},
multi(type, feature, crsIn, coordsIn, collection, properties) {
for (const coords of coordsIn) {
this[type](feature, crsIn, coords, collection, properties);
}
}
};
function coordinatesToFeature(type, feature, crsIn, coordinates, collection, properties) {
if (coordinates.length == 0) {
return;
}
switch (type) {
case 'point':
case 'linestring':
return toFeature.default(feature, crsIn, coordinates, collection, properties);
case 'multipoint':
return toFeature.multi('point', feature, crsIn, coordinates, collection, properties);
case 'multilinestring':
return toFeature.multi('default', feature, crsIn, coordinates, collection, properties);
case 'polygon':
return toFeature.polygon(feature, crsIn, coordinates, collection, properties);
case 'multipolygon':
return toFeature.multi('polygon', feature, crsIn, coordinates, collection, properties);
case 'geometrycollection':
default:
throw new Error(`Unhandled geojson type ${feature.type}`);
}
}
function toFeatureType(jsonType) {
switch (jsonType) {
case 'point':
case 'multipoint':
return FEATURE_TYPES.POINT;
case 'linestring':
case 'multilinestring':
return FEATURE_TYPES.LINE;
case 'polygon':
case 'multipolygon':
return FEATURE_TYPES.POLYGON;
case 'geometrycollection':
default:
throw new Error(`Unhandled geometry type ${jsonType}`);
}
}
const keyProperties = ['type', 'geometry', 'properties'];
const firstCoordinates = a => a === undefined || Array.isArray(a) && !isNaN(a[0]) ? a : firstCoordinates(a[0]);
function jsonFeatureToFeature(crsIn, json, collection) {
if (!json.geometry?.type) {
console.warn('No geometry provided');
return null;
}
const jsonType = json.geometry.type.toLowerCase();
const featureType = toFeatureType(jsonType);
const feature = collection.requestFeatureByType(featureType);
const coordinates = jsonType != 'point' ? json.geometry.coordinates : [json.geometry.coordinates];
const properties = json.properties || {};
feature.hasRawElevationData = firstCoordinates(coordinates)?.length === 3;
// copy other properties
for (const key of Object.keys(json)) {
if (!keyProperties.includes(key.toLowerCase())) {
// create `geojson` key if it does not exist yet
properties.geojson = properties.geojson || {};
// add key defined property to `geojson` property
properties.geojson[key] = json[key];
}
}
coordinatesToFeature(jsonType, feature, crsIn, coordinates, collection, properties);
return feature;
}
function jsonFeaturesToFeatures(crsIn, jsonFeatures, options) {
const collection = new FeatureCollection(options);
const filter = options.filter || (() => true);
for (const jsonFeature of jsonFeatures) {
if (filter(jsonFeature.properties, jsonFeature.geometry)) {
jsonFeatureToFeature(crsIn, jsonFeature, collection);
}
}
collection.removeEmptyFeature();
collection.updateExtent();
return collection;
}
/**
* The GeoJsonParser module provide a [parse]{@link module:GeoJsonParser.parse}
* method that takes a GeoJSON in and gives an object formatted for iTowns
* containing all necessary informations to display this GeoJSON.
*
* @module GeoJsonParser
*/
export default {
/**
* Parse a GeoJSON file content and return a {@link FeatureCollection}.
*
* @param {string} json - The GeoJSON file content to parse.
* @param {ParsingOptions} options - Options controlling the parsing.
* @return {Promise} A promise resolving with a {@link FeatureCollection}.
*/
parse(json) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
options = deprecatedParsingOptionsToNewOne(options);
options.in = options.in || {};
const out = options.out;
const _in = options.in;
if (typeof json === 'string') {
json = JSON.parse(json);
}
_in.crs = _in.crs || readCRS(json);
if (out.filteringExtent) {
if (typeof out.filteringExtent == 'boolean') {
out.filterExtent = options.extent.isExtent ? options.extent.as(_in.crs) : options.extent.toExtent(_in.crs);
} else if (out.filteringExtent.isExtent) {
out.filterExtent = out.filteringExtent;
}
}
switch (json.type.toLowerCase()) {
case 'featurecollection':
return Promise.resolve(jsonFeaturesToFeatures(_in.crs, json.features, out));
case 'feature':
return Promise.resolve(jsonFeaturesToFeatures(_in.crs, [json], out));
default:
throw new Error(`Unsupported GeoJSON type: '${json.type}`);
}
}
};