Skip to content

Commit 3f6e6d6

Browse files
zstadlerHarelM
andauthored
"geometry-type" to identify Multi- features (#519)
* "geometry-type" to identify Milti- features * Fix and extend tests - Add mapping from geometry-type to $type - Remove caching to geometry analysis results * Lint fixes, incomplete * Linting, continued Reverted one lint "fix" that caused test failure * Linting, var -> let, const * Linting, completed * Reuse `calculateSignedArea` * Simplify `geometryType()` code Use `return` often instead of nesting condisions * Split test * Use variables for sample geometries. Make comments part of the code * Extract code into `hasMultipleOuterRings()` * Update src/util/classify_rings.ts Co-authored-by: Harel M <[email protected]> * Updated `Changelog.md` * Add `["geomerty-type"]` tests * Rename test.json to test.json * Rename test.json to test.json * Rename test.json to test.json * Rename test.json to test.json * Rename test.json to test.json --------- Co-authored-by: Harel M <[email protected]>
1 parent 00b5831 commit 3f6e6d6

File tree

13 files changed

+418
-37
lines changed

13 files changed

+418
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## main
22

33
### ✨ Features and improvements
4-
- _...Add new stuff here..._
4+
- Aligned the implementation of `["geometry-type"]` with [its spec](https://maplibre.org/maplibre-style-spec/expressions/#geometry-type). Now, when applicable, `["geometry-type"]` returns values not available while using `"$type"`: `"MultiPoint"`, `"MultiLineString"`, and `"MultiPolygon"`. The behaviour of `"$type"` has not changed. ([#519](https://github.com/maplibre/maplibre-style-spec/pull/519))
55

66
### 🐞 Bug fixes
77
- _...Add new stuff here..._

src/expression/compound_expression.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ CompoundExpression.register(expressions, {
461461
'filter-type-==': [
462462
BooleanType,
463463
[StringType],
464-
(ctx, [v]) => ctx.geometryType() === (v as any).value
464+
(ctx, [v]) => ctx.geometryDollarType() === (v as any).value
465465
],
466466
'filter-<': [
467467
BooleanType,
@@ -548,7 +548,7 @@ CompoundExpression.register(expressions, {
548548
'filter-type-in': [
549549
BooleanType,
550550
[array(StringType)],
551-
(ctx, [v]) => (v as any).value.indexOf(ctx.geometryType()) >= 0
551+
(ctx, [v]) => (v as any).value.indexOf(ctx.geometryDollarType()) >= 0
552552
],
553553
'filter-id-in': [
554554
BooleanType,

src/expression/definitions/within.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,9 @@ class Within implements Expression {
192192

193193
evaluate(ctx: EvaluationContext) {
194194
if (ctx.geometry() != null && ctx.canonicalID() != null) {
195-
if (ctx.geometryType() === 'Point') {
195+
if (ctx.geometryDollarType() === 'Point') {
196196
return pointsWithinPolygons(ctx, this.geometries);
197-
} else if (ctx.geometryType() === 'LineString') {
197+
} else if (ctx.geometryDollarType() === 'LineString') {
198198
return linesWithinPolygons(ctx, this.geometries);
199199
}
200200
}

src/expression/evaluation_context.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import {Color} from './values';
22
import type {FormattedSection} from './types/formatted';
33
import type {GlobalProperties, Feature, FeatureState} from './index';
44
import {ICanonicalTileID} from '../tiles_and_coordinates';
5+
import {hasMultipleOuterRings} from '../util/classify_rings';
56

67
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
8+
const simpleGeometryType = {
9+
'Unknown': 'Unknown',
10+
'Point': 'Point',
11+
'MultiPoint': 'Point',
12+
'LineString': 'LineString',
13+
'MultiLineString': 'LineString',
14+
'Polygon': 'Polygon',
15+
'MultiPolygon': 'Polygon'
16+
};
717

818
class EvaluationContext {
919
globals: GlobalProperties;
@@ -14,6 +24,7 @@ class EvaluationContext {
1424
canonical: ICanonicalTileID;
1525

1626
_parseColorCache: {[_: string]: Color};
27+
_geometryType: string;
1728

1829
constructor() {
1930
this.globals = null;
@@ -29,8 +40,33 @@ class EvaluationContext {
2940
return this.feature && 'id' in this.feature ? this.feature.id : null;
3041
}
3142

43+
geometryDollarType() {
44+
return this.feature ?
45+
typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : simpleGeometryType[this.feature.type] :
46+
null;
47+
}
48+
3249
geometryType() {
33-
return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null;
50+
let geometryType = this.feature.type;
51+
if (typeof geometryType !== 'number') {
52+
return geometryType;
53+
}
54+
geometryType = geometryTypes[this.feature.type];
55+
if (geometryType === 'Unknown') {
56+
return geometryType;
57+
}
58+
const geom = this.geometry();
59+
const len = geom.length;
60+
if (len === 1) {
61+
return geometryType;
62+
}
63+
if (geometryType !== 'Polygon') {
64+
return `Multi${geometryType}`;
65+
}
66+
if (hasMultipleOuterRings(geom)) {
67+
return 'MultiPolygon';
68+
}
69+
return 'Polygon';
3470
}
3571

3672
geometry() {

src/feature_filter/convert.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function runtimeTypeChecks(expectedTypes: ExpectedTypes): ExpressionFilterSpecif
128128
function convertComparisonOp(property: string, value: any, op: string, expectedTypes?: ExpectedTypes | null): ExpressionFilterSpecification {
129129
let get;
130130
if (property === '$type') {
131-
return [op, ['geometry-type'], value] as ExpressionFilterSpecification;
131+
return convertInOp('$type', [value], op === '!=');
132132
} else if (property === '$id') {
133133
get = ['id'];
134134
} else {
@@ -162,6 +162,10 @@ function convertInOp(property: string, values: Array<any>, negate = false): Expr
162162

163163
let get: ExpressionSpecification;
164164
if (property === '$type') {
165+
const len = values.length;
166+
for (let i = 0; i < len; i++) {
167+
values.push(`Multi${values[i]}`);
168+
}
165169
get = ['geometry-type'];
166170
} else if (property === '$id') {
167171
get = ['id'];

src/feature_filter/feature_filter.test.ts

Lines changed: 191 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ describe('convert legacy filters to expressions', () => {
249249
[
250250
'match',
251251
['geometry-type'],
252-
['LineString', 'Point', 'Polygon'],
252+
['LineString', 'MultiLineString', 'MultiPoint', 'MultiPolygon', 'Point', 'Polygon'],
253253
true,
254254
false
255255
],
@@ -328,10 +328,96 @@ function legacyFilterTests(createFilterExpr) {
328328
expect(f({zoom: 0}, {properties: {}})).toBe(false);
329329
});
330330

331-
test('==, $type', () => {
332-
const f = createFilterExpr(['==', '$type', 'LineString']).filter;
333-
expect(f({zoom: 0}, {type: 1})).toBe(false);
334-
expect(f({zoom: 0}, {type: 2})).toBe(true);
331+
const UnknownGeometry = {type: 0};
332+
const PointGeometry = {type: 1, geometry: [
333+
[{x: 0, y: 0}]]};
334+
const MultiPointGeometry = {type: 1, geometry: [
335+
[{x: 0, y: 0}],
336+
[{x: 1, y: 1}]]};
337+
const LinestringGeometry = {type: 2, geometry: [
338+
[{x: 0, y: 0}, {x: 1, y: 1}]]};
339+
const MultiLineStringGeometry = {type: 2, geometry: [
340+
[{x: 0, y: 0}, {x: 1, y: 1}],
341+
[{x: 2, y: 0}, {x: 1, y: 0}]]};
342+
const PolygonGeometry = {type: 3, geometry: [
343+
[{x: 0, y: 0}, {x: 3, y: 0}, {x: 3, y: 3}, {x: 0, y: 3}, {x: 0, y: 0}],
344+
[{x: 0, y: 0}, {x: 0, y: 2}, {x: 2, y: 2}, {x: 2, y: 0}, {x: 0, y: 0}]]};
345+
const MultiPolygonGeometry = {type: 3, geometry: [
346+
[{x: 0, y: 0}, {x: 3, y: 0}, {x: 3, y: 3}, {x: 0, y: 3}, {x: 0, y: 0}],
347+
[{x: 0, y: 0}, {x: 0, y: 2}, {x: 2, y: 2}, {x: 2, y: 0}, {x: 0, y: 0}],
348+
[{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}, {x: 0, y: 0}]]};
349+
350+
test('==, $type, Point', () => {
351+
const fPoint = createFilterExpr(['==', '$type', 'Point']).filter;
352+
expect(fPoint({zoom: 0}, UnknownGeometry)).toBe(false);
353+
expect(fPoint({zoom: 0}, PointGeometry)).toBe(true);
354+
expect(fPoint({zoom: 0}, MultiPointGeometry)).toBe(true);
355+
expect(fPoint({zoom: 0}, LinestringGeometry)).toBe(false);
356+
expect(fPoint({zoom: 0}, MultiLineStringGeometry)).toBe(false);
357+
expect(fPoint({zoom: 0}, PolygonGeometry)).toBe(false);
358+
expect(fPoint({zoom: 0}, MultiPolygonGeometry)).toBe(false);
359+
expect(fPoint({zoom: 0}, {type: 'Unknown'})).toBe(false);
360+
expect(fPoint({zoom: 0}, {type: 'Point'})).toBe(true);
361+
expect(fPoint({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
362+
expect(fPoint({zoom: 0}, {type: 'LineString'})).toBe(false);
363+
expect(fPoint({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
364+
expect(fPoint({zoom: 0}, {type: 'Polygon'})).toBe(false);
365+
expect(fPoint({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
366+
});
367+
368+
test('==, $type, LineString', () => {
369+
const fLineString = createFilterExpr(['==', '$type', 'LineString']).filter;
370+
expect(fLineString({zoom: 0}, UnknownGeometry)).toBe(false);
371+
expect(fLineString({zoom: 0}, PointGeometry)).toBe(false);
372+
expect(fLineString({zoom: 0}, MultiPointGeometry)).toBe(false);
373+
expect(fLineString({zoom: 0}, LinestringGeometry)).toBe(true);
374+
expect(fLineString({zoom: 0}, MultiLineStringGeometry)).toBe(true);
375+
expect(fLineString({zoom: 0}, PolygonGeometry)).toBe(false);
376+
expect(fLineString({zoom: 0}, MultiPolygonGeometry)).toBe(false);
377+
expect(fLineString({zoom: 0}, {type: 'Unknown'})).toBe(false);
378+
expect(fLineString({zoom: 0}, {type: 'Point'})).toBe(false);
379+
expect(fLineString({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
380+
expect(fLineString({zoom: 0}, {type: 'LineString'})).toBe(true);
381+
expect(fLineString({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
382+
expect(fLineString({zoom: 0}, {type: 'Polygon'})).toBe(false);
383+
expect(fLineString({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
384+
});
385+
386+
test('==, $type, Polygon', () => {
387+
388+
const fPolygon = createFilterExpr(['==', '$type', 'Polygon']).filter;
389+
expect(fPolygon({zoom: 0}, UnknownGeometry)).toBe(false);
390+
expect(fPolygon({zoom: 0}, PointGeometry)).toBe(false);
391+
expect(fPolygon({zoom: 0}, MultiPointGeometry)).toBe(false);
392+
expect(fPolygon({zoom: 0}, LinestringGeometry)).toBe(false);
393+
expect(fPolygon({zoom: 0}, MultiLineStringGeometry)).toBe(false);
394+
expect(fPolygon({zoom: 0}, PolygonGeometry)).toBe(true);
395+
expect(fPolygon({zoom: 0}, MultiPolygonGeometry)).toBe(true);
396+
expect(fPolygon({zoom: 0}, {type: 'Unknown'})).toBe(false);
397+
expect(fPolygon({zoom: 0}, {type: 'Point'})).toBe(false);
398+
expect(fPolygon({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
399+
expect(fPolygon({zoom: 0}, {type: 'LineString'})).toBe(false);
400+
expect(fPolygon({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
401+
expect(fPolygon({zoom: 0}, {type: 'Polygon'})).toBe(true);
402+
expect(fPolygon({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
403+
});
404+
405+
test('==, $type, Unknown', () => {
406+
const fUnknown = createFilterExpr(['==', '$type', 'Unknown']).filter;
407+
expect(fUnknown({zoom: 0}, UnknownGeometry)).toBe(true);
408+
expect(fUnknown({zoom: 0}, PointGeometry)).toBe(false);
409+
expect(fUnknown({zoom: 0}, MultiPointGeometry)).toBe(false);
410+
expect(fUnknown({zoom: 0}, LinestringGeometry)).toBe(false);
411+
expect(fUnknown({zoom: 0}, MultiLineStringGeometry)).toBe(false);
412+
expect(fUnknown({zoom: 0}, PolygonGeometry)).toBe(false);
413+
expect(fUnknown({zoom: 0}, MultiPolygonGeometry)).toBe(false);
414+
expect(fUnknown({zoom: 0}, {type: 'Unknown'})).toBe(true);
415+
expect(fUnknown({zoom: 0}, {type: 'Point'})).toBe(false);
416+
expect(fUnknown({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
417+
expect(fUnknown({zoom: 0}, {type: 'LineString'})).toBe(false);
418+
expect(fUnknown({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
419+
expect(fUnknown({zoom: 0}, {type: 'Polygon'})).toBe(false);
420+
expect(fUnknown({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
335421
});
336422

337423
test('==, $id', () => {
@@ -373,10 +459,76 @@ function legacyFilterTests(createFilterExpr) {
373459
expect(f({zoom: 0}, {properties: {}})).toBe(true);
374460
});
375461

376-
test('!=, $type', () => {
377-
const f = createFilterExpr(['!=', '$type', 'LineString']).filter;
378-
expect(f({zoom: 0}, {type: 1})).toBe(true);
379-
expect(f({zoom: 0}, {type: 2})).toBe(false);
462+
test('!=, $type, Point', () => {
463+
const fPoint = createFilterExpr(['!=', '$type', 'Point']).filter;
464+
expect(fPoint({zoom: 0}, UnknownGeometry)).toBe(true);
465+
expect(fPoint({zoom: 0}, PointGeometry)).toBe(false);
466+
expect(fPoint({zoom: 0}, MultiPointGeometry)).toBe(false);
467+
expect(fPoint({zoom: 0}, LinestringGeometry)).toBe(true);
468+
expect(fPoint({zoom: 0}, MultiLineStringGeometry)).toBe(true);
469+
expect(fPoint({zoom: 0}, PolygonGeometry)).toBe(true);
470+
expect(fPoint({zoom: 0}, MultiPolygonGeometry)).toBe(true);
471+
expect(fPoint({zoom: 0}, {type: 'Unknown'})).toBe(true);
472+
expect(fPoint({zoom: 0}, {type: 'Point'})).toBe(false);
473+
expect(fPoint({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
474+
expect(fPoint({zoom: 0}, {type: 'LineString'})).toBe(true);
475+
expect(fPoint({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
476+
expect(fPoint({zoom: 0}, {type: 'Polygon'})).toBe(true);
477+
expect(fPoint({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
478+
});
479+
480+
test('!=, $type, LineString', () => {
481+
const fLineString = createFilterExpr(['!=', '$type', 'LineString']).filter;
482+
expect(fLineString({zoom: 0}, UnknownGeometry)).toBe(true);
483+
expect(fLineString({zoom: 0}, PointGeometry)).toBe(true);
484+
expect(fLineString({zoom: 0}, MultiPointGeometry)).toBe(true);
485+
expect(fLineString({zoom: 0}, LinestringGeometry)).toBe(false);
486+
expect(fLineString({zoom: 0}, MultiLineStringGeometry)).toBe(false);
487+
expect(fLineString({zoom: 0}, PolygonGeometry)).toBe(true);
488+
expect(fLineString({zoom: 0}, MultiPolygonGeometry)).toBe(true);
489+
expect(fLineString({zoom: 0}, {type: 'Unknown'})).toBe(true);
490+
expect(fLineString({zoom: 0}, {type: 'Point'})).toBe(true);
491+
expect(fLineString({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
492+
expect(fLineString({zoom: 0}, {type: 'LineString'})).toBe(false);
493+
expect(fLineString({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
494+
expect(fLineString({zoom: 0}, {type: 'Polygon'})).toBe(true);
495+
expect(fLineString({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
496+
});
497+
498+
test('!=, $type, Polygon', () => {
499+
const fPolygon = createFilterExpr(['!=', '$type', 'Polygon']).filter;
500+
expect(fPolygon({zoom: 0}, UnknownGeometry)).toBe(true);
501+
expect(fPolygon({zoom: 0}, PointGeometry)).toBe(true);
502+
expect(fPolygon({zoom: 0}, MultiPointGeometry)).toBe(true);
503+
expect(fPolygon({zoom: 0}, LinestringGeometry)).toBe(true);
504+
expect(fPolygon({zoom: 0}, MultiLineStringGeometry)).toBe(true);
505+
expect(fPolygon({zoom: 0}, PolygonGeometry)).toBe(false);
506+
expect(fPolygon({zoom: 0}, MultiPolygonGeometry)).toBe(false);
507+
expect(fPolygon({zoom: 0}, {type: 'Unknown'})).toBe(true);
508+
expect(fPolygon({zoom: 0}, {type: 'Point'})).toBe(true);
509+
expect(fPolygon({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
510+
expect(fPolygon({zoom: 0}, {type: 'LineString'})).toBe(true);
511+
expect(fPolygon({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
512+
expect(fPolygon({zoom: 0}, {type: 'Polygon'})).toBe(false);
513+
expect(fPolygon({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
514+
});
515+
516+
test('!=, $type, Unknown', () => {
517+
const fUnknown = createFilterExpr(['!=', '$type', 'Unknown']).filter;
518+
expect(fUnknown({zoom: 0}, UnknownGeometry)).toBe(false);
519+
expect(fUnknown({zoom: 0}, PointGeometry)).toBe(true);
520+
expect(fUnknown({zoom: 0}, MultiPointGeometry)).toBe(true);
521+
expect(fUnknown({zoom: 0}, LinestringGeometry)).toBe(true);
522+
expect(fUnknown({zoom: 0}, MultiLineStringGeometry)).toBe(true);
523+
expect(fUnknown({zoom: 0}, PolygonGeometry)).toBe(true);
524+
expect(fUnknown({zoom: 0}, MultiPolygonGeometry)).toBe(true);
525+
expect(fUnknown({zoom: 0}, {type: 'Unknown'})).toBe(false);
526+
expect(fUnknown({zoom: 0}, {type: 'Point'})).toBe(true);
527+
expect(fUnknown({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
528+
expect(fUnknown({zoom: 0}, {type: 'LineString'})).toBe(true);
529+
expect(fUnknown({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
530+
expect(fUnknown({zoom: 0}, {type: 'Polygon'})).toBe(true);
531+
expect(fUnknown({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
380532
});
381533

382534
test('<, number', () => {
@@ -563,15 +715,22 @@ function legacyFilterTests(createFilterExpr) {
563715

564716
test('in, $type', () => {
565717
const f = createFilterExpr(['in', '$type', 'LineString', 'Polygon']).filter;
566-
expect(f({zoom: 0}, {type: 1})).toBe(false);
567-
expect(f({zoom: 0}, {type: 2})).toBe(true);
568-
expect(f({zoom: 0}, {type: 3})).toBe(true);
718+
expect(f({zoom: 0}, {type: 'Unknown'})).toBe(false);
719+
expect(f({zoom: 0}, {type: 'Point'})).toBe(false);
720+
expect(f({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
721+
expect(f({zoom: 0}, {type: 'LineString'})).toBe(true);
722+
expect(f({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
723+
expect(f({zoom: 0}, {type: 'Polygon'})).toBe(true);
724+
expect(f({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
569725

570726
const f1 = createFilterExpr(['in', '$type', 'Polygon', 'LineString', 'Point']).filter;
571-
expect(f1({zoom: 0}, {type: 1})).toBe(true);
572-
expect(f1({zoom: 0}, {type: 2})).toBe(true);
573-
expect(f1({zoom: 0}, {type: 3})).toBe(true);
574-
727+
expect(f1({zoom: 0}, {type: 'Unknown'})).toBe(false);
728+
expect(f1({zoom: 0}, {type: 'Point'})).toBe(true);
729+
expect(f1({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
730+
expect(f1({zoom: 0}, {type: 'LineString'})).toBe(true);
731+
expect(f1({zoom: 0}, {type: 'MultiLineString'})).toBe(true);
732+
expect(f1({zoom: 0}, {type: 'Polygon'})).toBe(true);
733+
expect(f1({zoom: 0}, {type: 'MultiPolygon'})).toBe(true);
575734
});
576735

577736
test('!in, degenerate', () => {
@@ -621,9 +780,22 @@ function legacyFilterTests(createFilterExpr) {
621780

622781
test('!in, $type', () => {
623782
const f = createFilterExpr(['!in', '$type', 'LineString', 'Polygon']).filter;
624-
expect(f({zoom: 0}, {type: 1})).toBe(true);
625-
expect(f({zoom: 0}, {type: 2})).toBe(false);
626-
expect(f({zoom: 0}, {type: 3})).toBe(false);
783+
expect(f({zoom: 0}, {type: 'Unknown'})).toBe(true);
784+
expect(f({zoom: 0}, {type: 'Point'})).toBe(true);
785+
expect(f({zoom: 0}, {type: 'MultiPoint'})).toBe(true);
786+
expect(f({zoom: 0}, {type: 'LineString'})).toBe(false);
787+
expect(f({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
788+
expect(f({zoom: 0}, {type: 'Polygon'})).toBe(false);
789+
expect(f({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
790+
791+
const f1 = createFilterExpr(['!in', '$type', 'Polygon', 'LineString', 'Point']).filter;
792+
expect(f1({zoom: 0}, {type: 'Unknown'})).toBe(true);
793+
expect(f1({zoom: 0}, {type: 'Point'})).toBe(false);
794+
expect(f1({zoom: 0}, {type: 'MultiPoint'})).toBe(false);
795+
expect(f1({zoom: 0}, {type: 'LineString'})).toBe(false);
796+
expect(f1({zoom: 0}, {type: 'MultiLineString'})).toBe(false);
797+
expect(f1({zoom: 0}, {type: 'Polygon'})).toBe(false);
798+
expect(f1({zoom: 0}, {type: 'MultiPolygon'})).toBe(false);
627799
});
628800

629801
test('any', () => {

src/util/classify_rings.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,27 @@ function calculateSignedArea(ring: Point2D[]): number {
6969
}
7070
return sum;
7171
}
72+
73+
/**
74+
* Returns if there are multiple outer rings.
75+
* The first ring is an outer ring. Its direction, cw or ccw, defines the direction of outer rings.
76+
*
77+
* @param rings - List of rings
78+
* @returns Are there multiple outer rings
79+
*/
80+
export function hasMultipleOuterRings(rings: Point2D[][]): boolean {
81+
// Following https://github.com/mapbox/vector-tile-js/blob/77851380b63b07fd0af3d5a3f144cc86fb39fdd1/lib/vectortilefeature.js#L197
82+
const len = rings.length;
83+
for (let i = 0, direction; i < len; i++) {
84+
const area = calculateSignedArea(rings[i]);
85+
if (area === 0) continue;
86+
if (direction === undefined) {
87+
// Keep the direction of the first ring
88+
direction = area < 0;
89+
} else if (direction === area < 0) {
90+
// Same direction as the first ring -> a second outer ring
91+
return true;
92+
}
93+
}
94+
return false;
95+
}

0 commit comments

Comments
 (0)