@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.
@@ -2,7 +2,7 @@ import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import * as React3 from 'react';
5
- import React3__default from 'react';
5
+ import React3__default, { useMemo } from 'react';
6
6
  import { Marker, Map as Map$1, GeolocateControl, NavigationControl } from 'react-map-gl/maplibre';
7
7
 
8
8
  // src/utils/cn.ts
@@ -637,12 +637,93 @@ function MapLibre({
637
637
  }
638
638
  );
639
639
  }
640
+ var TILE_SIZE = 512;
641
+ function latToMercatorY(lat) {
642
+ const latRad = lat * Math.PI / 180;
643
+ const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
644
+ return TILE_SIZE / (2 * Math.PI) * (Math.PI - mercN);
645
+ }
646
+ function lngToMercatorX(lng) {
647
+ return TILE_SIZE / (2 * Math.PI) * ((lng + 180) / 360 * 2 * Math.PI);
648
+ }
649
+ function computeDefaultZoom(options) {
650
+ const {
651
+ coordinates,
652
+ mapWidth,
653
+ mapHeight,
654
+ padding = 50,
655
+ maxZoom = 18,
656
+ minZoom = 1
657
+ } = options;
658
+ if (coordinates.length === 0) return null;
659
+ if (coordinates.length === 1) return maxZoom;
660
+ if (mapWidth <= 0 || mapHeight <= 0) return null;
661
+ let minLat = Infinity;
662
+ let maxLat = -Infinity;
663
+ let minLng = Infinity;
664
+ let maxLng = -Infinity;
665
+ for (const coord of coordinates) {
666
+ if (coord.lat < minLat) minLat = coord.lat;
667
+ if (coord.lat > maxLat) maxLat = coord.lat;
668
+ if (coord.lng < minLng) minLng = coord.lng;
669
+ if (coord.lng > maxLng) maxLng = coord.lng;
670
+ }
671
+ const pixelXMin = lngToMercatorX(minLng);
672
+ const pixelXMax = lngToMercatorX(maxLng);
673
+ const pixelYMin = latToMercatorY(maxLat);
674
+ const pixelYMax = latToMercatorY(minLat);
675
+ const dx = Math.abs(pixelXMax - pixelXMin);
676
+ const dy = Math.abs(pixelYMax - pixelYMin);
677
+ const availableWidth = mapWidth - padding * 2;
678
+ const availableHeight = mapHeight - padding * 2;
679
+ if (availableWidth <= 0 || availableHeight <= 0) return minZoom;
680
+ let zoom;
681
+ if (dx === 0 && dy === 0) {
682
+ return maxZoom;
683
+ } else if (dx === 0) {
684
+ zoom = Math.log2(availableHeight / dy);
685
+ } else if (dy === 0) {
686
+ zoom = Math.log2(availableWidth / dx);
687
+ } else {
688
+ const zoomX = Math.log2(availableWidth / dx);
689
+ const zoomY = Math.log2(availableHeight / dy);
690
+ zoom = Math.min(zoomX, zoomY);
691
+ }
692
+ return Math.max(minZoom, Math.min(maxZoom, Math.floor(zoom * 100) / 100));
693
+ }
694
+ function useDefaultZoom(options) {
695
+ const { coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom } = options;
696
+ return useMemo(
697
+ () => computeDefaultZoom({
698
+ coordinates,
699
+ mapWidth,
700
+ mapHeight,
701
+ padding,
702
+ maxZoom,
703
+ minZoom
704
+ }),
705
+ [coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom]
706
+ );
707
+ }
640
708
  var PANEL_POSITION_CLASS = {
641
709
  "top-left": "left-4 top-4",
642
710
  "top-right": "right-4 top-4",
643
711
  "bottom-left": "bottom-4 left-4",
644
712
  "bottom-right": "bottom-4 right-4"
645
713
  };
714
+ function getOptimalPanelPosition(markerLat, markerLng, mapCenter) {
715
+ const isNorth = markerLat >= mapCenter.latitude;
716
+ const isEast = markerLng >= mapCenter.longitude;
717
+ if (isNorth && isEast) {
718
+ return "bottom-left";
719
+ } else if (isNorth && !isEast) {
720
+ return "bottom-right";
721
+ } else if (!isNorth && isEast) {
722
+ return "top-left";
723
+ } else {
724
+ return "top-right";
725
+ }
726
+ }
646
727
  var DEFAULT_VIEW_STATE = {
647
728
  latitude: 39.5,
648
729
  longitude: -98.35,
@@ -885,6 +966,13 @@ function GeoMap({
885
966
  IconComponent = FallbackIcon,
886
967
  ImgComponent = FallbackImg
887
968
  }) {
969
+ const containerRef = React3.useRef(null);
970
+ const [containerDimensions, setContainerDimensions] = React3.useState({
971
+ width: 800,
972
+ // Default width
973
+ height: 520
974
+ // Default height
975
+ });
888
976
  const [isMobile, setIsMobile] = React3.useState(false);
889
977
  React3.useEffect(() => {
890
978
  const checkMobile = () => {
@@ -900,6 +988,24 @@ function GeoMap({
900
988
  }
901
989
  return isMobile ? 420 : 520;
902
990
  }, [mapSize, isMobile]);
991
+ React3.useEffect(() => {
992
+ if (!containerRef.current) return;
993
+ const updateDimensions = () => {
994
+ if (containerRef.current) {
995
+ const rect = containerRef.current.getBoundingClientRect();
996
+ setContainerDimensions({
997
+ width: rect.width || 800,
998
+ height: rect.height || calculatedHeight
999
+ });
1000
+ }
1001
+ };
1002
+ updateDimensions();
1003
+ if (typeof ResizeObserver !== "undefined") {
1004
+ const resizeObserver = new ResizeObserver(updateDimensions);
1005
+ resizeObserver.observe(containerRef.current);
1006
+ return () => resizeObserver.disconnect();
1007
+ }
1008
+ }, [calculatedHeight]);
903
1009
  const normalizedStandaloneMarkers = React3.useMemo(
904
1010
  () => markers.map((marker, index) => ({
905
1011
  ...marker,
@@ -983,47 +1089,49 @@ function GeoMap({
983
1089
  longitude: DEFAULT_VIEW_STATE.longitude
984
1090
  };
985
1091
  }, [normalizedClusters, normalizedStandaloneMarkers]);
986
- const calculatedZoom = React3.useMemo(() => {
987
- if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
988
- return markerFocusZoom;
989
- }
990
- const allCoords = [];
1092
+ const zoomCoordinates = React3.useMemo(() => {
1093
+ const coords = [];
991
1094
  normalizedStandaloneMarkers.forEach((marker) => {
992
- allCoords.push({
993
- latitude: marker.latitude,
994
- longitude: marker.longitude
1095
+ coords.push({
1096
+ lat: marker.latitude,
1097
+ lng: marker.longitude
995
1098
  });
996
1099
  });
997
1100
  normalizedClusters.forEach((cluster) => {
998
- allCoords.push({
999
- latitude: cluster.latitude,
1000
- longitude: cluster.longitude
1101
+ coords.push({
1102
+ lat: cluster.latitude,
1103
+ lng: cluster.longitude
1001
1104
  });
1002
1105
  });
1003
- if (allCoords.length === 0) {
1106
+ return coords;
1107
+ }, [normalizedStandaloneMarkers, normalizedClusters]);
1108
+ const properZoom = useDefaultZoom({
1109
+ coordinates: zoomCoordinates,
1110
+ mapWidth: containerDimensions.width,
1111
+ mapHeight: containerDimensions.height,
1112
+ padding: 80,
1113
+ // Increased padding for better framing
1114
+ maxZoom: 18,
1115
+ minZoom: 1
1116
+ });
1117
+ const calculatedZoom = React3.useMemo(() => {
1118
+ if (zoomCoordinates.length === 1) {
1119
+ return markerFocusZoom;
1120
+ }
1121
+ if (zoomCoordinates.length === 0) {
1004
1122
  return DEFAULT_VIEW_STATE.zoom;
1005
1123
  }
1006
- const lats = allCoords.map((c) => c.latitude);
1007
- const lngs = allCoords.map((c) => c.longitude);
1008
- const latDiff = Math.max(...lats) - Math.min(...lats);
1009
- const lngDiff = Math.max(...lngs) - Math.min(...lngs);
1010
- const maxDiff = Math.max(latDiff, lngDiff);
1011
- const heightFactor = calculatedHeight / 520;
1012
- const paddingFactor = 0.85;
1013
- if (maxDiff > 10) return Math.max(2, 3 * heightFactor * paddingFactor);
1014
- if (maxDiff > 5) return Math.max(4, 5 * heightFactor * paddingFactor);
1015
- if (maxDiff > 2) return Math.max(6, 7 * heightFactor * paddingFactor);
1016
- if (maxDiff > 1) return Math.max(8, 9 * heightFactor * paddingFactor);
1017
- if (maxDiff > 0.5) return Math.max(9, 10 * heightFactor * paddingFactor);
1018
- if (maxDiff > 0.1) return Math.max(11, 12 * heightFactor * paddingFactor);
1019
- if (maxDiff > 0.01) return Math.max(12, 13 * heightFactor * paddingFactor);
1020
- return Math.max(11, 12 * heightFactor * paddingFactor);
1021
- }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom, calculatedHeight]);
1124
+ const adjustedZoom = properZoom ? properZoom - 0.5 : DEFAULT_VIEW_STATE.zoom;
1125
+ return Math.min(adjustedZoom, 15);
1126
+ }, [properZoom, zoomCoordinates.length, markerFocusZoom]);
1022
1127
  const [uncontrolledViewState, setUncontrolledViewState] = React3.useState({
1023
1128
  latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
1024
1129
  longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
1025
1130
  zoom: defaultViewState?.zoom ?? calculatedZoom
1026
1131
  });
1132
+ const [dynamicPanelPosition, setDynamicPanelPosition] = React3.useState(
1133
+ panelPosition
1134
+ );
1027
1135
  React3.useEffect(() => {
1028
1136
  if (!viewState && !defaultViewState) {
1029
1137
  setUncontrolledViewState({
@@ -1103,6 +1211,19 @@ function GeoMap({
1103
1211
  markerId: marker.id,
1104
1212
  clusterId: marker.clusterId
1105
1213
  });
1214
+ const currentCenter = resolvedViewState || {
1215
+ latitude: firstCoordinate.latitude,
1216
+ longitude: firstCoordinate.longitude
1217
+ };
1218
+ const optimalPosition = getOptimalPanelPosition(
1219
+ marker.latitude,
1220
+ marker.longitude,
1221
+ {
1222
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1223
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1224
+ }
1225
+ );
1226
+ setDynamicPanelPosition(optimalPosition);
1106
1227
  applyViewState({
1107
1228
  latitude: marker.latitude,
1108
1229
  longitude: marker.longitude,
@@ -1110,7 +1231,7 @@ function GeoMap({
1110
1231
  });
1111
1232
  emitSelectionChange({ type: "marker", marker });
1112
1233
  },
1113
- [applyViewState, emitSelectionChange, markerFocusZoom]
1234
+ [applyViewState, emitSelectionChange, markerFocusZoom, resolvedViewState, firstCoordinate]
1114
1235
  );
1115
1236
  const selectCluster = React3.useCallback(
1116
1237
  (cluster) => {
@@ -1118,6 +1239,19 @@ function GeoMap({
1118
1239
  type: "cluster",
1119
1240
  clusterId: cluster.id
1120
1241
  });
1242
+ const currentCenter = resolvedViewState || {
1243
+ latitude: firstCoordinate.latitude,
1244
+ longitude: firstCoordinate.longitude
1245
+ };
1246
+ const optimalPosition = getOptimalPanelPosition(
1247
+ cluster.latitude,
1248
+ cluster.longitude,
1249
+ {
1250
+ latitude: currentCenter.latitude ?? firstCoordinate.latitude,
1251
+ longitude: currentCenter.longitude ?? firstCoordinate.longitude
1252
+ }
1253
+ );
1254
+ setDynamicPanelPosition(optimalPosition);
1121
1255
  applyViewState({
1122
1256
  latitude: cluster.latitude,
1123
1257
  longitude: cluster.longitude,
@@ -1125,7 +1259,7 @@ function GeoMap({
1125
1259
  });
1126
1260
  emitSelectionChange({ type: "cluster", cluster });
1127
1261
  },
1128
- [applyViewState, clusterFocusZoom, emitSelectionChange]
1262
+ [applyViewState, clusterFocusZoom, emitSelectionChange, resolvedViewState, firstCoordinate]
1129
1263
  );
1130
1264
  const clearSelection = React3.useCallback(() => {
1131
1265
  setSelection({ type: "none" });
@@ -1344,6 +1478,7 @@ function GeoMap({
1344
1478
  return /* @__PURE__ */ jsxs(
1345
1479
  "div",
1346
1480
  {
1481
+ ref: containerRef,
1347
1482
  className: cn(
1348
1483
  "relative rounded-2xl border border-border bg-background",
1349
1484
  // Remove overflow-hidden from outer container to allow panel to overflow
@@ -1411,7 +1546,7 @@ function GeoMap({
1411
1546
  {
1412
1547
  className: cn(
1413
1548
  "pointer-events-none absolute z-30",
1414
- PANEL_POSITION_CLASS[panelPosition]
1549
+ PANEL_POSITION_CLASS[dynamicPanelPosition]
1415
1550
  ),
1416
1551
  style: {
1417
1552
  // Ensure panel can overflow and has higher z-index