var inherit = require('./inherit');
var registerFileReader = require('./registry').registerFileReader;
var fileReader = require('./fileReader');
/**
* Object specification for a geojsonReader.
*
* @typedef {geo.fileReader.spec} geo.geojsonReader.spec
* @extends geo.fileReader.spec
* @property {geo.pointFeature.styleSpec} [pointStyle] Default style for
* points.
* @property {geo.pointFeature.styleSpec} [lineStyle] Default style for lines.
* @property {geo.pointFeature.styleSpec} [polygonStyle] Default style for
* polygons.
*/
/**
* Create a new instance of class geo.geojsonReader.
*
* @class
* @alias geo.geojsonReader
* @extends geo.fileReader
* @param {geo.fileReader.spec} arg
* @returns {geo.geojsonReader}
*/
var geojsonReader = function (arg) {
'use strict';
if (!(this instanceof geojsonReader)) {
return new geojsonReader(arg);
}
var $ = require('jquery');
var convertColor = require('./util').convertColor;
var markerFeature = require('./markerFeature');
var transform = require('./transform');
var m_this = this,
m_options = {
...arg,
pointStyle: {
fill: true,
fillColor: '#ff7800',
fillOpacity: 0.8,
stroke: true,
strokeColor: '#000',
strokeWidth: 1,
strokeOpacity: 1,
radius: 8,
...arg.pointStyle
},
lineStyle: {
strokeColor: '#ff7800',
strokeWidth: 4,
strokeOpacity: 0.5,
strokeOffset: 0,
lineCap: 'butt',
lineJoin: 'miter',
uniformLine: true,
closed: false,
...arg.lineStyle
},
polygonStyle: {
fill: true,
fillColor: '#b0de5c',
fillOpacity: 0.8,
stroke: true,
strokeColor: '#999999',
strokeWidth: 2,
strokeOpacity: 1,
uniformPolygon: true,
...arg.polygonStyle
}
};
fileReader.call(this, m_options);
/**
* Tells the caller if it can handle the given file by returning a boolean.
*
* @param {File|Blob|string|object} file This is either a `File` object, a
* `Blob` object, a string representation of a file, or an object
* representing data from a file.
* @returns {boolean} `true` if this reader can read a file.
*/
this.canRead = function (file) {
if (file instanceof File || file instanceof Blob) {
return !!(file.type === 'application/json' || (file.name && file.name.match(/\.json$/)));
} else if (typeof file === 'string') {
try {
JSON.parse(file);
} catch (e) {
return false;
}
return true;
}
try {
if (Array.isArray(m_this._featureArray(file))) {
return true;
}
} catch (e) {}
return false;
};
/**
* Read or parse a file or object, then call a done function.
*
* @param {File|Blob|string|object} file This is either a `File` object, a
* `Blob` object, a string representation of a file, or an object
* representing data from a file.
* @param {function} done A callback function when the read is complete.
* This is called with `false` on error or the object that was read but
* not yet parsed.
* @param {function} [progress] A function which is passed `ProgressEvent`
* information from a `FileReader`. This includes `loaded` and `total`
* each with a number of bytes.
*/
this._readObject = function (file, done, progress) {
var object;
function onDone(fileString) {
// if fileString is not a JSON string, expect it to be a URL.
try {
object = JSON.parse(fileString);
done(object);
} catch (err) {
if (!object) {
$.ajax({
type: 'GET',
url: fileString,
dataType: 'text'
}).done(function (data) {
try {
object = JSON.parse(data);
done(object);
} catch (err) {
if (!object) {
done(false);
}
}
}).fail(function () {
done(false);
});
}
}
}
if (file instanceof File || file instanceof Blob) {
m_this._getString(file, onDone, progress);
} else if (typeof file === 'string') {
onDone(file);
} else {
done(file);
}
};
/**
* Return an array of normalized geojson features. This turns bare
* geometries into features and multi-geometry features into single geometry
* features.
*
* Returns an array of Point, LineString, or Polygon features.
* @param {geojson.object} spec A parsed geojson object.
* @returns {geojson.FeatureObject[]} An array of feature objects, none of
* which include multi-geometries, and none have empty geometries.
*/
this._featureArray = function (spec) {
var features, normalized = [];
switch (spec.type) {
case 'FeatureCollection':
features = spec.features;
break;
case 'Feature':
features = [spec];
break;
case 'GeometryCollection':
features = spec.geometries.map(function (g) {
return {
type: 'Feature',
geometry: g,
properties: {}
};
});
break;
case 'Point':
case 'LineString':
case 'Polygon':
case 'MultiPoint':
case 'MultiLineString':
case 'MultiPolygon':
features = [{
type: 'Feature',
geometry: spec,
properties: {}
}];
break;
default:
throw new Error('Invalid json type');
}
// flatten multi features
features.forEach(function (feature) {
Array.prototype.push.apply(normalized, m_this._feature(feature));
});
// remove features with empty geometries
normalized = normalized.filter(function (feature) {
return feature.geometry &&
feature.geometry.coordinates &&
feature.geometry.coordinates.length;
});
return normalized;
};
/**
* Normalize a feature object turning multi geometry features into an array
* of features, and single geometry features into an array containing one
* feature.
*
* @param {geojson.object} spec A parsed geojson object.
* @returns {geojson.FeatureObject[]} An array of feature objects, none of
* which include multi-geometries.
*/
this._feature = function (spec) {
if (spec.type !== 'Feature') {
throw new Error('Invalid feature object');
}
switch (spec.geometry.type) {
case 'Point':
case 'LineString':
case 'Polygon':
return [spec];
case 'MultiPoint':
case 'MultiLineString':
case 'MultiPolygon':
return spec.geometry.coordinates.map(function (c) {
return {
type: 'Feature',
geometry: {
type: spec.geometry.type.replace('Multi', ''),
coordinates: c
},
properties: spec.properties
};
});
default:
throw new Error('Invalid geometry type');
}
};
/**
* Convert from a geojson position array into a geojs position object.
*
* @param {number[]} p A coordinate in the form of an array with two or three
* components.
* @returns {geo.geoPosition}
*/
this._position = function (p) {
return {
x: p[0],
y: p[1],
z: p[2] || 0
};
};
/**
* Defines a style accessor the returns the given value of the property
* object or a default value.
*
* @param {string} prop The property name.
* @param {object} _default The default value.
* @returns {function} A style function for the property.
*/
this._style = function (prop, _default) {
var isColor = prop.toLowerCase().match(/color$/);
if (isColor) {
_default = convertColor(_default);
}
return function (v, j, d, i) {
var p;
if (d !== undefined && d.properties) {
p = d.properties;
} else {
p = v.properties;
}
if (p !== undefined && p.hasOwnProperty(prop)) {
return isColor ? convertColor(p[prop]) : p[prop];
}
return _default;
};
};
/**
* Reads the file and optionally calls a function when finished. The `done`
* function is called with a list of {@link geo.feature} on success or
* `false` on failure.
*
* @param {File|Blob|string|object} file This is either a `File` object, a
* `Blob` object, a string representation of a file, or an object
* representing data from a file.
* @param {function} [done] An optional callback function when the read is
* complete. This is called with `false` on error or a list of
* {@link geo.feature} on success.
* @param {function} [progress] A function which is passed `ProgressEvent`
* information from a `FileReader`. This includes `loaded` and `total`
* each with a number of bytes.
* @returns {Promise} A `Promise` that resolves with a list of
* {@link geo.feature} or is rejected if the reader fails.
*/
this.read = function (file, done, progress) {
var promise = new Promise(function (resolve, reject) {
/**
* Check if a feature is a circle or an ellipse.
*
* @param {geojson.object} f A geojson feature.
* @returns {boolean} true if this should be rendered as an ellipse or
* circle.
*/
function _isEllipse(f) {
if (f.geometry.type !== 'Polygon' || f.geometry.coordinates.length !== 1 || f.geometry.coordinates[0].length !== 5) {
return false;
}
return (
(f.properties || {}).annotationType === 'ellipse' ||
(f.properties || {}).annotationType === 'circle');
}
/**
* Given a parsed GeoJSON object, convert it into features on the
* reader's layer.
*
* @param {geojson.object|false} object Either a parse GeoJSON object or
* `false` for an error.
*/
function _done(object) {
if (object === false) {
if (done) {
done(object);
}
reject(new Error('Failed to parse GeoJSON'));
return;
}
let features, feature;
const allFeatures = [];
try {
features = m_this._featureArray(object);
} catch (err) {
reject(err);
return;
}
// process points
const points = features.filter(f => f.geometry.type === 'Point');
if (points.length) {
feature = m_this.layer().createFeature('point');
if (feature) {
feature
.data(points)
.position(d => m_this._position(d.geometry.coordinates))
// create an object with each property in m_options.pointStyle,
// mapping the values through the _style function.
.style(
[{}].concat(Object.keys(m_options.pointStyle)).reduce(
(styleObj, key) => ({
[key]: points.some(d => d.properties && d.properties[key] !== undefined) ?
m_this._style(key, m_options.pointStyle[key]) :
m_options.pointStyle[key],
...styleObj
}
))
);
allFeatures.push(feature);
}
}
// process lines
const lines = features.filter(f => f.geometry.type === 'LineString');
if (lines.length) {
feature = m_this.layer().createFeature('line');
if (feature) {
feature
.data(lines)
.line(d => d.geometry.coordinates)
.position(m_this._position)
// create an object with each property in m_options.lineStyle,
// mapping the values through the _style function.
.style(
[{}].concat(Object.keys(m_options.lineStyle)).reduce(
(styleObj, key) => ({
[key]: lines.some(d => d.properties && d.properties[key] !== undefined) ?
m_this._style(key, m_options.lineStyle[key]) :
m_options.lineStyle[key],
...styleObj
}
))
);
allFeatures.push(feature);
}
}
// process polygons
const polygons = features.filter(f => f.geometry.type === 'Polygon' && !_isEllipse(f));
if (polygons.length) {
feature = m_this.layer().createFeature('polygon');
if (feature) {
feature
.data(polygons)
.polygon((d, i) => ({
outer: d.geometry.coordinates[0],
inner: d.geometry.coordinates.slice(1)
}))
.position(m_this._position)
// create an object with each property in m_options.polygonStyle,
// mapping the values through the _style function.
.style(
[{}].concat(Object.keys(m_options.polygonStyle)).reduce(
(styleObj, key) => ({
[key]: polygons.some(d => d.properties && d.properties[key] !== undefined) ?
m_this._style(key, m_options.polygonStyle[key]) :
m_options.polygonStyle[key],
...styleObj
}
))
);
allFeatures.push(feature);
}
}
// handle ellipses and circle
const ellipses = features.filter(_isEllipse);
if (ellipses.length) {
feature = m_this.layer().createFeature('marker');
if (feature) {
ellipses.forEach((d) => {
const map = m_this.layer().map();
const coord = transform.transformCoordinates(map.ingcs(), map.gcs(), d.geometry.coordinates[0]);
const w = ((coord[0][0] - coord[1][0]) ** 2 + (coord[0][1] - coord[1][1]) ** 2) ** 0.5;
const h = ((coord[0][0] - coord[3][0]) ** 2 + (coord[0][1] - coord[3][1]) ** 2) ** 0.5;
const radius = Math.max(w, h) / 2 / map.unitsPerPixel(0);
const aspect = w ? h / w : 1e20;
const rotation = -Math.atan2(coord[1][1] - coord[0][1], coord[1][0] - coord[0][0]);
const pos = transform.transformCoordinates(map.gcs(), map.ingcs(), {
x: (coord[0][0] + coord[1][0] + coord[2][0] + coord[3][0]) / 4,
y: (coord[0][1] + coord[1][1] + coord[2][1] + coord[3][1]) / 4
});
d._props = {
pos: pos,
radius: radius,
aspect: aspect,
rotation: rotation
};
});
feature
.data(ellipses)
.position((d) => d._props.pos)
// create an object with each property in m_options.polygonStyle,
// mapping the values through the _style function.
.style(
[{}].concat(Object.keys(m_options.polygonStyle)).reduce(
(styleObj, key) => ({
[key]: ellipses.some(d => d.properties && d.properties[key] !== undefined) ?
m_this._style(key, m_options.polygonStyle[key]) :
m_options.polygonStyle[key],
radius: (d) => d._props.radius,
radiusIncludesStroke: false,
symbolValue: (d) => d._props.aspect,
rotation: (d) => d._props.rotation,
strokeOffset: 0,
rotateWithMap: true,
scaleWithZoom: markerFeature.scaleMode.fill,
...styleObj
}
))
);
allFeatures.push(feature);
}
}
if (done) {
done(allFeatures);
}
resolve(allFeatures);
}
m_this._readObject(file, _done, progress);
});
m_this.addPromise(promise);
return promise;
};
};
inherit(geojsonReader, fileReader);
registerFileReader('geojsonReader', geojsonReader);
// Also register under an alternate name (alias for backwards compatibility)
registerFileReader('jsonReader', geojsonReader);
module.exports = geojsonReader;