@local-logic/maps 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/src/components/Map/Root/BaseMap/index.tsx +1 -3
  4. package/src/components/Map/Root/BaseMap/styles.ts +1 -2
  5. package/src/components/Map/Root/Layers/Google/index.tsx +97 -0
  6. package/src/components/Map/Root/Layers/Mapbox/index.tsx +74 -0
  7. package/src/components/Map/Root/Layers/Maptiler/index.tsx +74 -0
  8. package/src/components/Map/Root/Layers/index.tsx +32 -0
  9. package/src/components/Map/Root/Layers/types.ts +15 -0
  10. package/src/components/Map/Root/Layers/utils.ts +64 -0
  11. package/src/components/Map/Root/Markers/Cluster/index.tsx +7 -6
  12. package/src/components/Map/Root/Markers/Cluster/styles.ts +9 -12
  13. package/src/components/Map/Root/Markers/Google/index.tsx +11 -11
  14. package/src/components/Map/Root/Markers/Mapbox/index.tsx +20 -9
  15. package/src/components/Map/Root/Markers/Maptiler/index.tsx +19 -11
  16. package/src/components/Map/Root/Markers/index.tsx +42 -22
  17. package/src/components/Map/Root/Markers/styles.ts +16 -8
  18. package/src/components/Map/Root/Markers/types.ts +19 -2
  19. package/src/components/Map/Root/Popup/Google/index.tsx +57 -0
  20. package/src/components/Map/Root/Popup/Google/popup.css +15 -0
  21. package/src/components/Map/Root/Popup/Mapbox/index.tsx +36 -0
  22. package/src/components/Map/Root/Popup/Mapbox/popup.css +15 -0
  23. package/src/components/Map/Root/Popup/Maptiler/index.tsx +37 -0
  24. package/src/components/Map/Root/Popup/Maptiler/popup.css +15 -0
  25. package/src/components/Map/Root/Popup/index.tsx +32 -0
  26. package/src/components/Map/Root/Popup/styles.ts +4 -0
  27. package/src/components/Map/Root/Popup/types.ts +20 -0
  28. package/src/components/Map/Root/index.tsx +14 -1
  29. package/src/components/Map/Root/types.ts +1 -13
  30. package/src/components/Map/index.stories.tsx +86 -70
  31. package/src/components/Map/storybook-data.ts +255 -0
  32. package/src/components/Map/Root/Markers/Google/styles.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @local-logic/maps
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - de9946a: LL22-5133: Added layer support to map package
8
+
9
+ ## 0.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 9dcc72d: LL22-5132: Added popup support for markers and clusters
14
+
3
15
  ## 0.0.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@local-logic/maps",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "This is a maps implementation allowing for the display of Local Logic data on a map.",
5
5
  "author": "Local Logic",
6
6
  "license": "ISC",
@@ -28,9 +28,7 @@ export const BaseMap = ({ children }: BaseMapProps) => {
28
28
 
29
29
  return (
30
30
  <div className={styles.wrapper}>
31
- <BaseMapProvider>
32
- {children}
33
- </BaseMapProvider>
31
+ <BaseMapProvider>{children}</BaseMapProvider>
34
32
  </div>
35
33
  );
36
34
  };
@@ -1,2 +1 @@
1
- export const wrapper = `relative w-full h-full`;
2
- export const map = `absolute w-full h-full`;
1
+ export const wrapper = `w-full h-full`;
@@ -0,0 +1,97 @@
1
+ import React, { useState, useEffect, useContext } from "react";
2
+
3
+ // Work inspired from:
4
+ // https://github.com/visgl/react-google-maps/blob/main/examples/geometry/src/components/polygon.tsx
5
+ // https://visgl.github.io/react-google-maps/examples/geometry
6
+
7
+ import { GoogleMapsContext } from "@vis.gl/react-google-maps";
8
+
9
+ import { getGooglePolygonFromLocalLogicGeometry } from "../utils";
10
+
11
+ import type { Props, Layer as LayerType } from "../types";
12
+
13
+ function getLayerPaint(type: LayerType["type"], color: string) {
14
+ switch (type) {
15
+ case "fill":
16
+ return {
17
+ fillColor: color,
18
+ fillOpacity: 0.25,
19
+ };
20
+ case "line":
21
+ return {
22
+ strokeColor: color,
23
+ strokeWeight: 2,
24
+ };
25
+ default:
26
+ return {};
27
+ }
28
+ }
29
+
30
+ export default function GoogleLayers({ sources }: Props) {
31
+ const [map, setMap] = useState<google.maps.Map | null>(null);
32
+ const [polygonRefs, setPolygonRefs] = useState<google.maps.Polygon[]>([]);
33
+ const mapContext = useContext(GoogleMapsContext);
34
+
35
+ useEffect(() => {
36
+ if (!mapContext) {
37
+ return;
38
+ }
39
+
40
+ setMap(mapContext.map);
41
+ }, [mapContext]);
42
+
43
+ useEffect(() => {
44
+ // Remove existing polygons
45
+ polygonRefs.forEach((polygonRef) => {
46
+ polygonRef?.setMap(null);
47
+ });
48
+
49
+ if (!map || typeof sources === "undefined" || sources.length === 0) {
50
+ return;
51
+ }
52
+
53
+ // Create new polygons
54
+ const newPolygons = sources.map((source) => {
55
+ const polygonOptions = {
56
+ paths: getGooglePolygonFromLocalLogicGeometry(source.geometry),
57
+ };
58
+
59
+ if (Array.isArray(source.layer)) {
60
+ source.layer.forEach((layer) => {
61
+ const layerPaint = getLayerPaint(layer.type, layer.color);
62
+ Object.assign(polygonOptions, layerPaint);
63
+ });
64
+ } else {
65
+ const layerPaint = getLayerPaint(source.layer.type, source.layer.color);
66
+ Object.assign(polygonOptions, layerPaint);
67
+ }
68
+
69
+ const newPolygon = new google.maps.Polygon({
70
+ // Set default options otherwise the polygon remains visible (even if eg: we only want the outline)
71
+ // We then overrider the default options with the layer options
72
+ strokeWeight: 0,
73
+ fillOpacity: 0,
74
+ ...polygonOptions,
75
+ });
76
+ newPolygon.setMap(map);
77
+
78
+ return newPolygon;
79
+ });
80
+
81
+ setPolygonRefs(newPolygons);
82
+ // Set the map for each polygon
83
+ newPolygons.forEach((polygonRef) => {
84
+ polygonRef.setMap(map);
85
+ });
86
+
87
+ // Clean up the polygons when the component unmounts or when sources change
88
+ // eslint-disable-next-line consistent-return
89
+ return () => {
90
+ newPolygons.forEach((polygonRef) => {
91
+ polygonRef?.setMap(null);
92
+ });
93
+ };
94
+ }, [map, sources]);
95
+
96
+ return <></>;
97
+ }
@@ -0,0 +1,74 @@
1
+ import React, { useMemo } from "react";
2
+
3
+ import { Source, Layer } from "react-map-gl/mapbox";
4
+
5
+ import { getFeatureCollectionFromLocalLogicGeometry } from "../utils";
6
+
7
+ import type { Props, Layer as LayerType } from "../types";
8
+
9
+ function getLayerPaint(type: LayerType["type"], color: string) {
10
+ switch (type) {
11
+ case "fill":
12
+ return {
13
+ "fill-color": color,
14
+ "fill-opacity": 0.15,
15
+ };
16
+ case "line":
17
+ return {
18
+ "line-color": color,
19
+ "line-width": 2,
20
+ };
21
+ default:
22
+ return {};
23
+ }
24
+ }
25
+
26
+ export default function MapboxLayers({ sources }: Props) {
27
+ const geometrySources = useMemo(() => {
28
+ if (!Array.isArray(sources) || sources.length === 0) {
29
+ return [];
30
+ }
31
+
32
+ return sources.map((geometry) => ({
33
+ ...geometry,
34
+ geoJson: geometry.geometry
35
+ ? getFeatureCollectionFromLocalLogicGeometry(geometry.geometry)
36
+ : null,
37
+ }));
38
+ }, [sources]);
39
+
40
+ if (geometrySources?.length === 0) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <>
46
+ {geometrySources?.map((source) => (
47
+ <div key={source.key}>
48
+ {source.geoJson && (
49
+ <Source id={source.key} key={source.key} type="geojson" data={source.geoJson}>
50
+ {source.layer &&
51
+ (Array.isArray(source.layer) ? (
52
+ source.layer.map((layer, index) => (
53
+ <Layer
54
+ key={`${source.key}-${index}`}
55
+ {...{
56
+ ...{ type: layer.type, paint: getLayerPaint(layer.type, layer.color) },
57
+ }}
58
+ />
59
+ ))
60
+ ) : (
61
+ <Layer
62
+ {...{
63
+ type: source.layer.type,
64
+ paint: getLayerPaint(source.layer.type, source.layer.color),
65
+ }}
66
+ />
67
+ ))}
68
+ </Source>
69
+ )}
70
+ </div>
71
+ ))}
72
+ </>
73
+ );
74
+ }
@@ -0,0 +1,74 @@
1
+ import React, { useMemo } from "react";
2
+
3
+ import { Source, Layer } from "react-map-gl/maplibre";
4
+
5
+ import { getFeatureCollectionFromLocalLogicGeometry } from "../utils";
6
+
7
+ import type { Props, Layer as LayerType } from "../types";
8
+
9
+ function getLayerPaint(type: LayerType["type"], color: string) {
10
+ switch (type) {
11
+ case "fill":
12
+ return {
13
+ "fill-color": color,
14
+ "fill-opacity": 0.25,
15
+ };
16
+ case "line":
17
+ return {
18
+ "line-color": color,
19
+ "line-width": 2,
20
+ };
21
+ default:
22
+ return {};
23
+ }
24
+ }
25
+
26
+ export default function MaptilerLayers({ sources }: Props) {
27
+ const geometrySources = useMemo(() => {
28
+ if (!Array.isArray(sources) || sources.length === 0) {
29
+ return [];
30
+ }
31
+
32
+ return sources.map((geometry) => ({
33
+ ...geometry,
34
+ geoJson: geometry.geometry
35
+ ? getFeatureCollectionFromLocalLogicGeometry(geometry.geometry)
36
+ : null,
37
+ }));
38
+ }, [sources]);
39
+
40
+ if (geometrySources?.length === 0) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <>
46
+ {geometrySources?.map((source) => (
47
+ <div key={source.key}>
48
+ {source.geoJson && (
49
+ <Source id={source.key} key={source.key} type="geojson" data={source.geoJson}>
50
+ {source.layer &&
51
+ (Array.isArray(source.layer) ? (
52
+ source.layer.map((layer, index) => (
53
+ <Layer
54
+ key={`${source.key}-${index}`}
55
+ {...{
56
+ ...{ type: layer.type, paint: getLayerPaint(layer.type, layer.color) },
57
+ }}
58
+ />
59
+ ))
60
+ ) : (
61
+ <Layer
62
+ {...{
63
+ type: source.layer.type,
64
+ paint: getLayerPaint(source.layer.type, source.layer.color),
65
+ }}
66
+ />
67
+ ))}
68
+ </Source>
69
+ )}
70
+ </div>
71
+ ))}
72
+ </>
73
+ );
74
+ }
@@ -0,0 +1,32 @@
1
+ import React, { lazy, useMemo } from "react";
2
+
3
+ import type { Props } from "./types";
4
+
5
+ import { useRootElement } from "../context";
6
+
7
+ const MaptilerLayers = lazy(() => import("./Maptiler"));
8
+ const GoogleLayers = lazy(() => import("./Google"));
9
+ const MapboxLayers = lazy(() => import("./Mapbox"));
10
+
11
+ export function Layers(props: Props) {
12
+ const { mapProvider } = useRootElement();
13
+
14
+ const BaseLayers = useMemo(() => {
15
+ switch (mapProvider?.name) {
16
+ case "maptiler":
17
+ return MaptilerLayers;
18
+ case "google":
19
+ return GoogleLayers;
20
+ case "mapbox":
21
+ return MapboxLayers;
22
+ default:
23
+ return null;
24
+ }
25
+ }, [mapProvider]);
26
+
27
+ if (!BaseLayers) {
28
+ return null;
29
+ }
30
+
31
+ return <BaseLayers {...props} />;
32
+ }
@@ -0,0 +1,15 @@
1
+ export type Layer = {
2
+ type: "fill" | "line";
3
+ color: string;
4
+ };
5
+
6
+ export type Source = {
7
+ key: string;
8
+ layer: Layer | Layer[];
9
+ // Expected [lat, lng] format
10
+ geometry: number[][][][];
11
+ };
12
+
13
+ export type Props = {
14
+ sources?: Source[];
15
+ };
@@ -0,0 +1,64 @@
1
+ import type { FeatureCollection, MultiPolygon } from "geojson";
2
+
3
+ type Geometry =
4
+ | number[][]
5
+ | number[][][]
6
+ | number[][][][]
7
+ | string[][]
8
+ | string[][][]
9
+ | string[][][][];
10
+
11
+ export function getFeatureCollectionFromLocalLogicGeometry(geometry: Geometry = []) {
12
+ /* Geometries need formatting before being added as a Source in the Map.
13
+ * Geometries from v3/schools have a format of [lat, lng]
14
+ * Geojson accepts polygons with a format of [lng, lat]
15
+ */
16
+
17
+ const geometryLatLngFormat = (arr: object): object =>
18
+ Object.values(arr).map((a) => {
19
+ if (Array.isArray(a[0])) {
20
+ return geometryLatLngFormat(a);
21
+ }
22
+
23
+ return [a[1], a[0]];
24
+ });
25
+
26
+ const finalCoordinates = geometryLatLngFormat(geometry);
27
+
28
+ const responseObject = {
29
+ type: "FeatureCollection",
30
+ features: [
31
+ {
32
+ type: "Feature",
33
+ geometry: {
34
+ type: "MultiPolygon",
35
+ coordinates: finalCoordinates,
36
+ },
37
+ },
38
+ ],
39
+ } as FeatureCollection<MultiPolygon>;
40
+
41
+ return responseObject;
42
+ }
43
+
44
+ export function getGooglePolygonFromLocalLogicGeometry(geometry: Geometry = []) {
45
+ /* Geometries need formatting before being added as a Source in the Map.
46
+ * Geometries from v3/schools have a format of [lat, lng]
47
+ * Google Maps accepts polygons with a format of { lat: number, lng: number }
48
+ */
49
+
50
+ const geometryLatLngFormat = (arr: object): object =>
51
+ Object.values(arr).map((a) => {
52
+ if (Array.isArray(a[0])) {
53
+ return geometryLatLngFormat(a.flat());
54
+ }
55
+
56
+ return { lat: a[0], lng: a[1] };
57
+ });
58
+
59
+ const finalCoordinates = geometryLatLngFormat(
60
+ geometry
61
+ ) as google.maps.MVCArray<google.maps.LatLng>;
62
+
63
+ return finalCoordinates;
64
+ }
@@ -7,9 +7,10 @@ import * as styles from "./styles";
7
7
  export function Cluster({ cluster }: { cluster: ClusterPoint }) {
8
8
  // Taking a somewhat similar calculation to what LocalLogic SDK used to
9
9
  // do for cluster diameter (bigger depending on count)
10
- const diameter = useMemo(() => (
11
- 20 + cluster.properties.point_count * 4
12
- ), [cluster.properties.point_count]);
10
+ const diameter = useMemo(
11
+ () => 20 + cluster.properties.point_count * 4,
12
+ [cluster.properties.point_count]
13
+ );
13
14
 
14
15
  return (
15
16
  <div
@@ -17,10 +18,10 @@ export function Cluster({ cluster }: { cluster: ClusterPoint }) {
17
18
  style={{
18
19
  width: `${diameter}px`,
19
20
  height: `${diameter}px`,
20
- fontSize: `${diameter / 2.5}px`
21
+ fontSize: `${diameter / 2.5}px`,
21
22
  }}
22
23
  >
23
24
  {cluster.properties.point_count}
24
25
  </div>
25
- )
26
- }
26
+ );
27
+ }
@@ -1,14 +1,11 @@
1
1
  export const container = `
2
- rounded-full
3
- bg-primary-100
4
- text-base-white
5
- flex
6
- justify-center
7
- items-center
8
- leading-none
9
- font-sans
10
- border-2
11
- border-solid
12
- border-base-white
13
- shadow-sm
2
+ rounded-full leading-none font-sans border-2
3
+ border-solid shadow-sm
4
+ flex justify-center items-center
5
+ bg-primary-100 text-base-white border-base-white
6
+ transition-all
7
+
8
+ hover:bg-base-white
9
+ hover:text-primary-100
10
+ hover:border-primary-100
14
11
  `;
@@ -8,15 +8,14 @@ import type { MapMarkerProps } from "../types";
8
8
  import { Cluster } from "../Cluster";
9
9
 
10
10
  import * as globalStyles from "../styles";
11
- import * as styles from "./styles";
12
11
 
13
- export default function GoogleMarkers({ clusters, children }: MapMarkerProps) {
14
- return clusters.map((cluster, index) => {
12
+ export default function GoogleMarkers({ clusters, onClick, children }: MapMarkerProps) {
13
+ return clusters?.map((cluster, index) => {
15
14
  const longitude = cluster.geometry.coordinates[0];
16
15
  const latitude = cluster.geometry.coordinates[1];
17
16
 
18
17
  const handleOnClick = () => {
19
- // TODO Without the on click the hover doesn't work
18
+ onClick?.(cluster);
20
19
  };
21
20
 
22
21
  return (
@@ -27,18 +26,19 @@ export default function GoogleMarkers({ clusters, children }: MapMarkerProps) {
27
26
  lng: longitude,
28
27
  }}
29
28
  onClick={handleOnClick}
30
- className={styles.marker}
31
29
  >
32
- <div className={globalStyles.container}>
30
+ <div className={globalStyles.container} data-is-cluster={!!cluster.properties.cluster}>
33
31
  {cluster.properties.cluster ? (
34
32
  <Cluster cluster={cluster} />
35
33
  ) : (
36
34
  <>
37
- {cluster.properties.icon ? (
38
- <cluster.properties.icon className={globalStyles.icon} />
39
- ) : (
40
- <MapPin className={globalStyles.icon} />
41
- )}
35
+ <div className={globalStyles.iconWrapper}>
36
+ {cluster.properties.icon ? (
37
+ <cluster.properties.icon className={globalStyles.icon} />
38
+ ) : (
39
+ <MapPin className={globalStyles.icon} />
40
+ )}
41
+ </div>
42
42
  {children}
43
43
  </>
44
44
  )}
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
3
  import { MapPin } from "@phosphor-icons/react";
4
- import { Marker as MapboxMarker } from "react-map-gl/mapbox";
4
+ import { Marker as MapboxMarker, type MarkerEvent } from "react-map-gl/mapbox";
5
5
 
6
6
  import type { MapMarkerProps } from "../types";
7
7
 
@@ -9,28 +9,39 @@ import { Cluster } from "../Cluster";
9
9
 
10
10
  import * as styles from "../styles";
11
11
 
12
- export default function MapboxMarkers({ clusters, children }: MapMarkerProps) {
13
- return clusters.map((cluster, index) => {
12
+ export default function MapboxMarkers({ clusters, onClick, children }: MapMarkerProps) {
13
+ return clusters?.map((cluster, index) => {
14
14
  const longitude = cluster.geometry.coordinates[0];
15
15
  const latitude = cluster.geometry.coordinates[1];
16
16
 
17
+ const handleOnClick = (event: MarkerEvent<MouseEvent>) => {
18
+ // If we let the click event propagates to the map, it will immediately close the popup
19
+ // with `closeOnClick: true`
20
+ // https://github.com/visgl/react-map-gl/blob/bdfcfd2d6cfdc641cddb5337d7ed477e5c3c28d5/examples/controls/src/app.tsx#L31-36
21
+ event.originalEvent.stopPropagation();
22
+ onClick?.(cluster);
23
+ };
24
+
17
25
  return (
18
26
  <MapboxMarker
19
27
  key={`map-marker-${longitude}-${latitude}-${index}`}
20
28
  anchor="bottom"
21
29
  longitude={longitude}
22
30
  latitude={latitude}
31
+ onClick={handleOnClick}
23
32
  >
24
- <div className={styles.container}>
33
+ <div className={styles.container} data-is-cluster={!!cluster.properties.cluster}>
25
34
  {cluster.properties.cluster ? (
26
35
  <Cluster cluster={cluster} />
27
36
  ) : (
28
37
  <>
29
- {cluster.properties.icon ? (
30
- <cluster.properties.icon className={styles.icon} />
31
- ) : (
32
- <MapPin className={styles.icon} />
33
- )}
38
+ <div className={styles.iconWrapper}>
39
+ {cluster.properties.icon ? (
40
+ <cluster.properties.icon className={styles.icon} />
41
+ ) : (
42
+ <MapPin className={styles.icon} />
43
+ )}
44
+ </div>
34
45
  {children}
35
46
  </>
36
47
  )}
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
3
  import { MapPin } from "@phosphor-icons/react";
4
- import { Marker as MaplibreMarker } from "react-map-gl/maplibre";
4
+ import { Marker as MaplibreMarker, type MarkerEvent } from "react-map-gl/maplibre";
5
5
 
6
6
  import type { MapMarkerProps } from "../types";
7
7
 
@@ -9,31 +9,39 @@ import { Cluster } from "../Cluster";
9
9
 
10
10
  import * as styles from "../styles";
11
11
 
12
- export default function MaptilerMarkers({
13
- clusters,
14
- children,
15
- }: MapMarkerProps) {
12
+ export default function MaptilerMarkers({ clusters, onClick, children }: MapMarkerProps) {
16
13
  return clusters?.map((cluster, index) => {
17
14
  const longitude = cluster.geometry.coordinates[0];
18
15
  const latitude = cluster.geometry.coordinates[1];
19
16
 
17
+ const handleOnClick = (event: MarkerEvent<MouseEvent>) => {
18
+ // If we let the click event propagates to the map, it will immediately close the popup
19
+ // with `closeOnClick: true`
20
+ // https://github.com/visgl/react-map-gl/blob/bdfcfd2d6cfdc641cddb5337d7ed477e5c3c28d5/examples/controls/src/app.tsx#L31-36
21
+ event.originalEvent.stopPropagation();
22
+ onClick?.(cluster);
23
+ };
24
+
20
25
  return (
21
26
  <MaplibreMarker
22
27
  key={`map-marker-${longitude}-${latitude}-${index}`}
23
28
  anchor="bottom"
24
29
  longitude={longitude}
25
30
  latitude={latitude}
31
+ onClick={handleOnClick}
26
32
  >
27
- <div className={styles.container}>
33
+ <div className={styles.container} data-is-cluster={!!cluster.properties.cluster}>
28
34
  {cluster.properties.cluster ? (
29
35
  <Cluster cluster={cluster} />
30
36
  ) : (
31
37
  <>
32
- {cluster.properties.icon ? (
33
- <cluster.properties.icon className={styles.icon} />
34
- ) : (
35
- <MapPin className={styles.icon} />
36
- )}
38
+ <div className={styles.iconWrapper}>
39
+ {cluster.properties.icon ? (
40
+ <cluster.properties.icon className={styles.icon} />
41
+ ) : (
42
+ <MapPin className={styles.icon} />
43
+ )}
44
+ </div>
37
45
  {children}
38
46
  </>
39
47
  )}