import toGeoJSON from './toGeoJson/togeojson';
export class AcquisitionPlansParser {
/**
* @alias AcquisitionPlansParser
* @constructor
* @param {Workers} workers
*/
constructor(workers) {
if (!workers) {
throw (new Error('AcquisitionPlansParser - constructor - missing workers instance'));
}
this.workers = workers;
this.InteriorCtor = WorldWind ? WorldWind.SurfacePolygon : null;
this.OutlineCtor = WorldWind ? WorldWind.Path : null;
this.ShapeAttributesCtor = WorldWind ? WorldWind.ShapeAttributes : null;
this.PositionCtor = WorldWind ? WorldWind.Position : null;
this.ColorCtor = WorldWind ? WorldWind.Color : null;
/**
* @type {DOMParser|null}
*/
this.domParser = null;
/**
* @type {Object}
*/
this.shapeAttributesMap = Object.create(null);
}
/**
* Parses an acquisition plan kml file.
* Parsing will be attempted in a web worker and if that fails it will be attemped on the main thread.
*
* @param {{ satName: String, url: String, type: String, interior: Boolean, outline: Boolean, outlineAlpha: Number, interiorAlpha: Number, highlightAlpha: Number, filterDate: String }} fileInfo
*
* @param {String} fileInfo.satName The satellite name for the acquisition plan. The convention is to use the short name: "s1a", "s2b", "s5p", etc.
* @param {String} fileInfo.url The url for the acquisition plan file
* @param {String} fileInfo.type A type for the web workers. Default is "downloadAndParseKmls"
* @param {Boolean} fileInfo.interior A flag that indicates if interior renderables should be created. Default is true
* @param {Boolean} fileInfo.outline A flag that indicates if outline renderables should be created. Default is true
* @param {Number} fileInfo.outlineAlpha Alpha value fot the outline. Default is 1
* @param {Number} fileInfo.interiorAlpha Alpha value for the interior. Default is 0.2
* @param {Number} fileInfo.highlightAlpha Alpha value for the interior highlight. Default is 0.5
* @param {String} fileInfo.filterDate An ISODate string used for filtering out shapes that lees than this value. Default is the current date
*
* @param {Function} cb A callback function, will be called with error and entry params
*/
parse(fileInfo, cb) {
fileInfo.type = fileInfo.type || 'downloadAndParseKmls';
fileInfo.filterDate = fileInfo.filterDate || new Date().toISOString();
this.workers.process(fileInfo, (err, result) => {
if (err) {
return cb(err);
}
const { shapes, satName, url } = result;
let parsedShapes;
if (typeof shapes === 'string') {
console.info('Worker unable to parse kml file', url);
try {
parsedShapes = this.parseSync(shapes, satName, fileInfo.filterDate);
}
catch (error) {
return cb(error);
}
}
else {
parsedShapes = shapes;
}
let outlines;
let interiors;
let interval;
try {
({ outlines, interiors, interval } = this.makeRenderables(parsedShapes, fileInfo));
}
catch (error) {
return cb(error);
}
if ((outlines || interiors) && interval) {
return cb(null, this.formatOutput({ satName, url }, interval, outlines, interiors));
}
});
}
parseSync(kmlString, satName, filterDate) {
if (!this.domParser) {
this.domParser = new DOMParser();
}
const kmlDoc = this.domParser.parseFromString(kmlString, 'text/xml');
const geoJson = toGeoJSON.kml(kmlDoc);
let shapes = geoJson.features;
const isSentinel2 = satName.includes('s2');
const nowDate = new Date(filterDate);
shapes = shapes.filter(shape => {
const isInTheFuture = new Date(shape.properties.timespan.end) >= nowDate;
if (isSentinel2) {
return (
isInTheFuture &&
shape.properties.Mode === 'NOBS' &&
shape.properties.Timeliness === 'NOMINAL'
);
}
return isInTheFuture;
});
return shapes;
}
makeRenderables(shapes, fileInfo) {
const outlines = [];
const interiors = [];
const interval = {
startDate: Number.MAX_SAFE_INTEGER,
endDate: Number.MIN_SAFE_INTEGER
};
for (let i = 0; i < shapes.length; i++) {
let shape = shapes[i];
let coords = shape.coordinates;
let color = shape.color;
let extendedData = shape.extendedData;
if (shape.geometry) {
coords = shape.geometry.coordinates[0];
}
if (shape.properties) {
color = shape.properties.lineColor;
extendedData = shape.properties;
}
let positions = this.makePositions(coords);
if (fileInfo.outline !== false) {
let pathAttributes = this.makePathAttributes(color, fileInfo.outlineAlpha);
let path = new this.OutlineCtor(positions, pathAttributes);
path.altitudeMode = 'clampToGround';
path.followTerrain = true;
path.expiryTime = Number.MAX_SAFE_INTEGER;
this.setTimeInterval(path, extendedData, interval);
path.kmlProps.satName = fileInfo.satName;
outlines.push(path);
}
if (fileInfo.interior !== false) {
let surfacePolygonAttributes = this.makeSurfacePolygonAttributes(color, fileInfo.interiorAlpha);
let surfacePolygon = new this.InteriorCtor(positions, surfacePolygonAttributes);
surfacePolygon.highlightAttributes = this.makeSurfacePolygonHighlightAttributes(color, fileInfo.highlightAlpha);
this.setTimeInterval(surfacePolygon, extendedData, interval);
surfacePolygon.kmlProps.satName = fileInfo.satName;
interiors.push(surfacePolygon);
}
}
interval.startDate = new Date(interval.startDate);
interval.endDate = new Date(interval.endDate);
return {
outlines,
interiors,
interval,
};
}
makePositions(coords) {
const positions = [];
for (let i = 0, len = coords.length; i < len; i++) {
let coord = coords[i];
let pos = new this.PositionCtor(
coord[1] || coord.latitude,
coord[0] || coord.longitude,
coord[2] || 0
);
if (pos.longitude < -180) {
pos.longitude = pos.longitude + 360;
}
if (pos.longitude > 180) {
pos.longitude = pos.longitude - 360;
}
positions.push(pos);
}
return positions;
}
makePathAttributes(hexColor, alpha = 1) {
const key = hexColor + 'path';
if (this.shapeAttributesMap[key]) {
return this.shapeAttributesMap[key];
}
const attributes = new this.ShapeAttributesCtor(null);
attributes.drawOutline = true;
attributes.drawInterior = false;
attributes.outlineColor = this.makeColor(hexColor, alpha);
this.shapeAttributesMap[key] = attributes;
return attributes;
}
makeSurfacePolygonAttributes(props, alpha = 0.2) {
const hexColor = props.lineColor || props;
const key = hexColor + 'poly';
if (this.shapeAttributesMap[key]) {
return this.shapeAttributesMap[key];
}
const attributes = new this.ShapeAttributesCtor(null);
attributes.drawOutline = false;
attributes.drawInterior = true;
attributes.interiorColor = this.makeColor(hexColor, alpha);
this.shapeAttributesMap[key] = attributes;
return attributes;
}
makeSurfacePolygonHighlightAttributes(props, alpha = 0.5) {
const hexColor = props.lineColor || props;
const attributes = new this.ShapeAttributesCtor(null);
attributes.drawOutline = false;
attributes.drawInterior = true;
attributes.interiorColor = this.makeColor(hexColor, alpha);
return attributes;
}
makeColor(hexColor, alpha) {
const blue = hexColor.substring(2, 4);
const green = hexColor.substring(4, 6);
const red = hexColor.substring(6, 8);
const r = parseInt(red, 16) / 255;
const g = parseInt(green, 16) / 255;
const b = parseInt(blue, 16) / 255;
return new this.ColorCtor(r, g, b, alpha);
}
setTimeInterval(renderable, props, interval) {
const startTime = this.dateStringToUTC(props.ObservationTimeStart || props.timespan.begin);
const endTime = this.dateStringToUTC(props.ObservationTimeStop || props.timespan.end);
const startTimeMs = (new Date(startTime)).getTime();
const endTimeMs = (new Date(endTime)).getTime();
if (interval.startDate > startTimeMs) {
interval.startDate = startTimeMs;
}
if (interval.endDate < endTimeMs) {
interval.endDate = endTimeMs;
}
renderable.kmlProps = props;
renderable.kmlProps.startDate = new Date(startTimeMs);
renderable.kmlProps.endDate = new Date(endTimeMs);
renderable.type = 'acqPlan';
}
dateStringToUTC(dateString) {
const lastChar = dateString[dateString.length - 1];
if (lastChar.toLowerCase() !== 'z') {
return dateString + 'Z';
}
return dateString;
}
formatOutput(fileInfo, interval, outlines = [], interiors = []) {
return {
url: fileInfo.url,
satName: fileInfo.satName,
outlines: outlines,
interiors: interiors,
startDate: interval.startDate,
endDate: interval.endDate,
};
}
}