@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.
@@ -539,12 +539,93 @@ function MapLibre({
539
539
  }
540
540
  );
541
541
  }
542
+ var TILE_SIZE = 512;
543
+ function latToMercatorY(lat) {
544
+ const latRad = lat * Math.PI / 180;
545
+ const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
546
+ return TILE_SIZE / (2 * Math.PI) * (Math.PI - mercN);
547
+ }
548
+ function lngToMercatorX(lng) {
549
+ return TILE_SIZE / (2 * Math.PI) * ((lng + 180) / 360 * 2 * Math.PI);
550
+ }
551
+ function computeDefaultZoom(options) {
552
+ const {
553
+ coordinates,
554
+ mapWidth,
555
+ mapHeight,
556
+ padding = 50,
557
+ maxZoom = 18,
558
+ minZoom = 1
559
+ } = options;
560
+ if (coordinates.length === 0) return null;
561
+ if (coordinates.length === 1) return maxZoom;
562
+ if (mapWidth <= 0 || mapHeight <= 0) return null;
563
+ let minLat = Infinity;
564
+ let maxLat = -Infinity;
565
+ let minLng = Infinity;
566
+ let maxLng = -Infinity;
567
+ for (const coord of coordinates) {
568
+ if (coord.lat < minLat) minLat = coord.lat;
569
+ if (coord.lat > maxLat) maxLat = coord.lat;
570
+ if (coord.lng < minLng) minLng = coord.lng;
571
+ if (coord.lng > maxLng) maxLng = coord.lng;
572
+ }
573
+ const pixelXMin = lngToMercatorX(minLng);
574
+ const pixelXMax = lngToMercatorX(maxLng);
575
+ const pixelYMin = latToMercatorY(maxLat);
576
+ const pixelYMax = latToMercatorY(minLat);
577
+ const dx = Math.abs(pixelXMax - pixelXMin);
578
+ const dy = Math.abs(pixelYMax - pixelYMin);
579
+ const availableWidth = mapWidth - padding * 2;
580
+ const availableHeight = mapHeight - padding * 2;
581
+ if (availableWidth <= 0 || availableHeight <= 0) return minZoom;
582
+ let zoom;
583
+ if (dx === 0 && dy === 0) {
584
+ return maxZoom;
585
+ } else if (dx === 0) {
586
+ zoom = Math.log2(availableHeight / dy);
587
+ } else if (dy === 0) {
588
+ zoom = Math.log2(availableWidth / dx);
589
+ } else {
590
+ const zoomX = Math.log2(availableWidth / dx);
591
+ const zoomY = Math.log2(availableHeight / dy);
592
+ zoom = Math.min(zoomX, zoomY);
593
+ }
594
+ return Math.max(minZoom, Math.min(maxZoom, Math.floor(zoom * 100) / 100));
595
+ }
596
+ function useDefaultZoom(options) {
597
+ const { coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom } = options;
598
+ return React3.useMemo(
599
+ () => computeDefaultZoom({
600
+ coordinates,
601
+ mapWidth,
602
+ mapHeight,
603
+ padding,
604
+ maxZoom,
605
+ minZoom
606
+ }),
607
+ [coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom]
608
+ );
609
+ }
542
610
  var PANEL_POSITION_CLASS = {
543
611
  "top-left": "left-4 top-4",
544
612
  "top-right": "right-4 top-4",
545
613
  "bottom-left": "bottom-4 left-4",
546
614
  "bottom-right": "bottom-4 right-4"
547
615
  };
616
+ function getOptimalPanelPosition(markerLat, markerLng, mapCenter) {
617
+ const isNorth = markerLat >= mapCenter.latitude;
618
+ const isEast = markerLng >= mapCenter.longitude;
619
+ if (isNorth && isEast) {
620
+ return "bottom-left";
621
+ } else if (isNorth && !isEast) {
622
+ return "bottom-right";
623
+ } else if (!isNorth && isEast) {
624
+ return "top-left";
625
+ } else {
626
+ return "top-right";
627
+ }
628
+ }
548
629
  var DEFAULT_VIEW_STATE = {
549
630
  latitude: 39.5,
550
631
  longitude: -98.35,
@@ -787,6 +868,13 @@ function GeoMap({
787
868
  IconComponent = FallbackIcon,
788
869
  ImgComponent = FallbackImg
789
870
  }) {
871
+ const containerRef = React3__namespace.useRef(null);
872
+ const [containerDimensions, setContainerDimensions] = React3__namespace.useState({
873
+ width: 800,
874
+ // Default width
875
+ height: 520
876
+ // Default height
877
+ });
790
878
  const [isMobile, setIsMobile] = React3__namespace.useState(false);
791
879
  React3__namespace.useEffect(() => {
792
880
  const checkMobile = () => {
@@ -802,6 +890,24 @@ function GeoMap({
802
890
  }
803
891
  return isMobile ? 420 : 520;
804
892
  }, [mapSize, isMobile]);
893
+ React3__namespace.useEffect(() => {
894
+ if (!containerRef.current) return;
895
+ const updateDimensions = () => {
896
+ if (containerRef.current) {
897
+ const rect = containerRef.current.getBoundingClientRect();
898
+ setContainerDimensions({
899
+ width: rect.width || 800,
900
+ height: rect.height || calculatedHeight
901
+ });
902
+ }
903
+ };
904
+ updateDimensions();
905
+ if (typeof ResizeObserver !== "undefined") {
906
+ const resizeObserver = new ResizeObserver(updateDimensions);
907
+ resizeObserver.observe(containerRef.current);
908
+ return () => resizeObserver.disconnect();
909
+ }
910
+ }, [calculatedHeight]);
805
911
  const normalizedStandaloneMarkers = React3__namespace.useMemo(
806
912
  () => markers.map((marker, index) => ({
807
913
  ...marker,
@@ -885,47 +991,49 @@ function GeoMap({
885
991
  longitude: DEFAULT_VIEW_STATE.longitude
886
992
  };
887
993
  }, [normalizedClusters, normalizedStandaloneMarkers]);
888
- const calculatedZoom = React3__namespace.useMemo(() => {
889
- if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
890
- return markerFocusZoom;
891
- }
892
- const allCoords = [];
994
+ const zoomCoordinates = React3__namespace.useMemo(() => {
995
+ const coords = [];
893
996
  normalizedStandaloneMarkers.forEach((marker) => {
894
- allCoords.push({
895
- latitude: marker.latitude,
896
- longitude: marker.longitude
997
+ coords.push({
998
+ lat: marker.latitude,
999
+ lng: marker.longitude
897
1000
  });
898
1001
  });
899
1002
  normalizedClusters.forEach((cluster) => {
900
- allCoords.push({
901
- latitude: cluster.latitude,
902
- longitude: cluster.longitude
1003
+ coords.push({
1004
+ lat: cluster.latitude,
1005
+ lng: cluster.longitude
903
1006
  });
904
1007
  });
905
- if (allCoords.length === 0) {
1008
+ return coords;
1009
+ }, [normalizedStandaloneMarkers, normalizedClusters]);
1010
+ const properZoom = useDefaultZoom({
1011
+ coordinates: zoomCoordinates,
1012
+ mapWidth: containerDimensions.width,
1013
+ mapHeight: containerDimensions.height,
1014
+ padding: 80,
1015
+ // Increased padding for better framing
1016
+ maxZoom: 18,
1017
+ minZoom: 1
1018
+ });
1019
+ const calculatedZoom = React3__namespace.useMemo(() => {
1020
+ if (zoomCoordinates.length === 1) {
1021
+ return markerFocusZoom;
1022
+ }
1023
+ if (zoomCoordinates.length === 0) {
906
1024
  return DEFAULT_VIEW_STATE.zoom;
907
1025
  }
908
- const lats = allCoords.map((c) => c.latitude);
909
- const lngs = allCoords.map((c) => c.longitude);
910
- const latDiff = Math.max(...lats) - Math.min(...lats);
911
- const lngDiff = Math.max(...lngs) - Math.min(...lngs);
912
- const maxDiff = Math.max(latDiff, lngDiff);
913
- const heightFactor = calculatedHeight / 520;
914
- const paddingFactor = 0.85;
915
- if (maxDiff > 10) return Math.max(2, 3 * heightFactor * paddingFactor);
916
- if (maxDiff > 5) return Math.max(4, 5 * heightFactor * paddingFactor);
917
- if (maxDiff > 2) return Math.max(6, 7 * heightFactor * paddingFactor);
918
- if (maxDiff > 1) return Math.max(8, 9 * heightFactor * paddingFactor);
919
- if (maxDiff > 0.5) return Math.max(9, 10 * heightFactor * paddingFactor);
920
- if (maxDiff > 0.1) return Math.max(11, 12 * heightFactor * paddingFactor);
921
- if (maxDiff > 0.01) return Math.max(12, 13 * heightFactor * paddingFactor);
922
- return Math.max(11, 12 * heightFactor * paddingFactor);
923
- }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom, calculatedHeight]);
1026
+ const adjustedZoom = properZoom ? properZoom - 0.5 : DEFAULT_VIEW_STATE.zoom;
1027
+ return Math.min(adjustedZoom, 15);
1028
+ }, [properZoom, zoomCoordinates.length, markerFocusZoom]);
924
1029
  const [uncontrolledViewState, setUncontrolledViewState] = React3__namespace.useState({
925
1030
  latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
926
1031
  longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
927
1032
  zoom: defaultViewState?.zoom ?? calculatedZoom
928
1033
  });
1034
+ const [dynamicPanelPosition, setDynamicPanelPosition] = React3__namespace.useState(
1035
+ panelPosition
1036
+ );
929
1037
  React3__namespace.useEffect(() => {
930
1038
  if (!viewState && !defaultViewState) {
931
1039
  setUncontrolledViewState({
@@ -1005,6 +1113,19 @@ function GeoMap({
1005
1113
  markerId: marker.id,
1006
1114
  clusterId: marker.clusterId
1007
1115
  });
1116
+ const currentCenter = resolvedViewState || {
1117
+ latitude: firstCoordinate.latitude,
1118
+ longitude: firstCoordinate.longitude
1119
+ };
1120
+ const optimalPosition = getOptimalPanelPosition(
1121
+ marker.latitude,
1122
+ marker.longitude,
1123
+ {
1124
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1125
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1126
+ }
1127
+ );
1128
+ setDynamicPanelPosition(optimalPosition);
1008
1129
  applyViewState({
1009
1130
  latitude: marker.latitude,
1010
1131
  longitude: marker.longitude,
@@ -1012,7 +1133,7 @@ function GeoMap({
1012
1133
  });
1013
1134
  emitSelectionChange({ type: "marker", marker });
1014
1135
  },
1015
- [applyViewState, emitSelectionChange, markerFocusZoom]
1136
+ [applyViewState, emitSelectionChange, markerFocusZoom, resolvedViewState, firstCoordinate]
1016
1137
  );
1017
1138
  const selectCluster = React3__namespace.useCallback(
1018
1139
  (cluster) => {
@@ -1020,6 +1141,19 @@ function GeoMap({
1020
1141
  type: "cluster",
1021
1142
  clusterId: cluster.id
1022
1143
  });
1144
+ const currentCenter = resolvedViewState || {
1145
+ latitude: firstCoordinate.latitude,
1146
+ longitude: firstCoordinate.longitude
1147
+ };
1148
+ const optimalPosition = getOptimalPanelPosition(
1149
+ cluster.latitude,
1150
+ cluster.longitude,
1151
+ {
1152
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1153
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1154
+ }
1155
+ );
1156
+ setDynamicPanelPosition(optimalPosition);
1023
1157
  applyViewState({
1024
1158
  latitude: cluster.latitude,
1025
1159
  longitude: cluster.longitude,
@@ -1027,7 +1161,7 @@ function GeoMap({
1027
1161
  });
1028
1162
  emitSelectionChange({ type: "cluster", cluster });
1029
1163
  },
1030
- [applyViewState, clusterFocusZoom, emitSelectionChange]
1164
+ [applyViewState, clusterFocusZoom, emitSelectionChange, resolvedViewState, firstCoordinate]
1031
1165
  );
1032
1166
  const clearSelection = React3__namespace.useCallback(() => {
1033
1167
  setSelection({ type: "none" });
@@ -1246,6 +1380,7 @@ function GeoMap({
1246
1380
  return /* @__PURE__ */ jsxRuntime.jsxs(
1247
1381
  "div",
1248
1382
  {
1383
+ ref: containerRef,
1249
1384
  className: cn(
1250
1385
  "relative rounded-2xl border border-border bg-background",
1251
1386
  // Remove overflow-hidden from outer container to allow panel to overflow
@@ -1313,7 +1448,7 @@ function GeoMap({
1313
1448
  {
1314
1449
  className: cn(
1315
1450
  "pointer-events-none absolute z-30",
1316
- PANEL_POSITION_CLASS[panelPosition]
1451
+ PANEL_POSITION_CLASS[dynamicPanelPosition]
1317
1452
  ),
1318
1453
  style: {
1319
1454
  // Ensure panel can overflow and has higher z-index