diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b52f07d6..a9c545637b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## main -- Update Popup methods `addClass` & `removeClass` to return instance of Popup ([#3975](https://github.com/maplibre/maplibre-gl-js/pull/3975)) ### ✨ Features and improvements -- _...Add new stuff here..._ + +- Update `Popup`'s methods `addClass` and `removeClass` to return an instance of Popup ([#3975](https://github.com/maplibre/maplibre-gl-js/pull/3975)) - New map option to decide whether to cancel previous pending tiles while zooming in ([#4051](https://github.com/maplibre/maplibre-gl-js/pull/4051)) - Sprites include optional textFitHeight and textFitWidth values ([#4019](https://github.com/maplibre/maplibre-gl-js/pull/4019)) +- Add support for `distance` expression ([#4076](https://github.com/maplibre/maplibre-gl-js/pull/4076)) +- _...Add new stuff here..._ ### 🐞 Bug fixes - _...Add new stuff here..._ diff --git a/package-lock.json b/package-lock.json index 12fa5d9ccb..42e4e4bc51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.1.1", + "@maplibre/maplibre-gl-style-spec": "^20.2.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/junit-report-builder": "^3.0.2", @@ -1573,16 +1573,18 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.1.1.tgz", - "integrity": "sha512-z85ARNPCBI2Cs5cPOS3DSbraTN+ue8zrcYVoSWBuNrD/mA+2SKAJ+hIzI22uN7gac6jBMnCdpPKRxS/V0KSZVQ==", + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.2.0.tgz", + "integrity": "sha512-BTw6/3ysowky22QMtNDjElp+YLwwvBDh3xxnq1izDFjTtUERm5nYSihlNZ6QaxXb+6lX2T2t0hBEjheAI+kBEQ==", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", + "quickselect": "^2.0.0", "rw": "^1.3.3", - "sort-object": "^3.0.3" + "sort-object": "^3.0.3", + "tinyqueue": "^2.0.3" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", diff --git a/package.json b/package.json index 4126c090bd..2b28e2f710 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.1.1", + "@maplibre/maplibre-gl-style-spec": "^20.2.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/junit-report-builder": "^3.0.2", diff --git a/src/data/bucket/fill_bucket.ts b/src/data/bucket/fill_bucket.ts index 2fc8711649..7b5083edda 100644 --- a/src/data/bucket/fill_bucket.ts +++ b/src/data/bucket/fill_bucket.ts @@ -5,7 +5,7 @@ import {SegmentVector} from '../segment'; import {ProgramConfigurationSet} from '../program_configuration'; import {LineIndexArray, TriangleIndexArray} from '../index_array_type'; import earcut from 'earcut'; -import {classifyRings} from '../../util/classify_rings'; +import {classifyRings} from '@maplibre/maplibre-gl-style-spec'; const EARCUT_MAX_RINGS = 500; import {register} from '../../util/web_worker_transfer'; import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; diff --git a/src/data/bucket/fill_extrusion_bucket.ts b/src/data/bucket/fill_extrusion_bucket.ts index 9dc1a1b291..9c318e3737 100644 --- a/src/data/bucket/fill_extrusion_bucket.ts +++ b/src/data/bucket/fill_extrusion_bucket.ts @@ -8,7 +8,7 @@ import {EXTENT} from '../extent'; import earcut from 'earcut'; import mvt from '@mapbox/vector-tile'; const vectorTileFeatureTypes = mvt.VectorTileFeature.types; -import {classifyRings} from '../../util/classify_rings'; +import {classifyRings} from '@maplibre/maplibre-gl-style-spec'; const EARCUT_MAX_RINGS = 500; import {register} from '../../util/web_worker_transfer'; import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; diff --git a/src/symbol/symbol_layout.ts b/src/symbol/symbol_layout.ts index bd37eb423c..c994642ef5 100644 --- a/src/symbol/symbol_layout.ts +++ b/src/symbol/symbol_layout.ts @@ -11,7 +11,6 @@ import { allowsLetterSpacing } from '../util/script_detection'; import {findPoleOfInaccessibility} from '../util/find_pole_of_inaccessibility'; -import {classifyRings} from '../util/classify_rings'; import {EXTENT} from '../data/extent'; import {SymbolBucket} from '../data/bucket/symbol_bucket'; import {EvaluationParameters} from '../style/evaluation_parameters'; @@ -31,7 +30,7 @@ import type {PossiblyEvaluatedPropertyValue} from '../style/properties'; import Point from '@mapbox/point-geometry'; import murmur3 from 'murmurhash-js'; import {getIconPadding, SymbolPadding} from '../style/style_layer/symbol_style_layer'; -import {VariableAnchorOffsetCollection} from '@maplibre/maplibre-gl-style-spec'; +import {VariableAnchorOffsetCollection, classifyRings} from '@maplibre/maplibre-gl-style-spec'; import {getTextVariableAnchorOffset, evaluateVariableOffset, INVALID_TEXT_OFFSET, TextAnchor, TextAnchorEnum} from '../style/style_layer/variable_text_anchor'; // The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and diff --git a/src/util/classify_rings.test.ts b/src/util/classify_rings.test.ts deleted file mode 100644 index 83294bff5a..0000000000 --- a/src/util/classify_rings.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import Protobuf from 'pbf'; -import {VectorTile} from '@mapbox/vector-tile'; -import {classifyRings} from './classify_rings'; -import Point from '@mapbox/point-geometry'; - -// Load a fill feature from fixture tile. -const vt = new VectorTile(new Protobuf(fs.readFileSync(path.resolve(__dirname, '../../test/unit/assets/mbsv5-6-18-23.vector.pbf')))); -const feature = vt.layers.water.feature(0); - -describe('classifyRings', () => { - test('classified.length', () => { - let geometry; - let classified; - - geometry = [ - [ - {x: 0, y: 0}, - {x: 0, y: 40}, - {x: 40, y: 40}, - {x: 40, y: 0}, - {x: 0, y: 0} - ] - ]; - classified = classifyRings(geometry, undefined); - expect(classified).toHaveLength(1); - expect(classified[0]).toHaveLength(1); - - geometry = [ - [ - {x: 0, y: 0}, - {x: 0, y: 40}, - {x: 40, y: 40}, - {x: 40, y: 0}, - {x: 0, y: 0} - ], - [ - {x: 60, y: 0}, - {x: 60, y: 40}, - {x: 100, y: 40}, - {x: 100, y: 0}, - {x: 60, y: 0} - ] - ]; - classified = classifyRings(geometry, undefined); - expect(classified).toHaveLength(2); - expect(classified[0]).toHaveLength(1); - expect(classified[1]).toHaveLength(1); - - geometry = [ - [ - {x: 0, y: 0}, - {x: 0, y: 40}, - {x: 40, y: 40}, - {x: 40, y: 0}, - {x: 0, y: 0} - ], - [ - {x: 10, y: 10}, - {x: 20, y: 10}, - {x: 20, y: 20}, - {x: 10, y: 10} - ] - ]; - classified = classifyRings(geometry, undefined); - expect(classified).toHaveLength(1); - expect(classified[0]).toHaveLength(2); - - geometry = feature.loadGeometry(); - classified = classifyRings(geometry, undefined); - expect(classified).toHaveLength(2); - expect(classified[0]).toHaveLength(1); - expect(classified[1]).toHaveLength(10); - }); -}); - -describe('classifyRings + maxRings', () => { - - function createGeometry(options?) { - const geometry = [ - // Outer ring, area = 3200 - [{x: 0, y: 0}, {x: 0, y: 40}, {x: 40, y: 40}, {x: 40, y: 0}, {x: 0, y: 0}], - // Inner ring, area = 100 - [{x: 30, y: 30}, {x: 32, y: 30}, {x: 32, y: 32}, {x: 30, y: 30}], - // Inner ring, area = 4 - [{x: 10, y: 10}, {x: 20, y: 10}, {x: 20, y: 20}, {x: 10, y: 10}] - ] as Point[][]; - if (options && options.reverse) { - geometry[0].reverse(); - geometry[1].reverse(); - geometry[2].reverse(); - } - return geometry; - } - - test('maxRings=undefined', () => { - const geometry = sortRings(classifyRings(createGeometry(), undefined)); - expect(geometry).toHaveLength(1); - expect(geometry[0]).toHaveLength(3); - expect(geometry[0][0].area).toBe(3200); - expect(geometry[0][1].area).toBe(100); - expect(geometry[0][2].area).toBe(4); - - }); - - test('maxRings=2', () => { - const geometry = sortRings(classifyRings(createGeometry(), 2)); - expect(geometry).toHaveLength(1); - expect(geometry[0]).toHaveLength(2); - expect(geometry[0][0].area).toBe(3200); - expect(geometry[0][1].area).toBe(100); - - }); - - test('maxRings=2, reversed geometry', () => { - const geometry = sortRings(classifyRings(createGeometry({reverse: true}), 2)); - expect(geometry).toHaveLength(1); - expect(geometry[0]).toHaveLength(2); - expect(geometry[0][0].area).toBe(3200); - expect(geometry[0][1].area).toBe(100); - - }); - - test('maxRings=5, geometry from fixture', () => { - const geometry = sortRings(classifyRings(feature.loadGeometry(), 5)); - expect(geometry).toHaveLength(2); - expect(geometry[0]).toHaveLength(1); - expect(geometry[1]).toHaveLength(5); - - const areas = geometry[1].map((ring) => { return ring.area; }); - expect(areas).toEqual([2763951, 21600, 8298, 4758, 3411]); - - }); - -}); - -function sortRings(geometry) { - for (let i = 0; i < geometry.length; i++) { - geometry[i] = geometry[i].sort(compareAreas); - } - return geometry; -} - -function compareAreas(a, b) { - return b.area - a.area; -} diff --git a/src/util/classify_rings.ts b/src/util/classify_rings.ts deleted file mode 100644 index f8fe2621dd..0000000000 --- a/src/util/classify_rings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import quickselect from 'quickselect'; - -import {calculateSignedArea} from './util'; - -import type Point from '@mapbox/point-geometry'; - -// classifies an array of rings into polygons with outer rings and holes -export function classifyRings(rings: Array>, maxRings: number) { - const len = rings.length; - - if (len <= 1) return [rings]; - - const polygons = []; - let polygon, - ccw; - - for (let i = 0; i < len; i++) { - const area = calculateSignedArea(rings[i]); - if (area === 0) continue; - - (rings[i] as any).area = Math.abs(area); - - if (ccw === undefined) ccw = area < 0; - - if (ccw === area < 0) { - if (polygon) polygons.push(polygon); - polygon = [rings[i]]; - - } else { - (polygon as any).push(rings[i]); - } - } - if (polygon) polygons.push(polygon); - - // Earcut performance degrades with the # of rings in a polygon. For this - // reason, we limit strip out all but the `maxRings` largest rings. - if (maxRings > 1) { - for (let j = 0; j < polygons.length; j++) { - if (polygons[j].length <= maxRings) continue; - quickselect(polygons[j], maxRings, 1, polygons[j].length - 1, compareAreas); - polygons[j] = polygons[j].slice(0, maxRings); - } - } - - return polygons; -} - -function compareAreas(a, b) { - return b.area - a.area; -} diff --git a/src/util/util.test.ts b/src/util/util.test.ts index 1def8b5730..44dcba80c3 100644 --- a/src/util/util.test.ts +++ b/src/util/util.test.ts @@ -1,5 +1,5 @@ import Point from '@mapbox/point-geometry'; -import {arraysIntersect, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isClosedPolygon, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, readImageDataUsingOffscreenCanvas, readImageUsingVideoFrame, uniqueId, wrap} from './util'; +import {arraysIntersect, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, readImageDataUsingOffscreenCanvas, readImageUsingVideoFrame, uniqueId, wrap} from './util'; import {Canvas} from 'canvas'; describe('util', () => { @@ -197,30 +197,6 @@ describe('util isCounterClockwise', () => { }); }); -describe('util isClosedPolygon', () => { - test('not enough points', done => { - const polygon = [new Point(0, 0), new Point(1, 0), new Point(0, 1)]; - - expect(isClosedPolygon(polygon)).toBe(false); - done(); - }); - - test('not equal first + last point', done => { - const polygon = [new Point(0, 0), new Point(1, 0), new Point(0, 1), new Point(1, 1)]; - - expect(isClosedPolygon(polygon)).toBe(false); - done(); - }); - - test('closed polygon', done => { - const polygon = [new Point(0, 0), new Point(1, 0), new Point(1, 1), new Point(0, 1), new Point(0, 0)]; - - expect(isClosedPolygon(polygon)).toBe(true); - done(); - }); - -}); - describe('util parseCacheControl', () => { test('max-age', done => { expect(parseCacheControl('max-age=123456789')).toEqual({ diff --git a/src/util/util.ts b/src/util/util.ts index 3a5ac7bcf4..86a7d681f2 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -291,47 +291,6 @@ export function findLineIntersection(a1: Point, a2: Point, b1: Point, b2: Point) return new Point(a1.x + (aInterpolation * aDeltaX), a1.y + (aInterpolation * aDeltaY)); } -/** - * Returns the signed area for the polygon ring. Positive areas are exterior rings and - * have a clockwise winding. Negative areas are interior rings and have a counter clockwise - * ordering. - * - * @param ring - Exterior or interior ring - */ -export function calculateSignedArea(ring: Array): number { - let sum = 0; - for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { - p1 = ring[i]; - p2 = ring[j]; - sum += (p2.x - p1.x) * (p1.y + p2.y); - } - return sum; -} - -/** - * Detects closed polygons, first + last point are equal - * - * @param points - array of points - * @returns `true` if the points are a closed polygon - */ -export function isClosedPolygon(points: Array): boolean { - // If it is 2 points that are the same then it is a point - // If it is 3 points with start and end the same then it is a line - if (points.length < 4) - return false; - - const p1 = points[0]; - const p2 = points[points.length - 1]; - - if (Math.abs(p1.x - p2.x) > 0 || - Math.abs(p1.y - p2.y) > 0) { - return false; - } - - // polygon simplification can produce polygons with zero area and more than 3 points - return Math.abs(calculateSignedArea(points)) > 0.01; -} - /** * Converts spherical coordinates to cartesian coordinates. * diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 6943510773..e99675059b 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -36,7 +36,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 776381; + const expectedBytes = 787777; expect(actualBytes - expectedBytes).toBeLessThan(increaseQuota); expect(expectedBytes - actualBytes).toBeLessThan(decreaseQuota); diff --git a/test/integration/render/tests/distance/paint-text/expected.png b/test/integration/render/tests/distance/paint-text/expected.png new file mode 100644 index 0000000000..3a40362489 Binary files /dev/null and b/test/integration/render/tests/distance/paint-text/expected.png differ diff --git a/test/integration/render/tests/distance/paint-text/style.json b/test/integration/render/tests/distance/paint-text/style.json new file mode 100644 index 0000000000..41e66bbd60 --- /dev/null +++ b/test/integration/render/tests/distance/paint-text/style.json @@ -0,0 +1,120 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 128 + } + }, + "zoom": 15, + "center": [0.0005, 0.0005], + "sources": { + "points": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + }, + "geometry": { + "type": "Point", + "coordinates": [ + 0.0005, + 0.0005 + ] + } + }, + { + "type": "Feature", + "properties": { + }, + "geometry": { + "type": "Point", + "coordinates": [ + 0.0005, + 0.0012 + ] + } + } + ] + } + }, + "border": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 0.001], + [0.001, 0.001], + [0.001, 0], + [0, 0] + ] + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "border", + "type": "fill", + "source": "border", + "paint": { + "fill-color": "black", + "fill-opacity": 0.5 + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "points", + "layout": { + "text-field": ["concat", "D: ", ["number-format", ["distance", { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 0.001], + [0.001, 0.001], + [0.001, 0], + [0, 0] + ] + ] + }], {"max-fraction-digits": 3}]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 10 + }, + "paint": { + "text-color": ["case", ["within", { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 0.001], + [0.001, 0.001], + [0.001, 0], + [0, 0] + ] + ] + } + ], "red", "blue"] + } + } + ] +}