Skip to content

opensite-ai/page-speed-maps

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

17 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@page-speed/maps

High-performance MapLibre primitives. An open source tool by OpenSite AI

PageSpeed Map React Component


npm version npm downloads License TypeScript Tree-Shakeable

Install

pnpm add @page-speed/maps maplibre-gl react-map-gl

Quick Start

import { MapLibre } from "@page-speed/maps";

export function Example() {
  return (
    <div style={{ width: "100%", height: 420 }}>
      <MapLibre
        stadiaApiKey={process.env.NEXT_PUBLIC_STADIA_API ?? ""}
        mapStyle="osm-bright"
        viewState={{ latitude: 40.7128, longitude: -74.006, zoom: 12 }}
        markers={[
          {
            id: "nyc",
            latitude: 40.7128,
            longitude: -74.006,
            label: "New York"
          }
        ]}
      />
    </div>
  );
}

Why This Package

  • Explicit Stadia auth: no hard-coded keys
  • Auto-loads MapLibre CSS: no extra stylesheet import required
  • Keyless fallback map style: if no Stadia key is available, roads/landmarks still render via Carto Positron
  • Tree-shakable exports: import only what you need
  • Auto-centering hooks: compute optimal center and zoom for any set of coordinates
  • Drop-in API compatibility: works with the MapLibre component used in dt-cms

Table of Contents


Components

MapLibre / DTMapLibreMap

The main map component. Renders a MapLibre GL map with markers, controls, and full interactivity.

import { MapLibre } from "@page-speed/maps";

<MapLibre
  stadiaApiKey="your-api-key"
  mapStyle="osm-bright"
  viewState={{ latitude: 33.4484, longitude: -112.074, zoom: 10 }}
  markers={[
    { id: "phx", latitude: 33.4484, longitude: -112.074, label: "Phoenix" }
  ]}
  showNavigationControl
  showGeolocateControl
  onClick={(coord) => console.log("Clicked:", coord)}
  onMoveEnd={(center, zoom, bounds) => console.log("Moved:", center, zoom)}
/>

Props

Prop Type Default Description
stadiaApiKey string required Your Stadia Maps API key
mapStyle string "osm-bright" Built-in style name or custom style URL
viewState Partial<MapViewState> - Controlled view state (lat, lng, zoom)
onViewStateChange (state) => void - Callback when view state changes
markers Array<MapLibreMarker | BasicMarkerInput> [] Array of markers to display
center { lat, lng } - Initial center (alternative to viewState)
zoom number 14 Initial zoom level
styleUrl string - Custom style URL (overrides mapStyle)
onClick (coord) => void - Map click handler
onMoveEnd (center, zoom, bounds) => void - Called when map stops moving
onMarkerDrag (markerId, coord) => void - Called when draggable marker moves
showNavigationControl boolean true Show zoom/rotation controls
showGeolocateControl boolean false Show user location button
navigationControlPosition MapControlPosition "bottom-right" Position of nav controls
geolocateControlPosition MapControlPosition "top-left" Position of geolocate button
flyToOptions MapLibreFlyToOptions {} Animation options for flyTo
className string - CSS class for wrapper div
style CSSProperties - Inline styles for wrapper
children ReactNode - Additional map layers/overlays
mapLibreCssHref string jsDelivr CDN Custom MapLibre CSS URL

Hooks

useGeoCenter

Computes the geographic center of an array of coordinates using the Cartesian 3D averaging method. Handles antimeridian crossing and polar coordinates correctly.

import { useGeoCenter } from "@page-speed/maps/hooks/useGeoCenter";
// or
import { useGeoCenter } from "@page-speed/maps";

const markers = [
  { lat: 33.4585, lng: -112.0715 },  // Downtown Phoenix
  { lat: 33.6510, lng: -111.9244 },  // Scottsdale
  { lat: 33.3062, lng: -111.8413 },  // Mesa
];

const center = useGeoCenter(markers);
// Result: { lat: 33.4719, lng: -111.9457 }

API

function useGeoCenter(coordinates: GeoCoordinate[]): GeoCenterResult | null;
function computeGeoCenter(coordinates: GeoCoordinate[]): GeoCenterResult | null;

interface GeoCoordinate {
  lat: number;
  lng: number;
}

interface GeoCenterResult {
  lat: number;
  lng: number;
}

Behavior

  • Empty array: Returns null
  • Single coordinate: Returns that coordinate
  • Multiple coordinates: Returns the geographic midpoint

useDefaultZoom

Computes the optimal zoom level to fit all coordinates within a given viewport, using Mercator projection math. Uses MapLibre's native 512px tile size.

import { useDefaultZoom } from "@page-speed/maps/hooks/useDefaultZoom";
// or
import { useDefaultZoom } from "@page-speed/maps";

const markers = [
  { lat: 33.4585, lng: -112.0715 },
  { lat: 33.6510, lng: -111.9244 },
];

const zoom = useDefaultZoom({
  coordinates: markers,
  mapWidth: 600,
  mapHeight: 400,
  padding: 50,
  maxZoom: 16,
  minZoom: 1,
});
// Result: ~10.5 (fits both markers with padding)

API

function useDefaultZoom(options: DefaultZoomOptions): number | null;
function computeDefaultZoom(options: DefaultZoomOptions): number | null;

interface DefaultZoomOptions {
  coordinates: GeoCoordinate[];
  mapWidth: number;
  mapHeight: number;
  padding?: number;   // default: 50
  maxZoom?: number;   // default: 18
  minZoom?: number;   // default: 1
}

Behavior

  • Empty array: Returns null
  • Single coordinate: Returns maxZoom
  • Multiple coordinates: Returns the highest zoom that fits all markers with padding
  • Invalid dimensions: Returns null or minZoom

Combined Usage: Auto-Centering Map

import { MapLibre, useGeoCenter, useDefaultZoom } from "@page-speed/maps";

function AutoCenteringMap({ locations }) {
  const coordinates = locations.map(loc => ({
    lat: loc.latitude,
    lng: loc.longitude,
  }));

  const center = useGeoCenter(coordinates);
  const zoom = useDefaultZoom({
    coordinates,
    mapWidth: 800,
    mapHeight: 600,
    padding: 60,
  });

  if (!center) return <div>No locations to display</div>;

  return (
    <div style={{ width: 800, height: 600 }}>
      <MapLibre
        stadiaApiKey={process.env.NEXT_PUBLIC_STADIA_API ?? ""}
        viewState={{
          latitude: center.lat,
          longitude: center.lng,
          zoom: zoom ?? 10,
        }}
        markers={locations.map((loc, i) => ({
          id: loc.id ?? i,
          latitude: loc.latitude,
          longitude: loc.longitude,
          label: loc.name,
        }))}
      />
    </div>
  );
}

Utilities

getMapLibreStyleUrl(style, stadiaApiKey)

Resolves a style name or URL to a fully-qualified MapLibre style URL with authentication.

import { getMapLibreStyleUrl } from "@page-speed/maps/utils/style-url";

const url = getMapLibreStyleUrl("osm-bright", "your-api-key");
// "https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=your-api-key"

const fallback = getMapLibreStyleUrl("osm-bright", "");
// "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" (keyless fallback)

appendStadiaApiKey(styleUrl, stadiaApiKey)

Appends the Stadia API key to a style URL if it's a Stadia Maps URL.

import { appendStadiaApiKey } from "@page-speed/maps/utils/style-url";

const url = appendStadiaApiKey(
  "https://tiles.stadiamaps.com/styles/osm_bright.json",
  "your-api-key"
);
// "https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=your-api-key"

generateGoogleMapLink(latitude, longitude, zoom?)

Generates a Google Maps URL for a location.

import { generateGoogleMapLink } from "@page-speed/maps/utils/google-links";

const url = generateGoogleMapLink(33.4484, -112.074, 15);
// "https://www.google.com/maps/@33.4484,-112.074,15z"

generateGoogleDirectionsLink(latitude, longitude)

Generates a Google Maps directions URL to a destination.

import { generateGoogleDirectionsLink } from "@page-speed/maps/utils/google-links";

const url = generateGoogleDirectionsLink(33.4484, -112.074);
// "https://www.google.com/maps/dir/?api=1&destination=33.4484,-112.074"

Types

All types are exported from @page-speed/maps/types or the main entry point:

import type {
  BasicMarkerInput,
  MapControlPosition,
  MapCoordinate,
  MapLibreFlyToOptions,
  MapLibreMarker,
  MapLibreProps,
  MapViewState,
  GeoCoordinate,
  GeoCenterResult,
  DefaultZoomOptions,
  MapLibreBuiltInStyle,
} from "@page-speed/maps";

Key Types

type MapViewState = {
  latitude: number;
  longitude: number;
  zoom: number;
};

type MapCoordinate = {
  latitude: number;
  longitude: number;
};

type GeoCoordinate = {
  lat: number;
  lng: number;
};

type BasicMarkerInput = {
  id?: string | number;
  latitude: number;
  longitude: number;
  color?: string;
  draggable?: boolean;
  label?: string;
  element?: (() => React.ReactNode) | React.ReactNode;
  onClick?: () => void;
};

type MapControlPosition =
  | "top-left"
  | "top-right"
  | "bottom-left"
  | "bottom-right";

Tree Shaking

This package supports granular tree-shaking. Import only what you need:

// Full bundle (all exports)
import { MapLibre, useGeoCenter, useDefaultZoom } from "@page-speed/maps";

// Just the component
import { MapLibre } from "@page-speed/maps/core";

// Just hooks (tree-shakable)
import { useGeoCenter, useDefaultZoom } from "@page-speed/maps/hooks";

// Individual hooks (maximum tree-shaking)
import { useGeoCenter } from "@page-speed/maps/hooks/useGeoCenter";
import { useDefaultZoom } from "@page-speed/maps/hooks/useDefaultZoom";

// Just utilities
import { getMapLibreStyleUrl } from "@page-speed/maps/utils/style-url";
import { generateGoogleMapLink } from "@page-speed/maps/utils/google-links";

// Just types
import type { MapLibreProps } from "@page-speed/maps/types";

Map Styles

Built-in style presets (requires Stadia API key):

Style Name Description
osm-bright Clean, bright OpenStreetMap style (default)
alidade-smooth Modern, smooth cartography
alidade-smooth-dark Dark theme variant
stadia-outdoors Outdoor/terrain focused
stamen-toner High-contrast black & white
stamen-terrain Terrain with hillshading
stamen-watercolor Artistic watercolor style
maplibre-default Carto Positron (no API key required)
<MapLibre mapStyle="stamen-terrain" stadiaApiKey="..." />

Or use a custom style URL:

<MapLibre styleUrl="https://your-tiles.com/style.json" stadiaApiKey="..." />

Advanced Usage

Custom Markers

Pass a custom React element for full control over marker rendering:

<MapLibre
  stadiaApiKey="..."
  markers={[
    {
      id: "custom",
      latitude: 33.4484,
      longitude: -112.074,
      element: (
        <div className="custom-marker">
          <img src="/pin.svg" alt="Location" />
          <span>Phoenix HQ</span>
        </div>
      ),
    },
  ]}
/>

Draggable Markers

<MapLibre
  stadiaApiKey="..."
  markers={[
    {
      id: "draggable",
      latitude: 33.4484,
      longitude: -112.074,
      draggable: true,
      label: "Drag me!",
    },
  ]}
  onMarkerDrag={(markerId, coord) => {
    console.log(`Marker ${markerId} moved to:`, coord);
  }}
/>

Controlled View State

function ControlledMap() {
  const [viewState, setViewState] = useState({
    latitude: 33.4484,
    longitude: -112.074,
    zoom: 12,
  });

  return (
    <>
      <MapLibre
        stadiaApiKey="..."
        viewState={viewState}
        onViewStateChange={setViewState}
      />
      <button onClick={() => setViewState(prev => ({ ...prev, zoom: prev.zoom + 1 }))}>
        Zoom In
      </button>
    </>
  );
}

Fly To Animation

<MapLibre
  stadiaApiKey="..."
  viewState={viewState}
  flyToOptions={{
    speed: 1.2,
    curve: 1.5,
    easing: (t) => t,
  }}
/>

Composing with UI Libraries

This package provides the core map primitives. For feature-rich components with clustering, info windows, and styled markers, see @opensite/ui which builds on top of @page-speed/maps:

// In @opensite/ui (consumer library)
import { useGeoCenter, useDefaultZoom, type GeoCoordinate } from "@page-speed/maps/hooks";

function GeoMap({ markers, clusters, defaultViewState }) {
  // Collect all coordinates
  const allCoordinates: GeoCoordinate[] = [
    ...markers.map(m => ({ lat: m.latitude, lng: m.longitude })),
    ...clusters.map(c => ({ lat: c.latitude, lng: c.longitude })),
  ];

  // Auto-compute center and zoom
  const geoCenter = useGeoCenter(allCoordinates);
  const defaultZoom = useDefaultZoom({
    coordinates: allCoordinates,
    mapWidth: 600,
    mapHeight: 520,
    padding: 60,
  });

  return (
    <MapLibre
      viewState={{
        latitude: defaultViewState?.latitude ?? geoCenter?.lat ?? 0,
        longitude: defaultViewState?.longitude ?? geoCenter?.lng ?? 0,
        zoom: defaultViewState?.zoom ?? defaultZoom ?? 10,
      }}
      // ... clustering, custom markers, info windows, etc.
    />
  );
}

API Reference

Exports Summary

Export Path Description
MapLibre @page-speed/maps Main map component
DTMapLibreMap @page-speed/maps Alias for MapLibre
useGeoCenter @page-speed/maps/hooks/useGeoCenter Geographic center hook
computeGeoCenter @page-speed/maps/hooks/useGeoCenter Pure function version
useDefaultZoom @page-speed/maps/hooks/useDefaultZoom Auto-zoom hook
computeDefaultZoom @page-speed/maps/hooks/useDefaultZoom Pure function version
getMapLibreStyleUrl @page-speed/maps/utils/style-url Style URL resolver
appendStadiaApiKey @page-speed/maps/utils/style-url API key appender
generateGoogleMapLink @page-speed/maps/utils/google-links Google Maps link
generateGoogleDirectionsLink @page-speed/maps/utils/google-links Google Directions link

License

BSD-3-Clause. See LICENSE for details.


New: Feature-Rich Components

GeoMap

Full-featured map component with markers, clusters, rich media panels, and automatic view calculation.

import { GeoMap, createMapMarkerElement } from "@page-speed/maps";
import type { GeoMapMarker } from "@page-speed/maps";

const markers: GeoMapMarker[] = [
  {
    id: 'office',
    latitude: 40.7128,
    longitude: -74.0060,
    title: 'New York Office',
    summary: 'Our headquarters in downtown Manhattan',
    locationLine: '123 Broadway, New York, NY 10001',
    hoursLine: 'Mon-Fri: 9:00 AM - 6:00 PM',
    mediaItems: [
      { id: '1', src: '/office.jpg', alt: 'Office' },
    ],
    markerElement: createMapMarkerElement({ size: 'lg' }),
    actions: [
      {
        label: 'Get Directions',
        href: 'https://maps.app.goo.gl/example',
      },
    ],
  },
];

<GeoMap
  markers={markers}
  stadiaApiKey="your-key"
  panelPosition="bottom-left"
  showNavigationControl
/>

Key Features:

  • βœ… Auto-calculated center and zoom from markers
  • βœ… Rich media carousels (images/videos)
  • βœ… Interactive marker panels
  • βœ… Clustering support
  • βœ… Custom marker elements
  • βœ… Action buttons and links

β†’ Full GeoMap Documentation

MapMarker

Beautiful concentric circle markers with hover and selection states.

import { MapMarker, NeutralMapMarker, createMapMarkerElement } from "@page-speed/maps";

// Direct usage
<MapMarker
  size="lg"
  isSelected
  dotColor="#1E40AF"
  innerRingColor="#3B82F6"
  middleRingColor="#93C5FD"
  outerRingColor="#DBEAFE"
/>

// With GeoMap
const markers = [{
  id: 'loc-1',
  latitude: 40.7128,
  longitude: -74.0060,
  markerElement: createMapMarkerElement({ size: 'lg' }),
}];

Sizes: sm | md | lg
Pre-configured: NeutralMapMarker for neutral gray design

β†’ MapMarker Examples


Migration from @opensite/ui

If you're migrating map components from @opensite/ui to @page-speed/maps, see our comprehensive migration guide:

β†’ Migration Guide

Key Changes:

  • βœ… Fixed zoom/centering bugs
  • βœ… New MapMarker components
  • βœ… Better tree-shakability
  • βœ… Optional peer dependencies for icons/images

Documentation


Related Packages


Made with ❀️ for the DashTrack Platform

About

πŸ“ High-performance MapLibre primitives for DashTrack and OpenSite apps.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages