@page-speed/maps 0.1.9 → 0.2.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as React3 from 'react';
2
- import React3__default from 'react';
2
+ import React3__default, { useMemo } from 'react';
3
3
  import { Marker, Map as Map$1, GeolocateControl, NavigationControl } from 'react-map-gl/maplibre';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
@@ -518,12 +518,93 @@ function MapLibre({
518
518
  }
519
519
  );
520
520
  }
521
+ var TILE_SIZE = 512;
522
+ function latToMercatorY(lat) {
523
+ const latRad = lat * Math.PI / 180;
524
+ const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
525
+ return TILE_SIZE / (2 * Math.PI) * (Math.PI - mercN);
526
+ }
527
+ function lngToMercatorX(lng) {
528
+ return TILE_SIZE / (2 * Math.PI) * ((lng + 180) / 360 * 2 * Math.PI);
529
+ }
530
+ function computeDefaultZoom(options) {
531
+ const {
532
+ coordinates,
533
+ mapWidth,
534
+ mapHeight,
535
+ padding = 50,
536
+ maxZoom = 18,
537
+ minZoom = 1
538
+ } = options;
539
+ if (coordinates.length === 0) return null;
540
+ if (coordinates.length === 1) return maxZoom;
541
+ if (mapWidth <= 0 || mapHeight <= 0) return null;
542
+ let minLat = Infinity;
543
+ let maxLat = -Infinity;
544
+ let minLng = Infinity;
545
+ let maxLng = -Infinity;
546
+ for (const coord of coordinates) {
547
+ if (coord.lat < minLat) minLat = coord.lat;
548
+ if (coord.lat > maxLat) maxLat = coord.lat;
549
+ if (coord.lng < minLng) minLng = coord.lng;
550
+ if (coord.lng > maxLng) maxLng = coord.lng;
551
+ }
552
+ const pixelXMin = lngToMercatorX(minLng);
553
+ const pixelXMax = lngToMercatorX(maxLng);
554
+ const pixelYMin = latToMercatorY(maxLat);
555
+ const pixelYMax = latToMercatorY(minLat);
556
+ const dx = Math.abs(pixelXMax - pixelXMin);
557
+ const dy = Math.abs(pixelYMax - pixelYMin);
558
+ const availableWidth = mapWidth - padding * 2;
559
+ const availableHeight = mapHeight - padding * 2;
560
+ if (availableWidth <= 0 || availableHeight <= 0) return minZoom;
561
+ let zoom;
562
+ if (dx === 0 && dy === 0) {
563
+ return maxZoom;
564
+ } else if (dx === 0) {
565
+ zoom = Math.log2(availableHeight / dy);
566
+ } else if (dy === 0) {
567
+ zoom = Math.log2(availableWidth / dx);
568
+ } else {
569
+ const zoomX = Math.log2(availableWidth / dx);
570
+ const zoomY = Math.log2(availableHeight / dy);
571
+ zoom = Math.min(zoomX, zoomY);
572
+ }
573
+ return Math.max(minZoom, Math.min(maxZoom, Math.floor(zoom * 100) / 100));
574
+ }
575
+ function useDefaultZoom(options) {
576
+ const { coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom } = options;
577
+ return useMemo(
578
+ () => computeDefaultZoom({
579
+ coordinates,
580
+ mapWidth,
581
+ mapHeight,
582
+ padding,
583
+ maxZoom,
584
+ minZoom
585
+ }),
586
+ [coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom]
587
+ );
588
+ }
521
589
  var PANEL_POSITION_CLASS = {
522
590
  "top-left": "left-4 top-4",
523
591
  "top-right": "right-4 top-4",
524
592
  "bottom-left": "bottom-4 left-4",
525
593
  "bottom-right": "bottom-4 right-4"
526
594
  };
595
+ function getOptimalPanelPosition(markerLat, markerLng, mapCenter) {
596
+ const isNorth = markerLat >= mapCenter.latitude;
597
+ const isEast = markerLng >= mapCenter.longitude;
598
+ if (isNorth && isEast) {
599
+ return "bottom-left";
600
+ } else if (isNorth && !isEast) {
601
+ return "bottom-right";
602
+ } else if (!isNorth && isEast) {
603
+ return "top-left";
604
+ } else {
605
+ return "top-right";
606
+ }
607
+ }
527
608
  var DEFAULT_VIEW_STATE = {
528
609
  latitude: 39.5,
529
610
  longitude: -98.35,
@@ -766,6 +847,13 @@ function GeoMap({
766
847
  IconComponent = FallbackIcon,
767
848
  ImgComponent = FallbackImg
768
849
  }) {
850
+ const containerRef = React3.useRef(null);
851
+ const [containerDimensions, setContainerDimensions] = React3.useState({
852
+ width: 800,
853
+ // Default width
854
+ height: 520
855
+ // Default height
856
+ });
769
857
  const [isMobile, setIsMobile] = React3.useState(false);
770
858
  React3.useEffect(() => {
771
859
  const checkMobile = () => {
@@ -781,6 +869,24 @@ function GeoMap({
781
869
  }
782
870
  return isMobile ? 420 : 520;
783
871
  }, [mapSize, isMobile]);
872
+ React3.useEffect(() => {
873
+ if (!containerRef.current) return;
874
+ const updateDimensions = () => {
875
+ if (containerRef.current) {
876
+ const rect = containerRef.current.getBoundingClientRect();
877
+ setContainerDimensions({
878
+ width: rect.width || 800,
879
+ height: rect.height || calculatedHeight
880
+ });
881
+ }
882
+ };
883
+ updateDimensions();
884
+ if (typeof ResizeObserver !== "undefined") {
885
+ const resizeObserver = new ResizeObserver(updateDimensions);
886
+ resizeObserver.observe(containerRef.current);
887
+ return () => resizeObserver.disconnect();
888
+ }
889
+ }, [calculatedHeight]);
784
890
  const normalizedStandaloneMarkers = React3.useMemo(
785
891
  () => markers.map((marker, index) => ({
786
892
  ...marker,
@@ -864,47 +970,49 @@ function GeoMap({
864
970
  longitude: DEFAULT_VIEW_STATE.longitude
865
971
  };
866
972
  }, [normalizedClusters, normalizedStandaloneMarkers]);
867
- const calculatedZoom = React3.useMemo(() => {
868
- if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
869
- return markerFocusZoom;
870
- }
871
- const allCoords = [];
973
+ const zoomCoordinates = React3.useMemo(() => {
974
+ const coords = [];
872
975
  normalizedStandaloneMarkers.forEach((marker) => {
873
- allCoords.push({
874
- latitude: marker.latitude,
875
- longitude: marker.longitude
976
+ coords.push({
977
+ lat: marker.latitude,
978
+ lng: marker.longitude
876
979
  });
877
980
  });
878
981
  normalizedClusters.forEach((cluster) => {
879
- allCoords.push({
880
- latitude: cluster.latitude,
881
- longitude: cluster.longitude
982
+ coords.push({
983
+ lat: cluster.latitude,
984
+ lng: cluster.longitude
882
985
  });
883
986
  });
884
- if (allCoords.length === 0) {
987
+ return coords;
988
+ }, [normalizedStandaloneMarkers, normalizedClusters]);
989
+ const properZoom = useDefaultZoom({
990
+ coordinates: zoomCoordinates,
991
+ mapWidth: containerDimensions.width,
992
+ mapHeight: containerDimensions.height,
993
+ padding: 80,
994
+ // Increased padding for better framing
995
+ maxZoom: 18,
996
+ minZoom: 1
997
+ });
998
+ const calculatedZoom = React3.useMemo(() => {
999
+ if (zoomCoordinates.length === 1) {
1000
+ return markerFocusZoom;
1001
+ }
1002
+ if (zoomCoordinates.length === 0) {
885
1003
  return DEFAULT_VIEW_STATE.zoom;
886
1004
  }
887
- const lats = allCoords.map((c) => c.latitude);
888
- const lngs = allCoords.map((c) => c.longitude);
889
- const latDiff = Math.max(...lats) - Math.min(...lats);
890
- const lngDiff = Math.max(...lngs) - Math.min(...lngs);
891
- const maxDiff = Math.max(latDiff, lngDiff);
892
- const heightFactor = calculatedHeight / 520;
893
- const paddingFactor = 0.85;
894
- if (maxDiff > 10) return Math.max(2, 3 * heightFactor * paddingFactor);
895
- if (maxDiff > 5) return Math.max(4, 5 * heightFactor * paddingFactor);
896
- if (maxDiff > 2) return Math.max(6, 7 * heightFactor * paddingFactor);
897
- if (maxDiff > 1) return Math.max(8, 9 * heightFactor * paddingFactor);
898
- if (maxDiff > 0.5) return Math.max(9, 10 * heightFactor * paddingFactor);
899
- if (maxDiff > 0.1) return Math.max(11, 12 * heightFactor * paddingFactor);
900
- if (maxDiff > 0.01) return Math.max(12, 13 * heightFactor * paddingFactor);
901
- return Math.max(11, 12 * heightFactor * paddingFactor);
902
- }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom, calculatedHeight]);
1005
+ const adjustedZoom = properZoom ? properZoom - 0.5 : DEFAULT_VIEW_STATE.zoom;
1006
+ return Math.min(adjustedZoom, 15);
1007
+ }, [properZoom, zoomCoordinates.length, markerFocusZoom]);
903
1008
  const [uncontrolledViewState, setUncontrolledViewState] = React3.useState({
904
1009
  latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
905
1010
  longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
906
1011
  zoom: defaultViewState?.zoom ?? calculatedZoom
907
1012
  });
1013
+ const [dynamicPanelPosition, setDynamicPanelPosition] = React3.useState(
1014
+ panelPosition
1015
+ );
908
1016
  React3.useEffect(() => {
909
1017
  if (!viewState && !defaultViewState) {
910
1018
  setUncontrolledViewState({
@@ -984,6 +1092,19 @@ function GeoMap({
984
1092
  markerId: marker.id,
985
1093
  clusterId: marker.clusterId
986
1094
  });
1095
+ const currentCenter = resolvedViewState || {
1096
+ latitude: firstCoordinate.latitude,
1097
+ longitude: firstCoordinate.longitude
1098
+ };
1099
+ const optimalPosition = getOptimalPanelPosition(
1100
+ marker.latitude,
1101
+ marker.longitude,
1102
+ {
1103
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1104
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1105
+ }
1106
+ );
1107
+ setDynamicPanelPosition(optimalPosition);
987
1108
  applyViewState({
988
1109
  latitude: marker.latitude,
989
1110
  longitude: marker.longitude,
@@ -991,7 +1112,7 @@ function GeoMap({
991
1112
  });
992
1113
  emitSelectionChange({ type: "marker", marker });
993
1114
  },
994
- [applyViewState, emitSelectionChange, markerFocusZoom]
1115
+ [applyViewState, emitSelectionChange, markerFocusZoom, resolvedViewState, firstCoordinate]
995
1116
  );
996
1117
  const selectCluster = React3.useCallback(
997
1118
  (cluster) => {
@@ -999,6 +1120,19 @@ function GeoMap({
999
1120
  type: "cluster",
1000
1121
  clusterId: cluster.id
1001
1122
  });
1123
+ const currentCenter = resolvedViewState || {
1124
+ latitude: firstCoordinate.latitude,
1125
+ longitude: firstCoordinate.longitude
1126
+ };
1127
+ const optimalPosition = getOptimalPanelPosition(
1128
+ cluster.latitude,
1129
+ cluster.longitude,
1130
+ {
1131
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1132
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1133
+ }
1134
+ );
1135
+ setDynamicPanelPosition(optimalPosition);
1002
1136
  applyViewState({
1003
1137
  latitude: cluster.latitude,
1004
1138
  longitude: cluster.longitude,
@@ -1006,7 +1140,7 @@ function GeoMap({
1006
1140
  });
1007
1141
  emitSelectionChange({ type: "cluster", cluster });
1008
1142
  },
1009
- [applyViewState, clusterFocusZoom, emitSelectionChange]
1143
+ [applyViewState, clusterFocusZoom, emitSelectionChange, resolvedViewState, firstCoordinate]
1010
1144
  );
1011
1145
  const clearSelection = React3.useCallback(() => {
1012
1146
  setSelection({ type: "none" });
@@ -1225,6 +1359,7 @@ function GeoMap({
1225
1359
  return /* @__PURE__ */ jsxs(
1226
1360
  "div",
1227
1361
  {
1362
+ ref: containerRef,
1228
1363
  className: cn(
1229
1364
  "relative rounded-2xl border border-border bg-background",
1230
1365
  // Remove overflow-hidden from outer container to allow panel to overflow
@@ -1292,7 +1427,7 @@ function GeoMap({
1292
1427
  {
1293
1428
  className: cn(
1294
1429
  "pointer-events-none absolute z-30",
1295
- PANEL_POSITION_CLASS[panelPosition]
1430
+ PANEL_POSITION_CLASS[dynamicPanelPosition]
1296
1431
  ),
1297
1432
  style: {
1298
1433
  // Ensure panel can overflow and has higher z-index