diff --git a/rendering/cases/format-mvt-geojson/expected.png b/rendering/cases/format-mvt-geojson/expected.png new file mode 100644 index 0000000000..432f88face Binary files /dev/null and b/rendering/cases/format-mvt-geojson/expected.png differ diff --git a/rendering/cases/format-mvt-geojson/main.js b/rendering/cases/format-mvt-geojson/main.js new file mode 100644 index 0000000000..711214dff5 --- /dev/null +++ b/rendering/cases/format-mvt-geojson/main.js @@ -0,0 +1,47 @@ +import {Feature, Map, View} from '../../../src/ol/index.js'; +import {GeoJSON, MVT} from '../../../src/ol/format.js'; +import {VectorTile as VectorTileLayer} from '../../../src/ol/layer.js'; +import {VectorTile as VectorTileSource} from '../../../src/ol/source.js'; +import {fromLonLat} from '../../../src/ol/proj.js'; + +const center = fromLonLat([0.26, 24.08]); + +const map = new Map({ + layers: [ + new VectorTileLayer({ + source: new VectorTileSource({ + format: new MVT(), + url: '/data/{z}-{x}-{y}.mvt', + minZoom: 7, + maxZoom: 7, + }), + }), + new VectorTileLayer({ + source: new VectorTileSource({ + format: new MVT({ + featureClass: Feature, + }), + url: '/data/{z}-{x}-{y}.mvt', + minZoom: 7, + maxZoom: 7, + }), + }), + new VectorTileLayer({ + source: new VectorTileSource({ + format: new GeoJSON(), + url: '/data/{z}-{x}-{y}.geojson', + minZoom: 7, + maxZoom: 7, + }), + }), + ], + target: 'map', + view: new View({ + center: center, + zoom: 10, + }), +}); + +map.getTargetElement().style.background = 'gray'; + +render(); diff --git a/rendering/data/7-64-55.geojson b/rendering/data/7-64-55.geojson new file mode 100644 index 0000000000..a52212c2cb --- /dev/null +++ b/rendering/data/7-64-55.geojson @@ -0,0 +1,11 @@ +{ +"type": "FeatureCollection", +"name": "7-64-55", +"features": [ +{ "type": "Feature", "properties": { "mvt_id": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.11810302734375, 24.046463999666589 ], [ 0.237579345703125, 24.046463999666589 ], [ 0.237579345703125, 23.956136333969283 ], [ 0.11810302734375, 23.956136333969283 ], [ 0.11810302734375, 24.046463999666589 ] ], [ [ 0.153121948242188, 24.01949779624486 ], [ 0.153121948242188, 23.979978958263413 ], [ 0.2032470703125, 23.979978958263413 ], [ 0.2032470703125, 24.01949779624486 ], [ 0.153121948242188, 24.01949779624486 ] ] ] ] } }, +{ "type": "Feature", "properties": { "mvt_id": 26 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.293197631835938, 24.036430724667376 ], [ 0.389328002929688, 24.036430724667376 ], [ 0.3570556640625, 23.95864629158493 ], [ 0.260238647460938, 23.95864629158493 ], [ 0.293197631835938, 24.036430724667376 ] ], [ [ 0.342636108398438, 24.0332951655089 ], [ 0.32684326171875, 23.988761970899695 ], [ 0.352935791015625, 23.988761970899695 ], [ 0.369415283203125, 24.0332951655089 ], [ 0.342636108398438, 24.0332951655089 ] ], [ [ 0.291824340820312, 24.018870607907278 ], [ 0.291824340820312, 23.971195346707443 ], [ 0.319290161132813, 23.971195346707443 ], [ 0.319290161132813, 24.018870607907278 ], [ 0.291824340820312, 24.018870607907278 ] ] ] ] } }, +{ "type": "Feature", "properties": { "mvt_id": 30 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.287704467773438, 24.219414393426444 ], [ 0.350875854492188, 24.219414393426444 ], [ 0.33233642578125, 24.147380157655896 ], [ 0.268478393554688, 24.147380157655896 ], [ 0.287704467773438, 24.219414393426444 ] ] ], [ [ [ 0.3460693359375, 24.166802085303235 ], [ 0.372848510742188, 24.166802085303235 ], [ 0.383148193359375, 24.144873887414654 ], [ 0.355682373046875, 24.144873887414654 ], [ 0.3460693359375, 24.166802085303235 ] ] ], [ [ [ 0.352249145507812, 24.218161971731128 ], [ 0.377655029296875, 24.218161971731128 ], [ 0.383834838867187, 24.186847428521244 ], [ 0.358428955078125, 24.186847428521244 ], [ 0.352249145507812, 24.218161971731128 ] ] ] ] } }, +{ "type": "Feature", "properties": { "mvt_id": 33 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.10986328125, 24.168055011483165 ], [ 0.199813842773438, 24.168055011483165 ], [ 0.199813842773438, 24.075305297879073 ], [ 0.10986328125, 24.075305297879073 ], [ 0.10986328125, 24.168055011483165 ] ] ], [ [ [ 0.202560424804687, 24.16742854993004 ], [ 0.240325927734375, 24.16742854993004 ], [ 0.240325927734375, 24.128581933124689 ], [ 0.202560424804687, 24.128581933124689 ], [ 0.202560424804687, 24.16742854993004 ] ] ] ] } }, +{ "type": "Feature", "properties": { "mvt_id": 48 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.268478393554688, 24.136101554583817 ], [ 0.336456298828125, 24.136101554583817 ], [ 0.31585693359375, 24.047718103928766 ], [ 0.247879028320312, 24.047718103928766 ], [ 0.268478393554688, 24.136101554583817 ] ], [ [ 0.27191162109375, 24.123568606548453 ], [ 0.27191162109375, 24.07279761626851 ], [ 0.31585693359375, 24.07279761626851 ], [ 0.31585693359375, 24.123568606548453 ], [ 0.27191162109375, 24.123568606548453 ] ] ], [ [ [ 0.328216552734375, 24.077812930451806 ], [ 0.357742309570313, 24.077812930451806 ], [ 0.3680419921875, 24.05085331099432 ], [ 0.339202880859375, 24.05085331099432 ], [ 0.328216552734375, 24.077812930451806 ] ] ] ] } } +] +} diff --git a/rendering/data/7-64-55.mvt b/rendering/data/7-64-55.mvt new file mode 100644 index 0000000000..f399c4c6bf Binary files /dev/null and b/rendering/data/7-64-55.mvt differ diff --git a/src/ol/format/MVT.js b/src/ol/format/MVT.js index 69350830d4..08a77e5f97 100644 --- a/src/ol/format/MVT.js +++ b/src/ol/format/MVT.js @@ -203,10 +203,16 @@ class MVT extends FeatureFormat { 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)); - prevEndIndex = i; + 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) { diff --git a/src/ol/geom/flat/orient.js b/src/ol/geom/flat/orient.js index 9fbe8fd26a..2a7d36cb08 100644 --- a/src/ol/geom/flat/orient.js +++ b/src/ol/geom/flat/orient.js @@ -4,6 +4,9 @@ import {coordinates as reverseCoordinates} from './reverse.js'; /** + * Is the linear ring oriented clockwise in a coordinate system with a bottom-left + * coordinate origin? For a coordinate system with a top-left coordinate origin, + * the ring's orientation is clockwise when this function returns false. * @param {Array} flatCoordinates Flat coordinates. * @param {number} offset Offset. * @param {number} end End. @@ -11,19 +14,69 @@ import {coordinates as reverseCoordinates} from './reverse.js'; * @return {boolean} Is clockwise. */ export function linearRingIsClockwise(flatCoordinates, offset, end, stride) { - // http://tinyurl.com/clockwise-method - // https://github.com/OSGeo/gdal/blob/trunk/gdal/ogr/ogrlinearring.cpp - let edge = 0; - let x1 = flatCoordinates[end - stride]; - let y1 = flatCoordinates[end - stride + 1]; - for (; offset < end; offset += stride) { - const x2 = flatCoordinates[offset]; - const y2 = flatCoordinates[offset + 1]; - edge += (x2 - x1) * (y2 + y1); - x1 = x2; - y1 = y2; + // https://stackoverflow.com/a/1180256/2389327 + // https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon + + let firstVertexRepeated = true; + for (let i = 0; i < stride; ++i) { + if (flatCoordinates[offset + i] !== flatCoordinates[end - stride + i]) { + firstVertexRepeated = false; + break; + } } - return edge > 0; + if (firstVertexRepeated) { + end -= stride; + } + const iMinVertex = findCornerVertex(flatCoordinates, offset, end, stride); + // Orientation matrix: + // [ 1 xa ya ] + // O = | 1 xb yb | + // [ 1 xc yc ] + let iPreviousVertex = iMinVertex - stride; + if (iPreviousVertex < offset) { + iPreviousVertex = end - stride; + } + let iNextVertex = iMinVertex + stride; + if (iNextVertex >= end) { + iNextVertex = offset; + } + const aX = flatCoordinates[iPreviousVertex]; + const aY = flatCoordinates[iPreviousVertex + 1]; + const bX = flatCoordinates[iMinVertex]; + const bY = flatCoordinates[iMinVertex + 1]; + const cX = flatCoordinates[iNextVertex]; + const cY = flatCoordinates[iNextVertex + 1]; + const determinant = + bX * cY + aX * bY + aY * cX - (aY * bX + bY * cX + aX * cY); + + return determinant < 0; +} + +// Find vertex along one edge of bounding box. +// In this case, we find smallest y; in case of tie also smallest x. +function findCornerVertex(flatCoordinates, offset, end, stride) { + let iMinVertex = -1; + let minY = Infinity; + let minXAtMinY = Infinity; + for (let i = offset; i < end; i += stride) { + const x = flatCoordinates[i]; + const y = flatCoordinates[i + 1]; + if (y > minY) { + continue; + } + if (y == minY) { + if (x >= minXAtMinY) { + continue; + } + } + + // Minimum so far. + iMinVertex = i; + minY = y; + minXAtMinY = x; + } + + return iMinVertex; } /** diff --git a/test/spec/ol/format/mvt.test.js b/test/spec/ol/format/mvt.test.js index 05b4644d69..660ed1b579 100644 --- a/test/spec/ol/format/mvt.test.js +++ b/test/spec/ol/format/mvt.test.js @@ -181,7 +181,7 @@ describe('ol.format.MVT', function () { flatCoordinates, ends ) { - flatCoordinates.push(0, 0, 3, 0, 3, 3, 3, 0, 0, 0); + flatCoordinates.push(0, 0, 3, 0, 3, 3, 0, 3, 0, 0); flatCoordinates.push(1, 1, 1, 2, 2, 2, 2, 1, 1, 1); ends.push(10, 20); }; @@ -207,8 +207,8 @@ describe('ol.format.MVT', function () { flatCoordinates, ends ) { - flatCoordinates.push(0, 0, 1, 0, 1, 1, 1, 0, 0, 0); - flatCoordinates.push(1, 1, 2, 1, 2, 2, 2, 1, 1, 1); + flatCoordinates.push(0, 0, 1, 0, 1, 1, 0, 1, 0, 0); + flatCoordinates.push(1, 1, 2, 1, 2, 2, 1, 2, 1, 1); ends.push(10, 20); }; const feature = format.createFeature_({}, rawFeature);