import * as React from 'react';
import type {
Feature,
FeatureCollection,
Point,
Position,
MultiPoint,
LineString,
MultiLineString,
Polygon,
MultiPolygon,
} from 'geojson';
import Marker, {type MapMarkerProps as MarkerProps} from './MapMarker';
import type {MapPolygonProps as PolygonProps} from './MapPolygon';
import type {MapPolylineProps as PolylineProps} from './MapPolyline';
import Polyline from './MapPolyline';
import MapPolygon from './MapPolygon';
import type {LatLng} from './sharedTypes';
export type GeojsonProps = {
/**
* Sets the anchor point for the marker.
* The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
*
* The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0],
* where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner.
*
* The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding.
* For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).
*
* @default {x: 0.5, y: 1.0}
* @platform iOS: Google Maps only. For Apple Maps, see the `centerOffset` prop
* @platform Android: Supported
*/
anchor?: MarkerProps['anchor'];
/**
* The offset (in points) at which to display the annotation view.
*
* By default, the center point of an annotation view is placed at the coordinate point of the associated annotation.
*
* Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.
*
* @default {x: 0.0, y: 0.0}
* @platform iOS: Apple Maps only. For Google Maps, see the `anchor` prop
* @platform Android: Not supported. See see the `anchor` prop
*/
centerOffset?: MarkerProps['centerOffset'];
/**
* The pincolor used on markers
*
* @platform iOS: Supported
* @platform Android: Supported
*/
color?: MarkerProps['pinColor'];
/**
* The fill color to use for the path.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
fillColor?: PolygonProps['fillColor'];
/**
* [Geojson](https://geojson.org/) description of object.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
geojson: FeatureCollection;
/**
* A custom image to be used as the marker's icon. Only local image resources are allowed to be
* used.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
image?: MarkerProps['image'];
/**
* The line cap style to apply to the open ends of the path.
* The default style is `round`.
*
* @platform iOS: Apple Maps only
* @platform Android: Supported
*/
lineCap?: PolylineProps['lineCap'];
/**
* An array of numbers specifying the dash pattern to use for the path.
*
* The array contains one or more numbers that indicate the lengths (measured in points) of the
* line segments and gaps in the pattern. The values in the array alternate, starting with the
* first line segment length, followed by the first gap length, followed by the second line
* segment length, and so on.
*
* This property is set to `null` by default, which indicates no line dash pattern.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
lineDashPattern?:
| PolygonProps['lineDashPattern']
| PolylineProps['lineDashPattern'];
/**
* The offset (in points) at which to start drawing the dash pattern.
*
* Use this property to start drawing a dashed line partway through a segment or gap. For
* example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the
* middle of the first gap.
*
* The default value of this property is 0.
*
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
lineDashPhase?: PolylineProps['lineDashPhase'];
/**
* The line join style to apply to corners of the path.
* The default style is `round`.
*
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
lineJoin?: PolylineProps['lineJoin'];
/**
* Component to render in place of the default marker when the overlay type is a `point`
*
* @platform iOS: Supported
* @platform Android: Supported
*/
markerComponent?: MarkerProps['children'];
/**
* The limiting value that helps avoid spikes at junctions between connected line segments.
* The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If
* the ratio of the miter length—that is, the diagonal length of the miter join—to the line
* thickness exceeds the miter limit, the joint is converted to a bevel join. The default
* miter limit is 10, which results in the conversion of miters whose angle at the joint
* is less than 11 degrees.
*
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
miterLimit?: PolylineProps['miterLimit'];
/**
* Callback that is called when the user presses any of the overlays
*/
onPress?: (event: OverlayPressEvent) => void;
/**
* The stroke color to use for the path.
*
* @platform — iOS: Supported
* @platform — Android: Supported
*/
strokeColor?: PolygonProps['strokeColor'] | PolylineProps['strokeColor'];
/**
* The stroke width to use for the path.
*
* @platform — iOS: Supported
* @platform — Android: Supported
*/
strokeWidth?: PolygonProps['strokeWidth'] | PolylineProps['strokeWidth'];
/**
* Make the `Polygon` or `Polyline` tappable
*
* @platform — iOS: Google Maps only
* @platform — Android: Supported
*/
tappable?: PolygonProps['tappable'] | PolylineProps['tappable'];
/**
* The title of the marker. This is only used if the component has no children that
* are a ``, in which case the default callout behavior will be used, which
* will show both the `title` and the `description`, if provided.
*
* @platform — iOS: Supported
* @platform — Android: Supported
*/
title?: MarkerProps['title'];
/**
* Sets whether this marker should track view changes.
* It's recommended to turn it off whenever it's possible to improve custom marker performance.
* This is the default value for all point markers in your geojson data. It can be overriden
* on a per point basis by adding a `trackViewChanges` property to the `properties` object on the point.
*
* @default true
* @platform iOS: Google Maps only
* @platform Android: Supported
*/
tracksViewChanges?: boolean;
/**
* The order in which this tile overlay is drawn with respect to other overlays. An overlay
* with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays
* with the same z-index is arbitrary. The default zIndex is 0.
*
* @platform iOS: Apple Maps: [Marker], Google Maps: [Marker, Polygon, Polyline]
* @platform Android: Supported
*/
zIndex?:
| MarkerProps['zIndex']
| PolygonProps['zIndex']
| PolylineProps['zIndex'];
};
const Geojson = (props: GeojsonProps) => {
const {
anchor,
centerOffset,
geojson,
strokeColor,
fillColor,
strokeWidth,
color,
title,
image,
zIndex,
onPress,
lineCap,
lineJoin,
tappable,
tracksViewChanges,
miterLimit,
lineDashPhase,
lineDashPattern,
markerComponent,
} = props;
const pointOverlays = makePointOverlays(geojson.features);
const lineOverlays = makeLineOverlays(geojson.features);
const polygonOverlays = makePolygonOverlays(geojson.features);
return (
{pointOverlays.map((overlay, index) => {
const markerColor = getColor(color, overlay, 'marker-color');
const pointOverlayTracksViewChanges =
overlay.feature.properties?.tracksViewChanges || tracksViewChanges;
return (
onPress && onPress(overlay)}>
{markerComponent}
);
})}
{lineOverlays.map((overlay, index) => {
const lineStrokeColor = getColor(strokeColor, overlay, 'stroke');
const lineStrokeWidth = getStrokeWidth(strokeWidth, overlay);
return (
onPress && onPress(overlay)}
/>
);
})}
{polygonOverlays.map((overlay, index) => {
const polygonFillColor = getColor(fillColor, overlay, 'fill');
const lineStrokeColor = getColor(strokeColor, overlay, 'stroke');
const lineStrokeWidth = getStrokeWidth(strokeWidth, overlay);
return (
onPress && onPress(overlay)}
zIndex={zIndex}
/>
);
})}
);
};
export default Geojson;
const makePointOverlays = (features: Feature[]): AnyPointOverlay[] => {
return features
.filter(isAnyPointFeature)
.map(feature =>
makeCoordinatesForAnyPoint(feature.geometry).map(coordinates =>
makeOverlayForAnyPoint(coordinates, feature),
),
)
.reduce((prev, curr) => prev.concat(curr), [])
.map(overlay => ({...overlay, type: 'point'}));
};
const makeLineOverlays = (features: Feature[]): AnyLineStringOverlay[] => {
return features
.filter(isAnyLineStringFeature)
.map(feature =>
makeCoordinatesForAnyLine(feature.geometry).map(coordinates =>
makeOverlayForAnyLine(coordinates, feature),
),
)
.reduce((prev, curr) => prev.concat(curr), [])
.map(overlay => ({...overlay, type: 'polyline'}));
};
const makePolygonOverlays = (features: Feature[]): AnyPolygonOverlay[] => {
const multipolygons: AnyPolygonOverlay[] = features
.filter(isMultiPolygonFeature)
.map(feature =>
makeCoordinatesForMultiPolygon(feature.geometry).map(coordinates =>
makeOverlayForAnyPolygon(coordinates, feature),
),
)
.reduce((prev, curr) => prev.concat(curr), [])
.map(overlay => ({...overlay, type: 'polygon'}));
const polygons: AnyPolygonOverlay[] = features
.filter(isPolygonFeature)
.map(feature =>
makeOverlayForAnyPolygon(
makeCoordinatesForPolygon(feature.geometry),
feature,
),
)
.reduce[]>(
(prev, curr) => prev.concat(curr),
[],
)
.map(overlay => ({...overlay, type: 'polygon'}));
return polygons.concat(multipolygons);
};
const makeOverlayForAnyPoint = (
coordinates: LatLng,
feature: Feature,
): Omit => {
return {feature, coordinates};
};
const makeOverlayForAnyLine = (
coordinates: LatLng[],
feature: Feature,
): Omit => {
return {feature, coordinates};
};
const makeOverlayForAnyPolygon = (
coordinates: LatLng[][],
feature: Feature,
): Omit => {
return {
feature,
coordinates: coordinates[0],
holes: coordinates.length > 1 ? coordinates.slice(1) : undefined,
};
};
const makePoint = (c: Position): LatLng => ({
latitude: c[1],
longitude: c[0],
});
const makeLine = (l: Position[]) => l.map(makePoint);
const makeCoordinatesForAnyPoint = (geometry: Point | MultiPoint) => {
if (geometry.type === 'Point') {
return [makePoint(geometry.coordinates)];
}
return geometry.coordinates.map(makePoint);
};
const makeCoordinatesForAnyLine = (geometry: LineString | MultiLineString) => {
if (geometry.type === 'LineString') {
return [makeLine(geometry.coordinates)];
}
return geometry.coordinates.map(makeLine);
};
const makeCoordinatesForPolygon = (geometry: Polygon) => {
return geometry.coordinates.map(makeLine);
};
const makeCoordinatesForMultiPolygon = (geometry: MultiPolygon) => {
return geometry.coordinates.map(p => p.map(makeLine));
};
const getRgbaFromHex = (hex: string, alpha: number = 1) => {
const matchArray = hex.match(/\w\w/g);
if (!matchArray || matchArray.length < 3) {
throw new Error('Invalid hex string');
}
const [r, g, b] = matchArray.map(x => {
const subColor = parseInt(x, 16);
if (Number.isNaN(subColor)) {
throw new Error('Invalid hex string');
}
return subColor;
});
return `rgba(${r},${g},${b},${alpha})`;
};
const getColor = (
prop: string | undefined,
overlay: Overlay,
colorType: string,
) => {
let color = overlay.feature.properties?.[colorType];
if (color) {
const opacityProperty = colorType + '-opacity';
const alpha = overlay.feature.properties?.[opacityProperty];
if (alpha && alpha !== '0' && color[0] === '#') {
color = getRgbaFromHex(color, alpha);
}
return color;
} else if (prop) {
return prop;
}
return undefined;
};
const getStrokeWidth = (
prop: GeojsonProps['strokeWidth'],
overlay: Overlay,
) => {
return overlay.feature.properties?.['stroke-width'] ?? prop;
};
// GeoJSON.Feature type-guards
const isPointFeature = (feature: Feature): feature is Feature =>
feature.geometry.type === 'Point';
const isMultiPointFeature = (
feature: Feature,
): feature is Feature => feature.geometry.type === 'MultiPoint';
const isAnyPointFeature = (
feature: Feature,
): feature is Feature | Feature =>
isPointFeature(feature) || isMultiPointFeature(feature);
const isLineStringFeature = (
feature: Feature,
): feature is Feature => feature.geometry.type === 'LineString';
const isMultiLineStringFeature = (
feature: Feature,
): feature is Feature =>
feature.geometry.type === 'MultiLineString';
const isAnyLineStringFeature = (
feature: Feature,
): feature is Feature | Feature =>
isLineStringFeature(feature) || isMultiLineStringFeature(feature);
const isPolygonFeature = (feature: Feature): feature is Feature =>
feature.geometry.type === 'Polygon';
const isMultiPolygonFeature = (
feature: Feature,
): feature is Feature => feature.geometry.type === 'MultiPolygon';
type OverlayPressEvent = {
type:
| AnyPointOverlay['type']
| AnyLineStringOverlay['type']
| AnyPolygonOverlay['type'];
feature:
| AnyPointOverlay['feature']
| AnyLineStringOverlay['feature']
| AnyPolygonOverlay['feature'];
coordinates:
| AnyPointOverlay['coordinates']
| AnyLineStringOverlay['coordinates']
| AnyPolygonOverlay['coordinates'];
holes?: AnyPolygonOverlay['holes'];
};
type AnyPointOverlay = {
type: 'point';
feature: Feature;
coordinates: LatLng;
};
type AnyLineStringOverlay = {
type: 'polyline';
feature: Feature;
coordinates: LatLng[];
};
type AnyPolygonOverlay = {
type: 'polygon';
feature: Feature;
coordinates: LatLng[];
holes?: LatLng[][];
};
type Overlay = {
type: 'point' | 'polyline' | 'polygon';
feature: Feature;
coordinates: LatLng | LatLng[];
holes?: LatLng[][];
};