|
1 | 1 | import {isValue} from '../values'; |
2 | | -import {NumberType} from '../types'; |
| 2 | +import {NumberType, ValueType} from '../types'; |
3 | 3 | import {classifyRings, updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment} from '../../util/geometry_util'; |
4 | 4 | import {lngFromMercatorX, latFromMercatorY} from '../../util/mercator'; |
5 | 5 | import TinyQueue from "tinyqueue"; |
6 | 6 | import EXTENT from '../../data/extent'; |
| 7 | +import Literal from './literal'; |
| 8 | +import {isGlobalPropertyConstant, isStateConstant} from '../is_constant'; |
7 | 9 |
|
8 | 10 | // Geodesic scale factors (cheap-ruler math): meters per degree lon/lat at a given latitude. |
9 | 11 | // Only the three distance operations used in this file are implemented. |
@@ -587,70 +589,130 @@ function isTypeValid(type: string) { |
587 | 589 | type === "MultiPolygon" |
588 | 590 | ); |
589 | 591 | } |
| 592 | + |
| 593 | +// Resolves a GeoJSON value (Feature / FeatureCollection / bare geometry) down |
| 594 | +// to the geometries `distance` measures against -- one per feature for a |
| 595 | +// FeatureCollection, one otherwise. Shared by parse-time (literal argument) |
| 596 | +// and evaluate-time (e.g. a `["config", ...]` argument, whose value isn't |
| 597 | +// known until evaluation) resolution. Returns null if the value isn't valid |
| 598 | +// GeoJSON, or if any feature has a geometry type other than |
| 599 | +// Point/LineString/Polygon (and their Multi* variants). |
| 600 | +function extractDistanceGeometry(value: unknown): Array<DistanceGeometry> | null { |
| 601 | + if (!isValue(value) || typeof value !== 'object' || value === null || Array.isArray(value)) { |
| 602 | + return null; |
| 603 | + } |
| 604 | + const geojson = value as GeoJSON.GeoJSON; |
| 605 | + if (geojson.type === 'FeatureCollection') { |
| 606 | + if (geojson.features.length === 0) return null; |
| 607 | + const geometries: Array<DistanceGeometry> = []; |
| 608 | + for (const feature of geojson.features) { |
| 609 | + if (!isTypeValid(feature.geometry.type)) return null; |
| 610 | + geometries.push(feature.geometry as DistanceGeometry); |
| 611 | + } |
| 612 | + return geometries; |
| 613 | + } |
| 614 | + if (geojson.type === 'Feature') { |
| 615 | + return isTypeValid(geojson.geometry.type) ? [geojson.geometry as DistanceGeometry] : null; |
| 616 | + } |
| 617 | + if (isTypeValid(geojson.type)) { |
| 618 | + return [geojson as DistanceGeometry]; |
| 619 | + } |
| 620 | + return null; |
| 621 | +} |
| 622 | + |
590 | 623 | class Distance implements Expression { |
591 | 624 | type: Type; |
592 | | - geojson: GeoJSON.GeoJSON; |
593 | | - geometries: DistanceGeometry; |
| 625 | + geojson: Expression; |
594 | 626 |
|
595 | | - constructor(geojson: GeoJSON.GeoJSON, geometries: DistanceGeometry) { |
| 627 | + constructor(geojson: Expression) { |
596 | 628 | this.type = NumberType; |
597 | 629 | this.geojson = geojson; |
598 | | - this.geometries = geometries; |
599 | 630 | } |
600 | 631 |
|
601 | 632 | static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Distance | null | void { |
602 | 633 | if (args.length !== 2) { |
603 | 634 | return context.error(`'distance' expression requires either one argument, but found ' ${args.length - 1} instead.`); |
604 | 635 | } |
605 | | - if (isValue(args[1])) { |
606 | | - const geojson = args[1] as GeoJSON.GeoJSON; |
607 | | - if (geojson.type === 'FeatureCollection') { |
608 | | - for (let i = 0; i < geojson.features.length; ++i) { |
609 | | - if (isTypeValid(geojson.features[i]!.geometry.type)) { |
610 | | - return new Distance(geojson, geojson.features[i]!.geometry as DistanceGeometry); |
611 | | - } |
612 | | - } |
613 | | - } else if (geojson.type === 'Feature') { |
614 | | - if (isTypeValid(geojson.geometry.type)) { |
615 | | - return new Distance(geojson, geojson.geometry as DistanceGeometry); |
616 | | - } |
617 | | - } else if (isTypeValid(geojson.type)) { |
618 | | - return new Distance(geojson, geojson as DistanceGeometry); |
| 636 | + |
| 637 | + const arg = args[1]; |
| 638 | + // A bare GeoJSON value (Feature / FeatureCollection / bare geometry) |
| 639 | + // isn't valid expression syntax on its own, so wrap it as a literal |
| 640 | + // like ["literal", {...}] would be. Anything else -- e.g. |
| 641 | + // `["config", "key"]` -- is left as-is and parsed as a regular |
| 642 | + // sub-expression, resolved to GeoJSON at evaluation time so a config |
| 643 | + // value can change at runtime without re-parsing the style. |
| 644 | + const isBareGeoJSON = isValue(arg) && !Array.isArray(arg); |
| 645 | + const parsed = context.parse(isBareGeoJSON ? ['literal', arg] : arg, 1, ValueType); |
| 646 | + if (!parsed) return null; |
| 647 | + |
| 648 | + for (const [globalProperties, name] of [ |
| 649 | + [['measure-light'], 'brightness'], |
| 650 | + [['pitch'], 'pitch'], |
| 651 | + [['distance-from-center'], 'distance-from-center'], |
| 652 | + ] as Array<[Array<string>, string]>) { |
| 653 | + if (!isGlobalPropertyConstant(parsed, globalProperties)) { |
| 654 | + return context.error(`'distance' expression may not depend on ${name}.`); |
619 | 655 | } |
620 | 656 | } |
621 | | - return context.error( |
622 | | - "'distance' expression needs to be an array with format [\'Distance\', GeoJSONObj]." |
623 | | - ); |
| 657 | + if (!isStateConstant(parsed)) { |
| 658 | + return context.error(`'distance' expression may not depend on feature-state.`); |
| 659 | + } |
| 660 | + |
| 661 | + // Literal arguments can be validated eagerly; a config-driven |
| 662 | + // argument can't be checked until it's evaluated. |
| 663 | + if (parsed instanceof Literal && !extractDistanceGeometry(parsed.value)) { |
| 664 | + return context.error( |
| 665 | + "'distance' expression needs to be an array with format [\'Distance\', GeoJSONObj]." |
| 666 | + ); |
| 667 | + } |
| 668 | + |
| 669 | + return new Distance(parsed); |
624 | 670 | } |
625 | 671 |
|
626 | 672 | evaluate(ctx: EvaluationContext): number | null { |
| 673 | + const geometries = extractDistanceGeometry(this.geojson.evaluate(ctx)); |
| 674 | + if (!geometries) { |
| 675 | + console.warn("Distance Expression: could not resolve a valid Point/LineString/Polygon GeoJSON geometry."); |
| 676 | + return null; |
| 677 | + } |
| 678 | + |
627 | 679 | const geometry = ctx.geometry(); |
628 | 680 | const canonical = ctx.canonicalID(); |
629 | | - if (geometry != null && canonical != null) { |
630 | | - if (ctx.geometryType() === 'Point') { |
631 | | - return pointsToGeometryDistance(geometry, canonical, this.geometries); |
632 | | - } |
633 | | - if (ctx.geometryType() === 'LineString') { |
634 | | - return linesToGeometryDistance(geometry, canonical, this.geometries); |
635 | | - } |
636 | | - if (ctx.geometryType() === 'Polygon') { |
637 | | - return polygonsToGeometryDistance(geometry, canonical, this.geometries); |
638 | | - } |
639 | | - console.warn("Distance Expression: currently only evaluates valid Point/LineString/Polygon geometries."); |
640 | | - } else { |
| 681 | + if (geometry == null || canonical == null) { |
641 | 682 | console.warn("Distance Expression: requires valid feature and canonical information."); |
| 683 | + return null; |
642 | 684 | } |
643 | | - return null; |
| 685 | + |
| 686 | + const geometryType = ctx.geometryType(); |
| 687 | + const distanceTo = geometryType === 'Point' ? pointsToGeometryDistance : |
| 688 | + geometryType === 'LineString' ? linesToGeometryDistance : |
| 689 | + geometryType === 'Polygon' ? polygonsToGeometryDistance : null; |
| 690 | + if (!distanceTo) { |
| 691 | + console.warn("Distance Expression: currently only evaluates valid Point/LineString/Polygon geometries."); |
| 692 | + return null; |
| 693 | + } |
| 694 | + |
| 695 | + // A FeatureCollection reference resolves to one geometry per feature; |
| 696 | + // report the distance to the closest one. |
| 697 | + let dist = Infinity; |
| 698 | + for (const g of geometries) { |
| 699 | + const tempDist = distanceTo(geometry, canonical, g); |
| 700 | + if (tempDist == null || isNaN(tempDist)) return tempDist; |
| 701 | + if ((dist = Math.min(dist, tempDist)) === 0) break; |
| 702 | + } |
| 703 | + return dist; |
644 | 704 | } |
645 | 705 |
|
646 | | - eachChild() {} |
| 706 | + eachChild(fn: (_: Expression) => void) { |
| 707 | + fn(this.geojson); |
| 708 | + } |
647 | 709 |
|
648 | 710 | outputDefined(): boolean { |
649 | 711 | return true; |
650 | 712 | } |
651 | 713 |
|
652 | 714 | serialize(): Array<unknown> { |
653 | | - return ['distance', this.geojson]; |
| 715 | + return ['distance', this.geojson.serialize()]; |
654 | 716 | } |
655 | 717 | } |
656 | 718 |
|
|
0 commit comments