diff --git a/src/ol/format/MVT.js b/src/ol/format/MVT.js index dd64218eff..dbdf15cbf5 100644 --- a/src/ol/format/MVT.js +++ b/src/ol/format/MVT.js @@ -19,7 +19,7 @@ import RenderFeature from '../render/Feature.js'; import Units from '../proj/Units.js'; import {assert} from '../asserts.js'; import {get} from '../proj.js'; -import {linearRingIsClockwise} from '../geom/flat/orient.js'; +import {inflateEnds} from '../geom/flat/orient.js'; /** * @typedef {Object} Options @@ -185,8 +185,8 @@ class MVT extends FeatureFormat { values[this.layerName_] = rawFeature.layer.name; - const flatCoordinates = []; - const ends = []; + const flatCoordinates = /** @type {Array} */ ([]); + const ends = /** @type {Array} */ ([]); this.readRawGeometry_(pbf, rawFeature, flatCoordinates, ends); const geometryType = getGeometryType(type, ends.length); @@ -203,28 +203,11 @@ class MVT extends FeatureFormat { } else { let geom; if (geometryType == GeometryType.POLYGON) { - const endss = []; - let offset = 0; - let prevEndIndex = 0; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - // classifies an array of rings into polygons with outer rings and holes - if (!linearRingIsClockwise(flatCoordinates, offset, end, 2)) { - endss.push(ends.slice(prevEndIndex, i + 1)); - } else { - if (endss.length === 0) { - continue; - } - endss[endss.length - 1].push(ends[prevEndIndex]); - } - prevEndIndex = i + 1; - offset = end; - } - if (endss.length > 1) { - geom = new MultiPolygon(flatCoordinates, GeometryLayout.XY, endss); - } else { - geom = new Polygon(flatCoordinates, GeometryLayout.XY, ends); - } + const endss = inflateEnds(flatCoordinates, ends); + geom = + endss.length > 1 + ? new MultiPolygon(flatCoordinates, GeometryLayout.XY, endss) + : new Polygon(flatCoordinates, GeometryLayout.XY, ends); } else { geom = geometryType === GeometryType.POINT @@ -248,7 +231,9 @@ class MVT extends FeatureFormat { } const geometry = transformGeometryWithOptions(geom, false, options); feature.setGeometry(geometry); - feature.setId(id); + if (id !== undefined) { + feature.setId(id); + } feature.setProperties(values, true); } diff --git a/src/ol/format/OSMXML.js b/src/ol/format/OSMXML.js index 50bba52f56..c75a217426 100644 --- a/src/ol/format/OSMXML.js +++ b/src/ol/format/OSMXML.js @@ -96,7 +96,9 @@ class OSMXML extends XMLFeature { } transformGeometryWithOptions(geometry, false, options); const feature = new Feature(geometry); - feature.setId(values.id); + if (values.id !== undefined) { + feature.setId(values.id); + } feature.setProperties(values.tags, true); state.features.push(feature); } @@ -146,7 +148,9 @@ function readNode(node, objectStack) { const geometry = new Point(coordinates); transformGeometryWithOptions(geometry, false, options); const feature = new Feature(geometry); - feature.setId(id); + if (id !== undefined) { + feature.setId(id); + } feature.setProperties(values.tags, true); state.features.push(feature); } diff --git a/src/ol/geom/flat/orient.js b/src/ol/geom/flat/orient.js index 30d46c6139..3a87e0e408 100644 --- a/src/ol/geom/flat/orient.js +++ b/src/ol/geom/flat/orient.js @@ -178,3 +178,31 @@ export function orientLinearRingsArray( } return offset; } + +/** + * Return a two-dimensional endss + * @param {Array} flatCoordinates Flat coordinates + * @param {Array} ends Linear ring end indexes + * @return {Array>} Two dimensional endss array that can + * be used to contruct a MultiPolygon + */ +export function inflateEnds(flatCoordinates, ends) { + const endss = []; + let offset = 0; + let prevEndIndex = 0; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + // classifies an array of rings into polygons with outer rings and holes + if (!linearRingIsClockwise(flatCoordinates, offset, end, 2)) { + endss.push(ends.slice(prevEndIndex, i + 1)); + } else { + if (endss.length === 0) { + continue; + } + endss[endss.length - 1].push(ends[prevEndIndex]); + } + prevEndIndex = i + 1; + offset = end; + } + return endss; +} diff --git a/src/ol/render/Feature.js b/src/ol/render/Feature.js index f9e64c1454..69820c6916 100644 --- a/src/ol/render/Feature.js +++ b/src/ol/render/Feature.js @@ -1,7 +1,17 @@ /** * @module ol/render/Feature */ +import Feature from '../Feature.js'; +import GeometryLayout from '../geom/GeometryLayout.js'; import GeometryType from '../geom/GeometryType.js'; +import { + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +} from '../geom.js'; import { compose as composeTransform, create as createTransform, @@ -18,6 +28,7 @@ import { getInteriorPointsOfMultiArray, } from '../geom/flat/interiorpoint.js'; import {get as getProjection} from '../proj.js'; +import {inflateEnds} from '../geom/flat/orient.js'; import {interpolatePoint} from '../geom/flat/interpolate.js'; import {linearRingss as linearRingssCenter} from '../geom/flat/center.js'; import {transform2D} from '../geom/flat/transform.js'; @@ -326,4 +337,70 @@ RenderFeature.prototype.getEndss = RenderFeature.prototype.getEnds; RenderFeature.prototype.getFlatCoordinates = RenderFeature.prototype.getOrientedFlatCoordinates; +/** + * Create a geometry from an `ol/render/Feature` + * @param {RenderFeature} renderFeature + * Render Feature + * @return {Point|MultiPoint|LineString|MultiLineString|Polygon|MultiPolygon} + * New geometry instance. + * @api + */ +export function toGeometry(renderFeature) { + const geometryType = renderFeature.getType(); + switch (geometryType) { + case GeometryType.POINT: + return new Point(renderFeature.getFlatCoordinates()); + case GeometryType.MULTI_POINT: + return new MultiPoint( + renderFeature.getFlatCoordinates(), + GeometryLayout.XY + ); + case GeometryType.LINE_STRING: + return new LineString( + renderFeature.getFlatCoordinates(), + GeometryLayout.XY + ); + case GeometryType.MULTI_LINE_STRING: + return new MultiLineString( + renderFeature.getFlatCoordinates(), + GeometryLayout.XY, + /** @type {Array} */ (renderFeature.getEnds()) + ); + case GeometryType.POLYGON: + const flatCoordinates = renderFeature.getFlatCoordinates(); + const ends = /** @type {Array} */ (renderFeature.getEnds()); + const endss = inflateEnds(flatCoordinates, ends); + return endss.length > 1 + ? new MultiPolygon(flatCoordinates, GeometryLayout.XY, endss) + : new Polygon(flatCoordinates, GeometryLayout.XY, ends); + default: + throw new Error('Invalid geometry type:' + geometryType); + } +} + +/** + * Create an `ol/Feature` from an `ol/render/Feature` + * @param {RenderFeature} renderFeature RenderFeature + * @param {string} [opt_geometryName='geometry'] Geometry name to use + * when creating the Feature. + * @return {Feature} Newly constructed `ol/Feature` with properties, + * geometry, and id copied over. + * @api + */ +export function toFeature(renderFeature, opt_geometryName) { + const id = renderFeature.getId(); + const geometry = toGeometry(renderFeature); + const properties = renderFeature.getProperties(); + const feature = new Feature(); + if (opt_geometryName !== undefined) { + feature.setGeometryName(opt_geometryName); + } + feature.setGeometry(geometry); + if (id !== undefined) { + feature.setId(id); + } + feature.setProperties(properties, true); + return feature; +} + export default RenderFeature; diff --git a/test/node/ol/render/Feature.test.js b/test/node/ol/render/Feature.test.js new file mode 100644 index 0000000000..3c432e6215 --- /dev/null +++ b/test/node/ol/render/Feature.test.js @@ -0,0 +1,209 @@ +import GeometryType from '../../../../src/ol/geom/GeometryType.js'; +import RenderFeature, { + toFeature, + toGeometry, +} from '../../../../src/ol/render/Feature.js'; +import expect from '../../expect.js'; +import { + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +} from '../../../../src/ol/geom.js'; + +describe('ol/render/Feature', function () { + describe('ol/render/Feature.toGeometry()', function () { + it('creates a Point', function () { + const geometry = new Point([0, 0]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + [] + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(Point); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getProperties()).to.eql({}); + }); + it('creates a MultiPoint', function () { + const geometry = new MultiPoint([ + [0, 0], + [4, 5], + ]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + [] + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(MultiPoint); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getProperties()).to.eql({}); + }); + it('creates a LineString', function () { + const geometry = new LineString([ + [0, 0], + [4, 5], + ]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + [] + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(LineString); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getProperties()).to.eql({}); + }); + it('creates a MultiLineString', function () { + const geometry = new MultiLineString([ + [ + [0, 0], + [4, 5], + ], + [ + [0, 0], + [4, 5], + ], + ]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + geometry.getEnds().slice() + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(MultiLineString); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getEnds()).to.eql(geometry.getEnds()); + expect(converted.getProperties()).to.eql({}); + }); + it('creates a Polygon', function () { + const geometry = new Polygon([ + [ + [0, 0], + [5, 0], + [5, 5], + [0, 0], + ], + [ + [1, 1], + [4, 4], + [4, 1], + [1, 1], + ], + ]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + geometry.getEnds().slice() + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(Polygon); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getEnds()).to.eql(geometry.getEnds()); + expect(converted.getProperties()).to.eql({}); + }); + it('creates a MultiPolygon from oriented polygon rings', function () { + const geometry = new MultiPolygon([ + [ + [ + [0, 0], + [5, 0], + [5, 5], + [0, 0], + ], + [ + [1, 1], + [4, 4], + [4, 1], + [1, 1], + ], + ], + [ + [ + [-0, -0], + [-5, -0], + [-5, -5], + [-0, -0], + ], + ], + ]); + const renderFeature = new RenderFeature( + GeometryType.POLYGON, + geometry.getFlatCoordinates().slice(), + geometry.getEndss().flat(1) + ); + const converted = toGeometry(renderFeature); + expect(converted).to.be.a(MultiPolygon); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(converted.getEndss()).to.eql(geometry.getEndss()); + expect(converted.getProperties()).to.eql({}); + }); + }); + + describe('ol/render/Feature.toFeature()', function () { + it('creates a Feature', function () { + const id = 'asdf'; + const properties = {test: '123'}; + const geometry = new Point([0, 0]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + [], + properties, + id + ); + const feature = toFeature(renderFeature); + const converted = feature.getGeometry(); + expect(converted).to.be.a(Point); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(feature.getId()).to.be(id); + const props = feature.getProperties(); + delete props.geometry; + expect(props).to.eql(properties); + }); + }); + it('creates a Feature with non-default geometry name', function () { + const id = 'asdf'; + const properties = {geometry: '123'}; + const geometry = new LineString([ + [0, 0], + [5, 5], + ]); + const renderFeature = new RenderFeature( + geometry.getType(), + geometry.getFlatCoordinates().slice(), + [], + properties, + id + ); + const geometryName = 'geom'; + const feature = toFeature(renderFeature, geometryName); + const converted = feature.getGeometry(); + expect(converted).to.be.a(LineString); + expect(feature.get(geometryName)).to.be(converted); + expect(converted.getFlatCoordinates()).to.eql( + geometry.getFlatCoordinates() + ); + expect(feature.getId()).to.be(id); + const props = feature.getProperties(); + delete props.geom; + expect(props).to.eql(properties); + }); +});