diff --git a/babel.config.js b/babel.config.js index 0f1515e6..84afd073 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,7 +2,7 @@ module.exports = { presets: [ ['@babel/env', { targets: { - node: ['10'] + node: '10' } }] ] diff --git a/src/java/lang/Double.js b/src/java/lang/Double.js index e73047f9..ea108cb3 100644 --- a/src/java/lang/Double.js +++ b/src/java/lang/Double.js @@ -2,6 +2,7 @@ import Long from './Long' export default function Double() { } +Double.NaN = NaN Double.isNaN = n => Number.isNaN(n) Double.isInfinite = n => !Number.isFinite(n) Double.MAX_VALUE = Number.MAX_VALUE diff --git a/src/org/locationtech/jts/io/WKTParser.js b/src/org/locationtech/jts/io/WKTParser.js index 5313bffc..f3906cff 100644 --- a/src/org/locationtech/jts/io/WKTParser.js +++ b/src/org/locationtech/jts/io/WKTParser.js @@ -1,19 +1,763 @@ import Coordinate from '../geom/Coordinate' import GeometryFactory from '../geom/GeometryFactory' -const regExes = { - typeStr: /^\s*(\w+)\s*\(\s*(.*)\s*\)\s*$/, - emptyTypeStr: /^\s*(\w+)\s*EMPTY\s*$/, - spaces: /\s+/, - parenComma: /\)\s*,\s*\(/, - doubleParenComma: /\)\s*\)\s*,\s*\(\s*\(/, // can't use {2} here - trimParens: /^\s*\(?(.*?)\)?\s*$/ +/** + * 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} + */ +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} 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} 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>} 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>>} 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>} 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>>} 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>>>} 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} 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>} An array of points. + * @private + */ + parsePointList_() { + const coordinates = [this.parsePoint_()] + while (this.match(TokenType.COMMA)) + coordinates.push(this.parsePoint_()) + + return coordinates + } + + /** + * @return {!Array>} An array of points. + * @private + */ + parsePointTextList_() { + const coordinates = [this.parsePointText_()] + while (this.match(TokenType.COMMA)) + coordinates.push(this.parsePointText_()) + + return coordinates + } + + /** + * @return {!Array>>} An array of points. + * @private + */ + parseLineStringTextList_() { + const coordinates = [this.parseLineStringText_()] + while (this.match(TokenType.COMMA)) + coordinates.push(this.parseLineStringText_()) + + return coordinates + } + + /** + * @return {!Array>>>} 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 coordinate = geom.getCoordinate() + const coordinates = [coordinate.x, coordinate.y] + if (coordinate.z) + coordinates.push(coordinate.z) + if (coordinate.m) + coordinates.push(coordinate.m) + return coordinates.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 => [c.x, c.y]) + 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) + dimInfo += Z + if (c.m) + dimInfo += M + return dimInfo +} + +/** + * @const + * @type {Object} + */ +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 2.11 implementation. + * NOTE: Adapted from OpenLayers. */ export default class WKTParser { @@ -38,21 +782,9 @@ export default class WKTParser { * @private */ read(wkt) { - let geometry, type, str - wkt = wkt.replace(/[\n\r]/g, ' ') - let matches = regExes.typeStr.exec(wkt) - if (wkt.search('EMPTY') !== -1) { - matches = regExes.emptyTypeStr.exec(wkt) - matches[2] = undefined - } - if (matches) { - type = matches[1].toLowerCase() - str = matches[2] - if (parse[type]) geometry = parse[type].call(this, str) - } - - if (geometry === undefined) throw new Error('Could not parse WKT ' + wkt) - + const lexer = new Lexer(wkt) + const parser = new Parser(lexer, this.geometryFactory) + const geometry = parser.parse() return geometry } @@ -64,308 +796,6 @@ export default class WKTParser { * @private */ write(geometry) { - return this.extractGeometry(geometry) - } - - /** - * Entry point to construct the WKT for a single Geometry object. - * - * @param {Geometry} geometry - * @return {String} A WKT string of representing the geometry. - * @private - */ - extractGeometry(geometry) { - const type = geometry.getGeometryType().toLowerCase() - if (!extract[type]) return null - - const wktType = type.toUpperCase() - let data - if (geometry.isEmpty()) - data = wktType + ' EMPTY' - else data = wktType + '(' + extract[type].call(this, geometry) + ')' - - return data - } -} - -/** - * Object with properties corresponding to the geometry types. Property values - * are functions that do the actual data extraction. - * @private - */ -const extract = { - coordinate(coordinate) { - this.precisionModel.makePrecise(coordinate) - return coordinate.x + ' ' + coordinate.y - }, - - /** - * Return a space delimited string of point coordinates. - * - * @param {Point} - * point - * @return {String} A string of coordinates representing the point. - */ - point(point) { - return extract.coordinate.call(this, point._coordinates._coordinates[0]) - }, - - /** - * Return a comma delimited string of point coordinates from a multipoint. - * - * @param {MultiPoint} - * multipoint - * @return {String} A string of point coordinate strings representing the - * multipoint. - */ - multipoint(multipoint) { - const array = [] - for (let i = 0, len = multipoint._geometries.length; i < len; ++i) array.push('(' + extract.point.call(this, multipoint._geometries[i]) + ')') - - return array.join(',') - }, - - /** - * Return a comma delimited string of point coordinates from a line. - * - * @param {LineString} linestring - * @return {String} A string of point coordinate strings representing the linestring. - */ - linestring(linestring) { - const array = [] - for (let i = 0, len = linestring._points._coordinates.length; i < len; ++i) array.push(extract.coordinate.call(this, linestring._points._coordinates[i])) - - return array.join(',') - }, - - linearring(linearring) { - const array = [] - for (let i = 0, len = linearring._points._coordinates.length; i < len; ++i) array.push(extract.coordinate.call(this, linearring._points._coordinates[i])) - - return array.join(',') - }, - - /** - * Return a comma delimited string of linestring strings from a - * multilinestring. - * - * @param {MultiLineString} multilinestring - * @return {String} A string of of linestring strings representing the multilinestring. - */ - multilinestring(multilinestring) { - const array = [] - for (let i = 0, len = multilinestring._geometries.length; i < len; ++i) - array.push('(' + - extract.linestring.call(this, multilinestring._geometries[i]) + - ')') - - return array.join(',') - }, - - /** - * Return a comma delimited string of linear ring arrays from a polygon. - * - * @param {Polygon} polygon - * @return {String} An array of linear ring arrays representing the polygon. - */ - polygon(polygon) { - const array = [] - array.push('(' + extract.linestring.call(this, polygon._shell) + ')') - for (let i = 0, len = polygon._holes.length; i < len; ++i) array.push('(' + extract.linestring.call(this, polygon._holes[i]) + ')') - - return array.join(',') - }, - - /** - * Return an array of polygon arrays from a multipolygon. - * - * @param {MultiPolygon} multipolygon - * @return {String} An array of polygon arrays representing the multipolygon. - */ - multipolygon(multipolygon) { - const array = [] - for (let i = 0, len = multipolygon._geometries.length; i < len; ++i) array.push('(' + extract.polygon.call(this, multipolygon._geometries[i]) + ')') - - return array.join(',') - }, - - /** - * Return the WKT portion between 'GEOMETRYCOLLECTION(' and ')' for an - * geometrycollection. - * - * @param {GeometryCollection} collection - * @return {String} internal WKT representation of the collection. - */ - geometrycollection(collection) { - const array = [] - for (let i = 0, len = collection._geometries.length; i < len; ++i) array.push(this.extractGeometry(collection._geometries[i])) - - return array.join(',') - } -} - -/** - * Object with properties corresponding to the geometry types. Property values - * are functions that do the actual parsing. - * @private - */ -const parse = { - - coord(str) { - const coords = str.trim().split(regExes.spaces) - const coord = new Coordinate(Number.parseFloat(coords[0]), Number.parseFloat(coords[1])) - this.precisionModel.makePrecise(coord) - return coord - }, - - /** - * Return point geometry given a point WKT fragment. - * - * @param {String} str A WKT fragment representing the point. - * @return {Point} A point geometry. - * @private - */ - point(str) { - if (str === undefined) return this.geometryFactory.createPoint() - return this.geometryFactory.createPoint(parse.coord.call(this, str)) - }, - - /** - * Return a multipoint geometry given a multipoint WKT fragment. - * - * @param {String} str A WKT fragment representing the multipoint. - * @return {Point} A multipoint feature. - * @private - */ - multipoint(str) { - if (str === undefined) return this.geometryFactory.createMultiPoint() - let point - const points = str.trim().split(',') - const components = [] - for (let i = 0, len = points.length; i < len; ++i) { - point = points[i].replace(regExes.trimParens, '$1') - components.push(parse.point.call(this, point)) - } - return this.geometryFactory.createMultiPoint(components) - }, - - /** - * Return a linestring geometry given a linestring WKT fragment. - * - * @param {String} str A WKT fragment representing the linestring. - * @return {LineString} A linestring geometry. - * @private - */ - linestring(str) { - if (str === undefined) return this.geometryFactory.createLineString() - - const points = str.trim().split(',') - const components = [] - let coords - for (let i = 0, len = points.length; i < len; ++i) components.push(parse.coord.call(this, points[i])) - - return this.geometryFactory.createLineString(components) - }, - - /** - * Return a linearring geometry given a linearring WKT fragment. - * - * @param {String} str A WKT fragment representing the linearring. - * @return {LinearRing} A linearring geometry. - * @private - */ - linearring(str) { - if (str === undefined) return this.geometryFactory.createLinearRing() - - const points = str.trim().split(',') - const components = [] - let coords - for (let i = 0, len = points.length; i < len; ++i) components.push(parse.coord.call(this, points[i])) - - return this.geometryFactory.createLinearRing(components) - }, - - /** - * Return a multilinestring geometry given a multilinestring WKT fragment. - * - * @param {String} str A WKT fragment representing the multilinestring. - * @return {MultiLineString} A multilinestring geometry. - * @private - */ - multilinestring(str) { - if (str === undefined) return this.geometryFactory.createMultiLineString() - - let line - const lines = str.trim().split(regExes.parenComma) - const components = [] - for (let i = 0, len = lines.length; i < len; ++i) { - line = lines[i].replace(regExes.trimParens, '$1') - components.push(parse.linestring.call(this, line)) - } - return this.geometryFactory.createMultiLineString(components) - }, - - /** - * Return a polygon geometry given a polygon WKT fragment. - * - * @param {String} str A WKT fragment representing the polygon. - * @return {Polygon} A polygon geometry. - * @private - */ - polygon(str) { - if (str === undefined) return this.geometryFactory.createPolygon() - - let ring, linestring, linearring - const rings = str.trim().split(regExes.parenComma) - let shell - const holes = [] - for (let i = 0, len = rings.length; i < len; ++i) { - ring = rings[i].replace(regExes.trimParens, '$1') - linestring = parse.linestring.call(this, ring) - linearring = this.geometryFactory.createLinearRing(linestring._points) - if (i === 0) - shell = linearring - else holes.push(linearring) - } - return this.geometryFactory.createPolygon(shell, holes) - }, - - /** - * Return a multipolygon geometry given a multipolygon WKT fragment. - * - * @param {String} str A WKT fragment representing the multipolygon. - * @return {MultiPolygon} A multipolygon geometry. - * @private - */ - multipolygon(str) { - if (str === undefined) return this.geometryFactory.createMultiPolygon() - - let polygon - const polygons = str.trim().split(regExes.doubleParenComma) - const components = [] - for (let i = 0, len = polygons.length; i < len; ++i) { - polygon = polygons[i].replace(regExes.trimParens, '$1') - components.push(parse.polygon.call(this, polygon)) - } - return this.geometryFactory.createMultiPolygon(components) - }, - - /** - * Return a geometrycollection given a geometrycollection WKT fragment. - * - * @param {String} str A WKT fragment representing the geometrycollection. - * @return {GeometryCollection} - * @private - */ - geometrycollection(str) { - if (str === undefined) return this.geometryFactory.createGeometryCollection() - - // separate components of the collection with | - str = str.replace(/,\s*([A-Za-z])/g, '|$1') - const wktArray = str.trim().split('|') - const components = [] - for (let i = 0, len = wktArray.length; i < len; ++i) components.push(this.read(wktArray[i])) - - return this.geometryFactory.createGeometryCollection(components) + return encode(geometry) } } diff --git a/test/auto/node/generate.js b/test/auto/node/generate.js index 6395a5cb..ba0954d5 100644 --- a/test/auto/node/generate.js +++ b/test/auto/node/generate.js @@ -6,6 +6,7 @@ import { expect } from 'chai' import GeometryFactory from 'org/locationtech/jts/geom/GeometryFactory' import PrecisionModel from 'org/locationtech/jts/geom/PrecisionModel' import WKTReader from 'org/locationtech/jts/io/WKTReader' +import WKTWriter from 'org/locationtech/jts/io/WKTWriter' import 'org/locationtech/jts/monkey' import BufferResultMatcher from '../BufferResultMatcher' @@ -13,7 +14,7 @@ import BufferResultMatcher from '../BufferResultMatcher' /** * @return GeometryFactory with PrecisionModel from test XML (undefined if no such info in XML) */ -function createGeometryFactory (precisionModelInfo) { +function createGeometryFactory(precisionModelInfo) { if (precisionModelInfo.length === 1) { const type = precisionModelInfo.attr('type') if (type !== 'FLOATING') { @@ -27,29 +28,35 @@ function createGeometryFactory (precisionModelInfo) { } } -function fail (r, e, i) { - throw new Error(`\nResult: ${r}\nExpected: ${e}\nInput: ${i}`) -} + /** * Translate JTS XML testcase document to Mocha suites */ -export default function (doc, title) { +export default function(doc, title) { const cases = $('case', doc) const geometryFactory = createGeometryFactory($('precisionModel', doc)) const reader = new WKTReader(geometryFactory) + const writer = new WKTWriter(geometryFactory) + + function fail(r, e, i) { + const rt = r ? writer.write(r) : 'undefined' + throw new Error(`\nResult: ${rt}\nExpected: ${e}\nInput: ${i}`) + } /** * Translate JTS XML "test" to a Jasmine test spec */ - const generateSpec = function (a, b, opname, arg2, arg3, expected) { + const generateSpec = function(a, b, opname, arg2, arg3, expected) { // fix opnames to real methods where needed if (opname === 'convexhull') opname = 'convexHull' else if (opname === 'getboundary') opname = 'getBoundary' else if (opname === 'symdifference') opname = 'symDifference' - it('Executing ' + opname + ' on test geometry', function () { - const inputs = ' Input geometry A: ' + a + (b ? ' B: ' + b : '') + it('Executing ' + opname + ' on test geometry', function() { + const at = writer.write(a) + const bt = b ? writer.write(b) : 'undefined' + const inputs = ' Input geometry A: ' + at + ' B: ' + bt var result @@ -99,7 +106,7 @@ export default function (doc, title) { result.normalize() expectedGeometry.normalize() if (!result.equalsExact(expectedGeometry)) { - fail(result, expected, inputs) + fail(result, writer.write(expectedGeometry), inputs) } else { expect(true).to.be.true } diff --git a/test/manual/io/WKTParser.js b/test/manual/io/WKTParser.js new file mode 100644 index 00000000..b341873c --- /dev/null +++ b/test/manual/io/WKTParser.js @@ -0,0 +1,23 @@ +import expect from 'expect.js' + +import WKTParser from 'org/locationtech/jts/io/WKTParser' + +const parser = new WKTParser() + +describe('WKTParser', function() { + function roundtrip(wkt) { + const expected = wkt + const g = parser.read(expected) + const actual = parser.write(g) + expect(expected).to.equal(actual) + } + + it('roundtrips', function() { + roundtrip('POINT EMPTY') + roundtrip('POINT (30 10)') + roundtrip('LINESTRING (30 10, 10 30, 40 40)') + roundtrip('POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))') + roundtrip('POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))') + roundtrip('MULTIPOINT ((10 40), (40 30), (20 20), (30 10))') + }) +}) diff --git a/test/manual/io/WKTReader.js b/test/manual/io/WKTReader.js index 030d3c7b..bece4811 100644 --- a/test/manual/io/WKTReader.js +++ b/test/manual/io/WKTReader.js @@ -1,11 +1,23 @@ -// import expect from 'expect.js' +import expect from 'expect.js' import WKTReader from 'org/locationtech/jts/io/WKTReader' const reader = new WKTReader() -describe('WKTReader', function () { - it('should be able to read a Polygon', function () { +describe('WKTReader', function() { + it('should be able to read a Polygon', function() { reader.read('POLYGON((57.722165171745836 14.202919006347656,57.71909404549173 14.21055793762207,57.71753546383143 14.212703704833984,57.71675614783365 14.212446212768555,57.715655908448745 14.212532043457031,57.71382210182487 14.21030044555664,57.71244668589343 14.20832633972168,57.71354702281898 14.205236434936523,57.712584229838065 14.202919006347656,57.71515162088769 14.201374053955078,57.71528915455559 14.196224212646484,57.71758130542645 14.192447662353516,57.72065256003978 14.196138381958008,57.72092758505232 14.199399948120117,57.72207350010876 14.201288223266602,57.722165171745836 14.202919006347656))') }) + + it('should be able to read a 3D Point', function() { + const p = reader.read('POINT Z (1 1 1)') + const c = p.getCoordinate() + expect(c.x).to.equal(1) + expect(c.y).to.equal(1) + expect(c.z).to.equal(1) + }) + + it('should be able to read a MultiPolygon', function() { + reader.read('MULTIPOLYGON (((20 0, 20 80, 100 80, 200 80, 200 0, 20 0)), ((100 80, 80 120, 120 120, 100 80)))') + }) }) diff --git a/test/manual/io/WKTWriter.js b/test/manual/io/WKTWriter.js index 2653159f..0b7a47cc 100644 --- a/test/manual/io/WKTWriter.js +++ b/test/manual/io/WKTWriter.js @@ -2,8 +2,8 @@ import expect from 'expect.js' import WKTWriter from 'org/locationtech/jts/io/WKTWriter' -describe('WKTWriter', function () { - it('should be able to create a LINESTRING from two points', function () { +describe('WKTWriter', function() { + it('should be able to create a LINESTRING from two points', function() { const p0 = { x: 10.0, y: 20.0 } const p1 = { x: 30.0, y: 40.0 } const p2 = { x: 10.123, y: 20.234 } diff --git a/test/manual/linearref/LengthIndexedLine.js b/test/manual/linearref/LengthIndexedLine.js index 96b2e9cf..157c9943 100644 --- a/test/manual/linearref/LengthIndexedLine.js +++ b/test/manual/linearref/LengthIndexedLine.js @@ -7,9 +7,9 @@ import LengthIndexedLine from 'org/locationtech/jts/linearref/LengthIndexedLine' const reader = new WKTReader() const writer = new WKTWriter() -describe('LengthIndexedLine', function () { - describe('indexOf', function () { - it('should be able to calc start and end indexOf', function () { +describe('LengthIndexedLine', function() { + describe('indexOf', function() { + it('should be able to calc start and end indexOf', function() { const linestring = reader.read('LINESTRING(1 1,2 2)') const lengthIndexedLine = new LengthIndexedLine(linestring) const indexOfStart = lengthIndexedLine.indexOf(new Coordinate(1, 1)) @@ -20,13 +20,13 @@ describe('LengthIndexedLine', function () { expect(indexOfEnd).to.eql(1.4142135623730951) }) - it('should be able to calc start and end indexOf', function () { + it('should be able to calc start and end indexOf', function() { const linestring = reader.read('LINESTRING(1 1,2 2)') const lengthIndexedLine = new LengthIndexedLine(linestring) const part1 = lengthIndexedLine.extractLine(0, 0.7071067811865476) const part2 = lengthIndexedLine.extractLine(0.7071067811865476, 1.4142135623730951) - expect(writer.write(part1)).to.eql('LINESTRING(1 1,1.5 1.5)') - expect(writer.write(part2)).to.eql('LINESTRING(1.5 1.5,2 2)') + expect(writer.write(part1)).to.eql('LINESTRING (1 1, 1.5 1.5)') + expect(writer.write(part2)).to.eql('LINESTRING (1.5 1.5, 2 2)') }) }) }) diff --git a/test/manual/operation/linemerge/LineMerger.js b/test/manual/operation/linemerge/LineMerger.js index 377987e9..dab54314 100644 --- a/test/manual/operation/linemerge/LineMerger.js +++ b/test/manual/operation/linemerge/LineMerger.js @@ -4,8 +4,8 @@ import WKTReader from 'org/locationtech/jts/io/WKTReader' import WKTWriter from 'org/locationtech/jts/io/WKTWriter' import LineMerger from 'org/locationtech/jts/operation/linemerge/LineMerger' -describe('LineMerger', function () { - it('#373', function () { +describe('LineMerger', function() { + it('#373', function() { const reader = new WKTReader() const ls1 = reader.read('LINESTRING(0 0, 1 1)') const ls2 = reader.read('LINESTRING(1 1, 2 2)') @@ -17,6 +17,6 @@ describe('LineMerger', function () { const mergedLineString = mergedLineStrings.get(0) const writer = new WKTWriter() const result = writer.write(mergedLineString) - expect(result).to.equal('LINESTRING(0 0,1 1,2 2)') + expect(result).to.equal('LINESTRING (0 0, 1 1, 2 2)') }) }) diff --git a/test/manual/operation/overlay/OverlayOp.js b/test/manual/operation/overlay/OverlayOp.js index 4f111605..961e523a 100644 --- a/test/manual/operation/overlay/OverlayOp.js +++ b/test/manual/operation/overlay/OverlayOp.js @@ -4,6 +4,9 @@ import Coordinate from 'org/locationtech/jts/geom/Coordinate' import GeometryFactory from 'org/locationtech/jts/geom/GeometryFactory' import OverlayOp from 'org/locationtech/jts/operation/overlay/OverlayOp' import RelateOp from 'org/locationtech/jts/operation/relate/RelateOp' +import PrecisionModel from 'org/locationtech/jts/geom/PrecisionModel' +import WKTReader from 'org/locationtech/jts/io/WKTReader' +import WKTWriter from 'org/locationtech/jts/io/WKTWriter' describe('OverlayOp', function () { it('intersection between GCs', function () { @@ -16,4 +19,15 @@ describe('OverlayOp', function () { expect(RelateOp.equalsTopo(intersection, p1)).to.be(true) }) + + it('specific case', function() { + const factory = new GeometryFactory(new PrecisionModel(1)) + const reader = new WKTReader(factory) + const writer = new WKTWriter(factory) + const a = reader.read('LINESTRING (240 190, 120 120)') + const b = reader.read('POLYGON ((110 240, 50 80, 240 70, 110 240))') + const r = OverlayOp.intersection(a, b) + const rt = writer.write(r) + expect('LINESTRING (177 153, 120 120)').to.equal(rt) + }) }) diff --git a/test/manual/triangulate/DelauneyTriangulationBuilder.js b/test/manual/triangulate/DelauneyTriangulationBuilder.js index 29eec816..e9b8ec42 100644 --- a/test/manual/triangulate/DelauneyTriangulationBuilder.js +++ b/test/manual/triangulate/DelauneyTriangulationBuilder.js @@ -4,8 +4,6 @@ import GeometryFactory from 'org/locationtech/jts/geom/GeometryFactory' import DelaunayTriangulationBuilder from 'org/locationtech/jts/triangulate/DelaunayTriangulationBuilder' import WKTReader from 'org/locationtech/jts/io/WKTReader' - - describe('DelauneyTriangulationBuilder', function () { var geomFact = new GeometryFactory() var reader = new WKTReader() @@ -16,26 +14,25 @@ describe('DelauneyTriangulationBuilder', function () { builder.setSites(sites) var result = null - if (computeTriangles) { + if (computeTriangles) result = builder.getTriangles(geomFact) - } else { + else result = builder.getEdges(geomFact) - } return result } - var runDelaunayEdges = function (sitesWKT) { + var runDelaunayEdges = function(sitesWKT) { return runDelaunay(sitesWKT, false) } - it('can be constructed', function () { + it('can be constructed', function() { var builder = new DelaunayTriangulationBuilder() expect(builder).to.be.an(DelaunayTriangulationBuilder) }) - it('can build from multipoints', function () { - var wkt = 'MULTIPOINT ((10 10 1), (10 20 2), (20 20 3))' + it('can build from multipoints', function() { + var wkt = 'MULTIPOINT Z ((10 10 1), (10 20 2), (20 20 3))' var expected = reader.read('MULTILINESTRING ((10 20, 20 20), (10 10, 10 20), (10 10, 20 20))') var result = runDelaunayEdges(wkt) expect(result.equalsExact(expected)).to.be.ok()