xref: /expo/packages/expo-maps/src/Utils.ts (revision ee0de234)
1import { Asset } from 'expo-asset';
2import React from 'react';
3
4import { Circle, CircleObject } from './Circle';
5import { Cluster, ClusterObject } from './Cluster';
6import { Color } from './Common.types';
7import { GeoJson, GeoJsonObject } from './GeoJson';
8import { Heatmap, HeatmapObject } from './Heatmap';
9import { KML, KMLObject } from './KML';
10import { Marker, MarkerObject } from './Marker';
11import { Overlay, OverlayObject } from './Overlay';
12import { Polygon, PolygonObject } from './Polygon';
13import { Polyline, PolylineObject } from './Polyline';
14
15export function isSimpleType(child: any) {
16  return (
17    typeof child == 'string' ||
18    typeof child == 'boolean' ||
19    typeof child == 'number' ||
20    child === null ||
21    child === undefined
22  );
23}
24
25export function isPolygon(child: any): child is Polygon {
26  if ('type' in child && String(child.type).includes('Polygon') && 'props' in child) {
27    const props = Object.keys(child.props);
28    if (props.includes('points')) {
29      return true;
30    }
31  }
32  return false;
33}
34
35export function isPolyline(child: any): child is Polyline {
36  if ('type' in child && String(child.type).includes('Polyline') && 'props' in child) {
37    const props = Object.keys(child.props);
38    if (props.includes('points')) {
39      return true;
40    }
41  }
42  return false;
43}
44
45export function isOverlay(child: any): child is Overlay {
46  if ('type' in child && String(child.type).includes('Overlay') && 'props' in child) {
47    const props = Object.keys(child.props);
48    if (props.includes('bounds') && props.includes('icon')) {
49      return true;
50    }
51  }
52  return false;
53}
54
55export function isCircle(child: any): child is Circle {
56  if ('type' in child && String(child.type).includes('Circle') && 'props' in child) {
57    const props = Object.keys(child.props);
58    if (props.includes('center') && props.includes('radius')) {
59      return true;
60    }
61  }
62  return false;
63}
64
65export function isMarker(child: any): child is Marker {
66  if ('type' in child && String(child.type).includes('Marker') && 'props' in child) {
67    const props = Object.keys(child.props);
68    if (props.includes('latitude') && props.includes('longitude')) {
69      return true;
70    }
71  }
72  return false;
73}
74
75export function isCluster(child: any): child is Cluster {
76  if ('type' in child && String(child.type).includes('Cluster') && 'props' in child) {
77    const props = Object.keys(child.props);
78    if (props.includes('name')) {
79      return true;
80    }
81  }
82  return false;
83}
84
85export function isKML(child: any): child is KML {
86  if ('type' in child && String(child.type).includes('KML') && 'props' in child) {
87    const props = Object.keys(child.props);
88    if (props.includes('filePath')) {
89      return true;
90    }
91  }
92  return false;
93}
94
95export function isGeoJson(child: any): child is GeoJson {
96  if ('type' in child && String(child.type).includes('GeoJson') && 'props' in child) {
97    const props = Object.keys(child.props);
98    if (props.includes('geoJsonString')) {
99      return true;
100    }
101  }
102  return false;
103}
104
105export function isHeatmap(child: any): child is Heatmap {
106  if ('type' in child && String(child.type).includes('Heatmap') && 'props' in child) {
107    const props = Object.keys(child.props);
108    if (props.includes('points')) {
109      return true;
110    }
111  }
112  return false;
113}
114
115export function isHexColor(color: any): color is Color {
116  return color.length > 0 && color[0] === '#';
117}
118
119export function mapColorToHexColor(color: Color, defaultColor?: string): string {
120  let defColor = defaultColor;
121  if (defColor === undefined) defColor = '#000000';
122
123  const colors: Record<Color | 'default', string> = {
124    red: '#ff0000',
125    blue: '#0000ff',
126    green: '#00ff00',
127    black: '#000000',
128    white: '#ffffff',
129    gray: '#808080',
130    cyan: '#00ffff',
131    magenta: '#ff00ff',
132    yellow: '#ffff00',
133    lightgray: '#d3d3d3',
134    darkgray: '#a9a9a9',
135    aqua: '#00ffff',
136    fuchsia: '#ca2c92',
137    lime: '#bfff00',
138    maroon: '#800000',
139    navy: '#000080',
140    olive: '#808000',
141    purple: '#800080',
142    silver: '#c0c0c0',
143    teal: '#008080',
144    default: defColor,
145  };
146
147  return colors[color] || colors['default'];
148}
149
150export function warnIfChildIsIncompatible(child: any) {
151  if (typeof child == 'string' || typeof child == 'boolean' || typeof child == 'number') {
152    console.warn(`Warning! Child of type ${typeof child} isn't valid ExpoMap child!`);
153  } else if (child != null && child !== undefined) {
154    console.log(child.type);
155    console.warn(
156      `Warning! Child of type ${(child as React.ReactElement<any>).type} isn't valid ExpoMap child!`
157    );
158  }
159}
160
161export function buildGeoJsonObject(child: GeoJson): GeoJsonObject {
162  if (
163    child.props.defaultStyle?.marker?.color !== undefined &&
164    !isHexColor(child.props.defaultStyle.marker.color)
165  ) {
166    child.props.defaultStyle.marker.color = mapColorToHexColor(
167      child.props.defaultStyle?.marker.color as Color,
168      '#ff0000'
169    );
170  }
171
172  if (
173    child.props.defaultStyle?.polygon?.fillColor !== undefined &&
174    !isHexColor(child.props.defaultStyle.polygon.fillColor)
175  ) {
176    child.props.defaultStyle.polygon.fillColor = mapColorToHexColor(
177      child.props.defaultStyle.polygon.fillColor as Color
178    );
179  }
180
181  if (
182    child.props.defaultStyle?.polygon?.strokeColor !== undefined &&
183    !isHexColor(child.props.defaultStyle.polygon.strokeColor)
184  ) {
185    child.props.defaultStyle.polygon.strokeColor = mapColorToHexColor(
186      child.props.defaultStyle.polygon.strokeColor as Color
187    );
188  }
189
190  if (
191    child.props.defaultStyle?.polyline?.color !== undefined &&
192    !isHexColor(child.props.defaultStyle.polyline.color)
193  ) {
194    child.props.defaultStyle.polyline.color = mapColorToHexColor(
195      child.props.defaultStyle.polyline.color as Color
196    );
197  }
198
199  return {
200    type: 'geojson',
201    geoJsonString: child.props.geoJsonString,
202    defaultStyle: child.props.defaultStyle,
203  } as GeoJsonObject;
204}
205
206export async function buildMarkerObject(child: Marker): Promise<MarkerObject> {
207  let iconPath: Asset | undefined = undefined;
208  if (child.props.icon !== undefined) {
209    iconPath = await Asset.fromModule(child.props.icon).downloadAsync();
210  }
211
212  const markerObject = {
213    type: 'marker',
214    id: child.props.id,
215    latitude: child.props.latitude,
216    longitude: child.props.longitude,
217    markerTitle: child.props.markerTitle,
218    markerSnippet: child.props.markerSnippet,
219    icon: iconPath?.localUri,
220    color: child.props.color,
221    draggable: child.props.draggable ? child.props.draggable : false,
222    anchorU: child.props.anchorU,
223    anchorV: child.props.anchorV,
224    opacity: child.props.opacity ? child.props.opacity : 1,
225  } as MarkerObject;
226
227  if (markerObject.color !== undefined && !isHexColor(markerObject.color)) {
228    markerObject.color = mapColorToHexColor(markerObject.color as Color, '#ff0000');
229  }
230  return markerObject;
231}
232
233export async function buildOverlayObject(child: Overlay): Promise<OverlayObject> {
234  const iconPath = await Asset.fromModule(child.props.icon).downloadAsync();
235
236  const overlayObject = {
237    type: 'overlay',
238    bounds: child.props.bounds,
239    icon: iconPath.localUri,
240  } as OverlayObject;
241
242  return overlayObject;
243}
244
245export function buildPolygonObject(child: Polygon): PolygonObject {
246  const polygonObject = {
247    type: 'polygon',
248    points: child.props.points,
249    fillColor: child.props.fillColor,
250    strokeColor: child.props.strokeColor,
251    strokeWidth: child.props.strokeWidth,
252    strokePattern: child.props.strokePattern,
253    jointType: child.props.jointType,
254  } as PolygonObject;
255  if (polygonObject.fillColor !== undefined && !isHexColor(polygonObject.fillColor)) {
256    polygonObject.fillColor = mapColorToHexColor(polygonObject.fillColor as Color);
257  }
258  return polygonObject;
259}
260
261export function buildPolylineObject(child: Polyline): PolylineObject {
262  const polylineObject = {
263    type: 'polyline',
264    points: child.props.points,
265    color: child.props.color,
266    width: child.props.width,
267    pattern: child.props.pattern,
268    jointType: child.props.jointType,
269    capType: child.props.capType,
270  } as PolylineObject;
271  if (polylineObject.color !== undefined && !isHexColor(polylineObject.color)) {
272    polylineObject.color = mapColorToHexColor(polylineObject.color as Color);
273  }
274  return polylineObject;
275}
276
277export function buildCircleObject(child: Circle): CircleObject {
278  return {
279    type: 'circle',
280    center: child.props.center,
281    radius: child.props.radius,
282    fillColor: child.props.fillColor,
283    strokeColor: child.props.strokeColor,
284    strokeWidth: child.props.strokeWidth,
285  } as CircleObject;
286}
287
288export async function buildKMLObject(child: KML): Promise<KMLObject> {
289  const filePath = await Asset.fromModule(child.props.filePath).downloadAsync();
290  return {
291    type: 'kml',
292    filePath: filePath.localUri,
293  } as KMLObject;
294}
295
296export async function buildClusterObject(child: Cluster): Promise<ClusterObject | null> {
297  const clusterChildrenArray = React.Children.map(child.props.children, async (clusterChild) => {
298    if (!isSimpleType(clusterChild)) {
299      if (isMarker(clusterChild)) {
300        return buildMarkerObject(clusterChild);
301      }
302    }
303    warnIfChildIsIncompatible(clusterChild);
304    return null;
305  });
306
307  if (clusterChildrenArray !== undefined) {
308    let iconPath: Asset | undefined = undefined;
309    if (child.props.icon !== undefined) {
310      iconPath = await Asset.fromModule(child.props.icon).downloadAsync();
311    }
312
313    // TODO(@lukmccall): remove any cast
314    const clusterPropObjects = await Promise.all(clusterChildrenArray as any);
315    let minimumClusterSize: number;
316
317    if (child.props.minimumClusterSize !== undefined && child.props.minimumClusterSize > 0) {
318      minimumClusterSize = child.props.minimumClusterSize;
319    } else {
320      minimumClusterSize = 4;
321    }
322
323    const clusterObject = {
324      type: 'cluster',
325      id: child.props.id,
326      markers: clusterPropObjects,
327      name: child.props.name,
328      minimumClusterSize,
329      markerTitle: child.props.markerTitle,
330      markerSnippet: child.props.markerSnippet,
331      icon: iconPath?.localUri,
332      color: child.props.color,
333      opacity: child.props.opacity ? child.props.opacity : 1,
334    } as ClusterObject;
335
336    if (clusterObject.color !== undefined && !isHexColor(clusterObject.color)) {
337      clusterObject.color = mapColorToHexColor(clusterObject.color as Color, '#ff0000');
338    }
339    return clusterObject;
340  }
341  return null;
342}
343
344export function buildHeatmapObject(child: Heatmap): HeatmapObject {
345  return {
346    type: 'heatmap',
347    points: child.props.points,
348    radius: child.props.radius,
349    gradient: child.props.gradient,
350    opacity: child.props.opacity,
351  } as HeatmapObject;
352}
353