UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

154 lines (136 loc) • 7.96 kB
import {describe, expect, test} from 'vitest'; import Point from '@mapbox/point-geometry'; import {reprojectGeoJSONFromWorld, reprojectGeoJSONSourceDiffToWorld, reprojectGeoJSONToWorld} from './geojson_reproject.ts'; import {CRSWorldCoordinateHelper, identityCrs} from '../geo/projection/crs.ts'; import {polygonIntersectsBox, polygonIntersectsBufferedMultiLine, polygonIntersectsPolygon} from './intersection_tests.ts'; import {createPolarStereographicCrs} from './test/util.ts'; import type {GeoJSONSourceDiff} from '../source/geojson_source_diff.ts'; function createIdentityHelper() { return new CRSWorldCoordinateHelper(identityCrs); } function createPolarHelper() { return new CRSWorldCoordinateHelper(createPolarStereographicCrs()); } describe('reprojectGeoJSONToWorld', () => { test('moves an identity-CRS position to its world coordinates', () => { const point: GeoJSON.Point = {type: 'Point', coordinates: [45, 45]}; expect(reprojectGeoJSONToWorld(point, createIdentityHelper()).coordinates).toEqual([0.75, 0.25]); }); test('keeps the altitude and any further ordinates', () => { const point: GeoJSON.Point = {type: 'Point', coordinates: [0, 0, 123, 4]}; expect(reprojectGeoJSONToWorld(point, createIdentityHelper()).coordinates).toEqual([0.5, 0.5, 123, 4]); }); test('walks features, collections, nested geometries and geometry collections without mutating the input', () => { const data: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: [{ type: 'Feature', id: 'a', properties: {name: 'a'}, geometry: {type: 'Polygon', coordinates: [[[0, 0], [45, 0], [45, 45], [0, 45], [0, 0]]]} }, { type: 'Feature', properties: null, geometry: { type: 'GeometryCollection', geometries: [ {type: 'MultiLineString', coordinates: [[[0, 0], [45, 45]]]}, {type: 'Point', coordinates: [45, 0]} ] } }, { type: 'Feature', properties: {}, geometry: null }] }; const copy = JSON.parse(JSON.stringify(data)); const result = reprojectGeoJSONToWorld(data, createIdentityHelper()); expect(data).toEqual(copy); expect(result).not.toBe(data); expect(result.features[0].id).toBe('a'); expect(result.features[0].properties).toBe(data.features[0].properties); expect(result.features[2].geometry).toBeNull(); const polygon = result.features[0].geometry as GeoJSON.Polygon; expect(polygon.coordinates[0][2]).toEqual([0.75, 0.25]); const collection = result.features[1].geometry as GeoJSON.GeometryCollection; expect((collection.geometries[1] as GeoJSON.Point).coordinates).toEqual([0.75, 0.5]); }); }); describe('reprojectGeoJSONToWorld in a polar CRS, whose latitude range ends at 53 S', () => { test('keeps a polygon with an edge across the far pole out of the world square', () => { const isea4hSouthPoleCell: GeoJSON.Polygon = {type: 'Polygon', coordinates: [[ [88.3853, -87.67805], [54.96953, -88.57819], [0, -89.52868], [180, -89.31865], [142.1755, -88.10424], [115.34565, -87.43262], [88.3853, -87.67805] ]]}; const ring = reprojectGeoJSONToWorld(isea4hSouthPoleCell, createPolarHelper()).coordinates[0]; expect(polygonIntersectsBox(ring.map(([x, y]) => new Point(x, y)), 0, 0, 1, 1)).toBe(false); }); test('keeps a line along a parallel past the southern limit out of the world square', () => { const farParallel: GeoJSON.LineString = {type: 'LineString', coordinates: [[0, -80], [180, -80]]}; const line = reprojectGeoJSONToWorld(farParallel, createPolarHelper()).coordinates.map(([x, y]) => new Point(x, y)); const worldSquare = [new Point(0, 0), new Point(1, 0), new Point(1, 1), new Point(0, 1), new Point(0, 0)]; expect(polygonIntersectsBufferedMultiLine(worldSquare, [line], 0)).toBe(false); }); test('fills an edge along the southern limit, so a polygon of the whole globe covers the world square', () => { const wholeGlobe: GeoJSON.Polygon = {type: 'Polygon', coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]]}; const ring = reprojectGeoJSONToWorld(wholeGlobe, createPolarHelper()).coordinates[0]; const worldRing = ring.map(([x, y]) => new Point(x, y)); const insideWorldSquare = new Point(0.75, 0.75); expect(polygonIntersectsPolygon([insideWorldSquare], worldRing)).toBe(true); }); }); describe('reprojectGeoJSONToWorld in a polar CRS on a 1 km grid at 5 W, 70 N', () => { function createSmallGridHelper() { const polar = createPolarStereographicCrs(); const [x, y] = polar.projection.forward([-5, 70]); return new CRSWorldCoordinateHelper({...polar, tileMatrixSet: {origin: [x - 500, y + 500], extentAtZoom0: 1000}}); } test('keeps a polygon that covers the grid over all of it', () => { const rectangle: GeoJSON.Polygon = {type: 'Polygon', coordinates: [[[-20, 60], [10, 60], [10, 80], [-20, 80], [-20, 60]]]}; const ring = reprojectGeoJSONToWorld(rectangle, createSmallGridHelper()).coordinates[0].map(([x, y]) => new Point(x, y)); for (const [x, y] of [[0.5, 0.5], [0.01, 0.01], [0.99, 0.01], [0.99, 0.99], [0.01, 0.99]]) { expect(polygonIntersectsPolygon([new Point(x, y)], ring)).toBe(true); } }); }); describe('reprojectGeoJSONFromWorld', () => { test('round trips with reprojectGeoJSONToWorld, 3D coordinates and geometry collections included', () => { const feature: GeoJSON.Feature = { type: 'Feature', properties: {}, geometry: { type: 'GeometryCollection', geometries: [ {type: 'Point', coordinates: [-30, -30, 500]}, {type: 'MultiPolygon', coordinates: [[[[0, 0, 1], [45, 0, 2], [45, 45, 3], [0, 0, 1]]]]} ] } }; const roundTripped = reprojectGeoJSONFromWorld(reprojectGeoJSONToWorld(feature, createIdentityHelper()), createIdentityHelper()); const geometries = (roundTripped.geometry as GeoJSON.GeometryCollection).geometries; expect((geometries[0] as GeoJSON.Point).coordinates).toEqual([expect.closeTo(-30, 9), expect.closeTo(-30, 9), 500]); expect((geometries[1] as GeoJSON.MultiPolygon).coordinates).toEqual([[[ [expect.closeTo(0, 9), expect.closeTo(0, 9), 1], [expect.closeTo(45, 9), expect.closeTo(0, 9), 2], [expect.closeTo(45, 9), expect.closeTo(45, 9), 3], [expect.closeTo(0, 9), expect.closeTo(0, 9), 1] ]]]); }); }); describe('reprojectGeoJSONSourceDiffToWorld', () => { test('moves the added features and the replaced geometries, and keeps removals and property changes', () => { const diff = { remove: ['gone'], add: [{type: 'Feature', id: 'new', properties: {}, geometry: {type: 'Point', coordinates: [45, 45]}}], update: [ {id: 'moved', newGeometry: {type: 'Point', coordinates: [45, 45]}}, {id: 'renamed', addOrUpdateProperties: [{key: 'name', value: 'b'}]} ] } satisfies GeoJSONSourceDiff; const result = reprojectGeoJSONSourceDiffToWorld(diff, createIdentityHelper()); expect(result.remove).toEqual(['gone']); expect((result.add[0].geometry as GeoJSON.Point).coordinates).toEqual([0.75, 0.25]); expect((result.update[0].newGeometry as GeoJSON.Point).coordinates).toEqual([0.75, 0.25]); expect(result.update[1]).toBe(diff.update[1]); }); });