@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.
@@ -658,12 +658,93 @@ function MapLibre({
658
658
  }
659
659
  );
660
660
  }
661
+ var TILE_SIZE = 512;
662
+ function latToMercatorY(lat) {
663
+ const latRad = lat * Math.PI / 180;
664
+ const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
665
+ return TILE_SIZE / (2 * Math.PI) * (Math.PI - mercN);
666
+ }
667
+ function lngToMercatorX(lng) {
668
+ return TILE_SIZE / (2 * Math.PI) * ((lng + 180) / 360 * 2 * Math.PI);
669
+ }
670
+ function computeDefaultZoom(options) {
671
+ const {
672
+ coordinates,
673
+ mapWidth,
674
+ mapHeight,
675
+ padding = 50,
676
+ maxZoom = 18,
677
+ minZoom = 1
678
+ } = options;
679
+ if (coordinates.length === 0) return null;
680
+ if (coordinates.length === 1) return maxZoom;
681
+ if (mapWidth <= 0 || mapHeight <= 0) return null;
682
+ let minLat = Infinity;
683
+ let maxLat = -Infinity;
684
+ let minLng = Infinity;
685
+ let maxLng = -Infinity;
686
+ for (const coord of coordinates) {
687
+ if (coord.lat < minLat) minLat = coord.lat;
688
+ if (coord.lat > maxLat) maxLat = coord.lat;
689
+ if (coord.lng < minLng) minLng = coord.lng;
690
+ if (coord.lng > maxLng) maxLng = coord.lng;
691
+ }
692
+ const pixelXMin = lngToMercatorX(minLng);
693
+ const pixelXMax = lngToMercatorX(maxLng);
694
+ const pixelYMin = latToMercatorY(maxLat);
695
+ const pixelYMax = latToMercatorY(minLat);
696
+ const dx = Math.abs(pixelXMax - pixelXMin);
697
+ const dy = Math.abs(pixelYMax - pixelYMin);
698
+ const availableWidth = mapWidth - padding * 2;
699
+ const availableHeight = mapHeight - padding * 2;
700
+ if (availableWidth <= 0 || availableHeight <= 0) return minZoom;
701
+ let zoom;
702
+ if (dx === 0 && dy === 0) {
703
+ return maxZoom;
704
+ } else if (dx === 0) {
705
+ zoom = Math.log2(availableHeight / dy);
706
+ } else if (dy === 0) {
707
+ zoom = Math.log2(availableWidth / dx);
708
+ } else {
709
+ const zoomX = Math.log2(availableWidth / dx);
710
+ const zoomY = Math.log2(availableHeight / dy);
711
+ zoom = Math.min(zoomX, zoomY);
712
+ }
713
+ return Math.max(minZoom, Math.min(maxZoom, Math.floor(zoom * 100) / 100));
714
+ }
715
+ function useDefaultZoom(options) {
716
+ const { coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom } = options;
717
+ return React3.useMemo(
718
+ () => computeDefaultZoom({
719
+ coordinates,
720
+ mapWidth,
721
+ mapHeight,
722
+ padding,
723
+ maxZoom,
724
+ minZoom
725
+ }),
726
+ [coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom]
727
+ );
728
+ }
661
729
  var PANEL_POSITION_CLASS = {
662
730
  "top-left": "left-4 top-4",
663
731
  "top-right": "right-4 top-4",
664
732
  "bottom-left": "bottom-4 left-4",
665
733
  "bottom-right": "bottom-4 right-4"
666
734
  };
735
+ function getOptimalPanelPosition(markerLat, markerLng, mapCenter) {
736
+ const isNorth = markerLat >= mapCenter.latitude;
737
+ const isEast = markerLng >= mapCenter.longitude;
738
+ if (isNorth && isEast) {
739
+ return "bottom-left";
740
+ } else if (isNorth && !isEast) {
741
+ return "bottom-right";
742
+ } else if (!isNorth && isEast) {
743
+ return "top-left";
744
+ } else {
745
+ return "top-right";
746
+ }
747
+ }
667
748
  var DEFAULT_VIEW_STATE = {
668
749
  latitude: 39.5,
669
750
  longitude: -98.35,
@@ -906,6 +987,13 @@ function GeoMap({
906
987
  IconComponent = FallbackIcon,
907
988
  ImgComponent = FallbackImg
908
989
  }) {
990
+ const containerRef = React3__namespace.useRef(null);
991
+ const [containerDimensions, setContainerDimensions] = React3__namespace.useState({
992
+ width: 800,
993
+ // Default width
994
+ height: 520
995
+ // Default height
996
+ });
909
997
  const [isMobile, setIsMobile] = React3__namespace.useState(false);
910
998
  React3__namespace.useEffect(() => {
911
999
  const checkMobile = () => {
@@ -921,6 +1009,24 @@ function GeoMap({
921
1009
  }
922
1010
  return isMobile ? 420 : 520;
923
1011
  }, [mapSize, isMobile]);
1012
+ React3__namespace.useEffect(() => {
1013
+ if (!containerRef.current) return;
1014
+ const updateDimensions = () => {
1015
+ if (containerRef.current) {
1016
+ const rect = containerRef.current.getBoundingClientRect();
1017
+ setContainerDimensions({
1018
+ width: rect.width || 800,
1019
+ height: rect.height || calculatedHeight
1020
+ });
1021
+ }
1022
+ };
1023
+ updateDimensions();
1024
+ if (typeof ResizeObserver !== "undefined") {
1025
+ const resizeObserver = new ResizeObserver(updateDimensions);
1026
+ resizeObserver.observe(containerRef.current);
1027
+ return () => resizeObserver.disconnect();
1028
+ }
1029
+ }, [calculatedHeight]);
924
1030
  const normalizedStandaloneMarkers = React3__namespace.useMemo(
925
1031
  () => markers.map((marker, index) => ({
926
1032
  ...marker,
@@ -1004,47 +1110,49 @@ function GeoMap({
1004
1110
  longitude: DEFAULT_VIEW_STATE.longitude
1005
1111
  };
1006
1112
  }, [normalizedClusters, normalizedStandaloneMarkers]);
1007
- const calculatedZoom = React3__namespace.useMemo(() => {
1008
- if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
1009
- return markerFocusZoom;
1010
- }
1011
- const allCoords = [];
1113
+ const zoomCoordinates = React3__namespace.useMemo(() => {
1114
+ const coords = [];
1012
1115
  normalizedStandaloneMarkers.forEach((marker) => {
1013
- allCoords.push({
1014
- latitude: marker.latitude,
1015
- longitude: marker.longitude
1116
+ coords.push({
1117
+ lat: marker.latitude,
1118
+ lng: marker.longitude
1016
1119
  });
1017
1120
  });
1018
1121
  normalizedClusters.forEach((cluster) => {
1019
- allCoords.push({
1020
- latitude: cluster.latitude,
1021
- longitude: cluster.longitude
1122
+ coords.push({
1123
+ lat: cluster.latitude,
1124
+ lng: cluster.longitude
1022
1125
  });
1023
1126
  });
1024
- if (allCoords.length === 0) {
1127
+ return coords;
1128
+ }, [normalizedStandaloneMarkers, normalizedClusters]);
1129
+ const properZoom = useDefaultZoom({
1130
+ coordinates: zoomCoordinates,
1131
+ mapWidth: containerDimensions.width,
1132
+ mapHeight: containerDimensions.height,
1133
+ padding: 80,
1134
+ // Increased padding for better framing
1135
+ maxZoom: 18,
1136
+ minZoom: 1
1137
+ });
1138
+ const calculatedZoom = React3__namespace.useMemo(() => {
1139
+ if (zoomCoordinates.length === 1) {
1140
+ return markerFocusZoom;
1141
+ }
1142
+ if (zoomCoordinates.length === 0) {
1025
1143
  return DEFAULT_VIEW_STATE.zoom;
1026
1144
  }
1027
- const lats = allCoords.map((c) => c.latitude);
1028
- const lngs = allCoords.map((c) => c.longitude);
1029
- const latDiff = Math.max(...lats) - Math.min(...lats);
1030
- const lngDiff = Math.max(...lngs) - Math.min(...lngs);
1031
- const maxDiff = Math.max(latDiff, lngDiff);
1032
- const heightFactor = calculatedHeight / 520;
1033
- const paddingFactor = 0.85;
1034
- if (maxDiff > 10) return Math.max(2, 3 * heightFactor * paddingFactor);
1035
- if (maxDiff > 5) return Math.max(4, 5 * heightFactor * paddingFactor);
1036
- if (maxDiff > 2) return Math.max(6, 7 * heightFactor * paddingFactor);
1037
- if (maxDiff > 1) return Math.max(8, 9 * heightFactor * paddingFactor);
1038
- if (maxDiff > 0.5) return Math.max(9, 10 * heightFactor * paddingFactor);
1039
- if (maxDiff > 0.1) return Math.max(11, 12 * heightFactor * paddingFactor);
1040
- if (maxDiff > 0.01) return Math.max(12, 13 * heightFactor * paddingFactor);
1041
- return Math.max(11, 12 * heightFactor * paddingFactor);
1042
- }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom, calculatedHeight]);
1145
+ const adjustedZoom = properZoom ? properZoom - 0.5 : DEFAULT_VIEW_STATE.zoom;
1146
+ return Math.min(adjustedZoom, 15);
1147
+ }, [properZoom, zoomCoordinates.length, markerFocusZoom]);
1043
1148
  const [uncontrolledViewState, setUncontrolledViewState] = React3__namespace.useState({
1044
1149
  latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
1045
1150
  longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
1046
1151
  zoom: defaultViewState?.zoom ?? calculatedZoom
1047
1152
  });
1153
+ const [dynamicPanelPosition, setDynamicPanelPosition] = React3__namespace.useState(
1154
+ panelPosition
1155
+ );
1048
1156
  React3__namespace.useEffect(() => {
1049
1157
  if (!viewState && !defaultViewState) {
1050
1158
  setUncontrolledViewState({
@@ -1124,6 +1232,19 @@ function GeoMap({
1124
1232
  markerId: marker.id,
1125
1233
  clusterId: marker.clusterId
1126
1234
  });
1235
+ const currentCenter = resolvedViewState || {
1236
+ latitude: firstCoordinate.latitude,
1237
+ longitude: firstCoordinate.longitude
1238
+ };
1239
+ const optimalPosition = getOptimalPanelPosition(
1240
+ marker.latitude,
1241
+ marker.longitude,
1242
+ {
1243
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1244
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1245
+ }
1246
+ );
1247
+ setDynamicPanelPosition(optimalPosition);
1127
1248
  applyViewState({
1128
1249
  latitude: marker.latitude,
1129
1250
  longitude: marker.longitude,
@@ -1131,7 +1252,7 @@ function GeoMap({
1131
1252
  });
1132
1253
  emitSelectionChange({ type: "marker", marker });
1133
1254
  },
1134
- [applyViewState, emitSelectionChange, markerFocusZoom]
1255
+ [applyViewState, emitSelectionChange, markerFocusZoom, resolvedViewState, firstCoordinate]
1135
1256
  );
1136
1257
  const selectCluster = React3__namespace.useCallback(
1137
1258
  (cluster) => {
@@ -1139,6 +1260,19 @@ function GeoMap({
1139
1260
  type: "cluster",
1140
1261
  clusterId: cluster.id
1141
1262
  });
1263
+ const currentCenter = resolvedViewState || {
1264
+ latitude: firstCoordinate.latitude,
1265
+ longitude: firstCoordinate.longitude
1266
+ };
1267
+ const optimalPosition = getOptimalPanelPosition(
1268
+ cluster.latitude,
1269
+ cluster.longitude,
1270
+ {
1271
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1272
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1273
+ }
1274
+ );
1275
+ setDynamicPanelPosition(optimalPosition);
1142
1276
  applyViewState({
1143
1277
  latitude: cluster.latitude,
1144
1278
  longitude: cluster.longitude,
@@ -1146,7 +1280,7 @@ function GeoMap({
1146
1280
  });
1147
1281
  emitSelectionChange({ type: "cluster", cluster });
1148
1282
  },
1149
- [applyViewState, clusterFocusZoom, emitSelectionChange]
1283
+ [applyViewState, clusterFocusZoom, emitSelectionChange, resolvedViewState, firstCoordinate]
1150
1284
  );
1151
1285
  const clearSelection = React3__namespace.useCallback(() => {
1152
1286
  setSelection({ type: "none" });
@@ -1365,6 +1499,7 @@ function GeoMap({
1365
1499
  return /* @__PURE__ */ jsxRuntime.jsxs(
1366
1500
  "div",
1367
1501
  {
1502
+ ref: containerRef,
1368
1503
  className: cn(
1369
1504
  "relative rounded-2xl border border-border bg-background",
1370
1505
  // Remove overflow-hidden from outer container to allow panel to overflow
@@ -1432,7 +1567,7 @@ function GeoMap({
1432
1567
  {
1433
1568
  className: cn(
1434
1569
  "pointer-events-none absolute z-30",
1435
- PANEL_POSITION_CLASS[panelPosition]
1570
+ PANEL_POSITION_CLASS[dynamicPanelPosition]
1436
1571
  ),
1437
1572
  style: {
1438
1573
  // Ensure panel can overflow and has higher z-index