node-red-contrib-tak-registration
Version:
A Node-RED node to register to TAK and to help wrap files as datapackages to send to TAK
785 lines (722 loc) • 20.4 kB
JavaScript
import Coordinate from '../geom/Coordinate'
import GeometryFactory from '../geom/GeometryFactory'
/**
* The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
* or measure ('M') coordinate is available. Supported values are `'XY'`,
* `'XYZ'`, `'XYM'`, `'XYZM'`.
* @enum {string}
*/
const GeometryLayout = {
XY: 'XY',
XYZ: 'XYZ',
XYM: 'XYM',
XYZM: 'XYZM',
}
/**
* The geometry type. One of `'Point'`, `'LineString'`, `'LinearRing'`,
* `'Polygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPolygon'`,
* `'GeometryCollection'`, `'Circle'`.
* @enum {string}
*/
const GeometryType = {
POINT: 'Point',
LINE_STRING: 'LineString',
LINEAR_RING: 'LinearRing',
POLYGON: 'Polygon',
MULTI_POINT: 'MultiPoint',
MULTI_LINE_STRING: 'MultiLineString',
MULTI_POLYGON: 'MultiPolygon',
GEOMETRY_COLLECTION: 'GeometryCollection',
CIRCLE: 'Circle',
}
/**
* @typedef {Object} Options
* @property {boolean} [splitCollection=false] Whether to split GeometryCollections into
* multiple features on reading.
*/
/**
* @typedef {Object} Token
* @property {number} type
* @property {number|string} [value]
* @property {number} position
*/
/**
* @const
* @type {string}
*/
const EMPTY = 'EMPTY'
/**
* @const
* @type {string}
*/
const Z = 'Z'
/**
* @const
* @type {string}
*/
const M = 'M'
/**
* @const
* @type {string}
*/
const ZM = 'ZM'
/**
* @const
* @enum {number}
*/
const TokenType = {
TEXT: 1,
LEFT_PAREN: 2,
RIGHT_PAREN: 3,
NUMBER: 4,
COMMA: 5,
EOF: 6,
}
/**
* @const
* @type {Object<string, string>}
*/
const WKTGeometryType = {}
for (const type in GeometryType)
WKTGeometryType[type] = GeometryType[type].toUpperCase()
/**
* Class to tokenize a WKT string.
*/
class Lexer {
/**
* @param {string} wkt WKT string.
*/
constructor(wkt) {
/**
* @type {string}
*/
this.wkt = wkt
/**
* @type {number}
* @private
*/
this.index_ = -1
}
/**
* @param {string} c Character.
* @return {boolean} Whether the character is alphabetic.
* @private
*/
isAlpha_(c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
/**
* @param {string} c Character.
* @param {boolean=} opt_decimal Whether the string number
* contains a dot, i.e. is a decimal number.
* @return {boolean} Whether the character is numeric.
* @private
*/
isNumeric_(c, opt_decimal) {
const decimal = opt_decimal !== undefined ? opt_decimal : false
return (c >= '0' && c <= '9') || (c == '.' && !decimal)
}
/**
* @param {string} c Character.
* @return {boolean} Whether the character is whitespace.
* @private
*/
isWhiteSpace_(c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
/**
* @return {string} Next string character.
* @private
*/
nextChar_() {
return this.wkt.charAt(++this.index_)
}
/**
* Fetch and return the next token.
* @return {!Token} Next string token.
*/
nextToken() {
const c = this.nextChar_()
const position = this.index_
/** @type {number|string} */
let value = c
let type
if (c == '(') {
type = TokenType.LEFT_PAREN
} else if (c == ',') {
type = TokenType.COMMA
} else if (c == ')') {
type = TokenType.RIGHT_PAREN
} else if (this.isNumeric_(c) || c == '-') {
type = TokenType.NUMBER
value = this.readNumber_()
} else if (this.isAlpha_(c)) {
type = TokenType.TEXT
value = this.readText_()
} else if (this.isWhiteSpace_(c)) {
return this.nextToken()
} else if (c === '') {
type = TokenType.EOF
} else {
throw new Error('Unexpected character: ' + c)
}
return { position: position, value: value, type: type }
}
/**
* @return {number} Numeric token value.
* @private
*/
readNumber_() {
let c
const index = this.index_
let decimal = false
let scientificNotation = false
do {
if (c == '.')
decimal = true
else if (c == 'e' || c == 'E')
scientificNotation = true
c = this.nextChar_()
} while (
this.isNumeric_(c, decimal) ||
// if we haven't detected a scientific number before, 'e' or 'E'
// hint that we should continue to read
(!scientificNotation && (c == 'e' || c == 'E')) ||
// once we know that we have a scientific number, both '-' and '+'
// are allowed
(scientificNotation && (c == '-' || c == '+'))
)
return parseFloat(this.wkt.substring(index, this.index_--))
}
/**
* @return {string} String token value.
* @private
*/
readText_() {
let c
const index = this.index_
do
c = this.nextChar_()
while (this.isAlpha_(c))
return this.wkt.substring(index, this.index_--).toUpperCase()
}
}
/**
* Class to parse the tokens from the WKT string.
*/
class Parser {
/**
* @param {Lexer} lexer The lexer.
*/
constructor(lexer, factory) {
/**
* @type {Lexer}
* @private
*/
this.lexer_ = lexer
/**
* @type {Token}
* @private
*/
this.token_
/**
* @type {import("../geom/GeometryLayout.js").default}
* @private
*/
this.layout_ = GeometryLayout.XY
this.factory = factory
}
/**
* Fetch the next token form the lexer and replace the active token.
* @private
*/
consume_() {
this.token_ = this.lexer_.nextToken()
}
/**
* Tests if the given type matches the type of the current token.
* @param {TokenType} type Token type.
* @return {boolean} Whether the token matches the given type.
*/
isTokenType(type) {
const isMatch = this.token_.type == type
return isMatch
}
/**
* If the given type matches the current token, consume it.
* @param {TokenType} type Token type.
* @return {boolean} Whether the token matches the given type.
*/
match(type) {
const isMatch = this.isTokenType(type)
if (isMatch)
this.consume_()
return isMatch
}
/**
* Try to parse the tokens provided by the lexer.
* @return {import("../geom/Geometry.js").default} The geometry.
*/
parse() {
this.consume_()
const geometry = this.parseGeometry_()
return geometry
}
/**
* Try to parse the dimensional info.
* @return {import("../geom/GeometryLayout.js").default} The layout.
* @private
*/
parseGeometryLayout_() {
let layout = GeometryLayout.XY
const dimToken = this.token_
if (this.isTokenType(TokenType.TEXT)) {
const dimInfo = dimToken.value
if (dimInfo === Z)
layout = GeometryLayout.XYZ
else if (dimInfo === M)
layout = GeometryLayout.XYM
else if (dimInfo === ZM)
layout = GeometryLayout.XYZM
if (layout !== GeometryLayout.XY)
this.consume_()
}
return layout
}
/**
* @return {!Array<import("../geom/Geometry.js").default>} A collection of geometries.
* @private
*/
parseGeometryCollectionText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const geometries = []
do
geometries.push(this.parseGeometry_())
while (this.match(TokenType.COMMA))
if (this.match(TokenType.RIGHT_PAREN))
return geometries
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {Array<number>} All values in a point.
* @private
*/
parsePointText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const coordinates = this.parsePoint_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return null
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<number>>} All points in a linestring.
* @private
*/
parseLineStringText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const coordinates = this.parsePointList_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<!Array<number>>>} All points in a polygon.
* @private
*/
parsePolygonText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const coordinates = this.parseLineStringTextList_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<number>>} All points in a multipoint.
* @private
*/
parseMultiPointText_() {
if (this.match(TokenType.LEFT_PAREN)) {
let coordinates
if (this.token_.type == TokenType.LEFT_PAREN)
coordinates = this.parsePointTextList_()
else
coordinates = this.parsePointList_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<!Array<number>>>} All linestring points
* in a multilinestring.
* @private
*/
parseMultiLineStringText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const coordinates = this.parseLineStringTextList_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<!Array<!Array<number>>>>} All polygon points in a multipolygon.
* @private
*/
parseMultiPolygonText_() {
if (this.match(TokenType.LEFT_PAREN)) {
const coordinates = this.parsePolygonTextList_()
if (this.match(TokenType.RIGHT_PAREN))
return coordinates
} else if (this.isEmptyGeometry_()) {
return []
}
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<number>} A point.
* @private
*/
parsePoint_() {
const coordinates = []
const dimensions = this.layout_.length
for (let i = 0; i < dimensions; ++i) {
const token = this.token_
if (this.match(TokenType.NUMBER))
coordinates.push(/** @type {number} */(token.value))
else
break
}
if (coordinates.length == dimensions)
return coordinates
throw new Error(this.formatErrorMessage_())
}
/**
* @return {!Array<!Array<number>>} An array of points.
* @private
*/
parsePointList_() {
const coordinates = [this.parsePoint_()]
while (this.match(TokenType.COMMA))
coordinates.push(this.parsePoint_())
return coordinates
}
/**
* @return {!Array<!Array<number>>} An array of points.
* @private
*/
parsePointTextList_() {
const coordinates = [this.parsePointText_()]
while (this.match(TokenType.COMMA))
coordinates.push(this.parsePointText_())
return coordinates
}
/**
* @return {!Array<!Array<!Array<number>>>} An array of points.
* @private
*/
parseLineStringTextList_() {
const coordinates = [this.parseLineStringText_()]
while (this.match(TokenType.COMMA))
coordinates.push(this.parseLineStringText_())
return coordinates
}
/**
* @return {!Array<!Array<!Array<!Array<number>>>>} An array of points.
* @private
*/
parsePolygonTextList_() {
const coordinates = [this.parsePolygonText_()]
while (this.match(TokenType.COMMA))
coordinates.push(this.parsePolygonText_())
return coordinates
}
/**
* @return {boolean} Whether the token implies an empty geometry.
* @private
*/
isEmptyGeometry_() {
const isEmpty =
this.isTokenType(TokenType.TEXT) && this.token_.value == EMPTY
if (isEmpty)
this.consume_()
return isEmpty
}
/**
* Create an error message for an unexpected token error.
* @return {string} Error message.
* @private
*/
formatErrorMessage_() {
return (
'Unexpected `' +
this.token_.value +
'` at position ' +
this.token_.position +
' in `' +
this.lexer_.wkt +
'`'
)
}
/**
* @return {!import("../geom/Geometry.js").default} The geometry.
* @private
*/
parseGeometry_() {
const factory = this.factory
const o2c = ordinates => new Coordinate(...ordinates)
const ca2p = coordinates => {
const rings = coordinates.map(a => factory.createLinearRing(a.map(o2c)))
if (rings.length > 1)
return factory.createPolygon(rings[0], rings.slice(1))
else
return factory.createPolygon(rings[0])
}
const token = this.token_
if (this.match(TokenType.TEXT)) {
const geomType = token.value
this.layout_ = this.parseGeometryLayout_()
if (geomType == 'GEOMETRYCOLLECTION') {
const geometries = this.parseGeometryCollectionText_()
return factory.createGeometryCollection(geometries)
} else {
switch (geomType) {
case 'POINT': {
const ordinates = this.parsePointText_()
if (!ordinates)
return factory.createPoint()
return factory.createPoint(new Coordinate(...ordinates))
}
case 'LINESTRING': {
const coordinates = this.parseLineStringText_()
const components = coordinates.map(o2c)
return factory.createLineString(components)
}
case 'LINEARRING': {
const coordinates = this.parseLineStringText_()
const components = coordinates.map(o2c)
return factory.createLinearRing(components)
}
case 'POLYGON': {
const coordinates = this.parsePolygonText_()
if (!coordinates || coordinates.length === 0)
return factory.createPolygon()
return ca2p(coordinates)
}
case 'MULTIPOINT': {
const coordinates = this.parseMultiPointText_()
if (!coordinates || coordinates.length === 0)
return factory.createMultiPoint()
const components = coordinates.map(o2c).map(c => factory.createPoint(c))
return factory.createMultiPoint(components)
}
case 'MULTILINESTRING': {
const coordinates = this.parseMultiLineStringText_()
const components = coordinates.map(a => factory.createLineString(a.map(o2c)))
return factory.createMultiLineString(components)
}
case 'MULTIPOLYGON': {
const coordinates = this.parseMultiPolygonText_()
if (!coordinates || coordinates.length === 0)
return factory.createMultiPolygon()
const polygons = coordinates.map(ca2p)
return factory.createMultiPolygon(polygons)
}
default: {
throw new Error('Invalid geometry type: ' + geomType)
}
}
}
}
throw new Error(this.formatErrorMessage_())
}
}
/**
* @param {Point} geom Point geometry.
* @return {string} Coordinates part of Point as WKT.
*/
function encodePointGeometry(geom) {
if (geom.isEmpty())
return ''
const c = geom.getCoordinate()
const cs = [c.x, c.y]
if (c.z !== undefined && !Number.isNaN(c.z))
cs.push(c.z)
if (c.m !== undefined && !Number.isNaN(c.m))
cs.push(c.m)
return cs.join(' ')
}
/**
* @param {MultiPoint} geom MultiPoint geometry.
* @return {string} Coordinates part of MultiPoint as WKT.
*/
function encodeMultiPointGeometry(geom) {
const array = []
for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i)
array.push('(' + encodePointGeometry(geom.getGeometryN(i)) + ')')
return array.join(', ')
}
/**
* @param {GeometryCollection} geom GeometryCollection geometry.
* @return {string} Coordinates part of GeometryCollection as WKT.
*/
function encodeGeometryCollectionGeometry(geom) {
const array = []
for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i)
array.push(encode(geom.getGeometryN(i)))
return array.join(', ')
}
/**
* @param {LineString|import("../geom/LinearRing.js").default} geom LineString geometry.
* @return {string} Coordinates part of LineString as WKT.
*/
function encodeLineStringGeometry(geom) {
const coordinates = geom.getCoordinates()
.map(c => {
const a = [c.x, c.y]
if (c.z !== undefined && !Number.isNaN(c.z))
a.push(c.z)
if (c.m !== undefined && !Number.isNaN(c.m))
a.push(c.m)
return a
})
const array = []
for (let i = 0, ii = coordinates.length; i < ii; ++i)
array.push(coordinates[i].join(' '))
return array.join(', ')
}
/**
* @param {MultiLineString} geom MultiLineString geometry.
* @return {string} Coordinates part of MultiLineString as WKT.
*/
function encodeMultiLineStringGeometry(geom) {
const array = []
for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i)
array.push('(' + encodeLineStringGeometry(geom.getGeometryN(i)) + ')')
return array.join(', ')
}
/**
* @param {Polygon} geom Polygon geometry.
* @return {string} Coordinates part of Polygon as WKT.
*/
function encodePolygonGeometry(geom) {
const array = []
array.push('(' + encodeLineStringGeometry(geom.getExteriorRing()) + ')')
for (let i = 0, ii = geom.getNumInteriorRing(); i < ii; ++i)
array.push('(' + encodeLineStringGeometry(geom.getInteriorRingN(i)) + ')')
return array.join(', ')
}
/**
* @param {MultiPolygon} geom MultiPolygon geometry.
* @return {string} Coordinates part of MultiPolygon as WKT.
*/
function encodeMultiPolygonGeometry(geom) {
const array = []
for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i)
array.push('(' + encodePolygonGeometry(geom.getGeometryN(i)) + ')')
return array.join(', ')
}
/**
* @param {Geometry} geom Geometry geometry.
* @return {string} Potential dimensional information for WKT type.
*/
function encodeGeometryLayout(geom) {
let dimInfo = ''
if (geom.isEmpty())
return dimInfo
const c = geom.getCoordinate()
if (c.z !== undefined && !Number.isNaN(c.z))
dimInfo += Z
if (c.m !== undefined && !Number.isNaN(c.m))
dimInfo += M
return dimInfo
}
/**
* @const
* @type {Object<string, function(import("../geom/Geometry.js").default): string>}
*/
const GeometryEncoder = {
'Point': encodePointGeometry,
'LineString': encodeLineStringGeometry,
'LinearRing': encodeLineStringGeometry,
'Polygon': encodePolygonGeometry,
'MultiPoint': encodeMultiPointGeometry,
'MultiLineString': encodeMultiLineStringGeometry,
'MultiPolygon': encodeMultiPolygonGeometry,
'GeometryCollection': encodeGeometryCollectionGeometry,
}
/**
* Encode a geometry as WKT.
* @param {!import("../geom/Geometry.js").default} geom The geometry to encode.
* @return {string} WKT string for the geometry.
*/
function encode(geom) {
let type = geom.getGeometryType()
const geometryEncoder = GeometryEncoder[type]
type = type.toUpperCase()
const dimInfo = encodeGeometryLayout(geom)
if (dimInfo.length > 0)
type += ' ' + dimInfo
if (geom.isEmpty())
return type + ' ' + EMPTY
const enc = geometryEncoder(geom)
return type + ' (' + enc + ')'
}
/**
* Class for reading and writing Well-Known Text.
*
* NOTE: Adapted from OpenLayers.
*/
export default class WKTParser {
/** Create a new parser for WKT
*
* @param {GeometryFactory} geometryFactory
* @return An instance of WKTParser.
* @private
*/
constructor(geometryFactory) {
this.geometryFactory = geometryFactory || new GeometryFactory()
this.precisionModel = this.geometryFactory.getPrecisionModel()
}
/**
* Deserialize a WKT string and return a geometry. Supports WKT for POINT,
* MULTIPOINT, LINESTRING, LINEARRING, MULTILINESTRING, POLYGON, MULTIPOLYGON,
* and GEOMETRYCOLLECTION.
*
* @param {String} wkt A WKT string.
* @return {Geometry} A geometry instance.
* @private
*/
read(wkt) {
const lexer = new Lexer(wkt)
const parser = new Parser(lexer, this.geometryFactory)
const geometry = parser.parse()
return geometry
}
/**
* Serialize a geometry into a WKT string.
*
* @param {Geometry} geometry A feature or array of features.
* @return {String} The WKT string representation of the input geometries.
* @private
*/
write(geometry) {
return encode(geometry)
}
}