Features & Customizations

Core Concepts

Normalized Coordinates

map-routing uses a normalized coordinate system where all positions are expressed as values between 0 and 1, making routes responsive and independent of actual image dimensions.

Coordinate System Explained


(0,0) ─────────────────────── (1,0)
  │                              │
  │      • (0.5, 0.3)            │
  │                              │
  │                • (0.7, 0.6)  │
  │                              │
(0,1) ─────────────────────── (1,1)

X-axis: 0 (left) → 1 (right)
Y-axis: 0 (top) → 1 (bottom)
        

Example Point Definitions

const route = [
  { x: 0, y: 0, marked: 0 },       // Top-left corner at t=0s
  { x: 0.5, y: 0.5, start: 0, end: 10 },  // Center point, path from 0-10s
  { x: 1, y: 1, marked: 10 }       // Bottom-right corner at t=10s
];

Why Normalized Coordinates? This approach ensures your routes scale perfectly across different screen sizes and image resolutions without any code changes.

Point Types

There are two types of points in a route:

const route = [
  // Standalone point (marker)
  { x: 0.2, y: 0.3, marked: 0, data: "Starting location" },

  // Path segments (connected)
  { x: 0.4, y: 0.5, start: 0, end: 5 },
  { x: 0.6, y: 0.7, start: 5, end: 10 },

  // Another standalone point (marker)
  { x: 0.8, y: 0.8, marked: 10, data: "Ending location" }
];

Interaction Controls

Mouse Controls

Touch Gestures

Tap Detection: Use the tapThreshold prop to adjust how much movement is allowed before a tap becomes a drag (default: 10 pixels).

Configuration Options

Zoom Configuration

Fine-tune the zoom behavior to match your needs:

<ImageRoute
  src="/map.jpg"
  route={route}
  minZoom={0.5}           // Allow zooming out to 50%
  maxZoom={10}            // Allow zooming in to 1000%
  zoomSensitivity={0.002} // Higher = more sensitive wheel zoom
/>

Zoom Presets

// Conservative (limited zoom)
const conservativeZoom = {
  minZoom: 0.8,
  maxZoom: 3,
  zoomSensitivity: 0.0005
};

// Default (balanced)
const defaultZoom = {
  minZoom: 1,
  maxZoom: 5,
  zoomSensitivity: 0.001
};

// Aggressive (wide zoom range)
const aggressiveZoom = {
  minZoom: 0.5,
  maxZoom: 15,
  zoomSensitivity: 0.003
};

Animation Configuration

Control the smoothness and timing of animations:

<ImageRoute
  src="/map.jpg"
  route={route}
  animationDuration={400}  // Slower, smoother transitions
  animationDelay={50}      // Slight delay before animation starts
/>

Interaction Thresholds

<ImageRoute
  src="/map.jpg"
  route={route}
  snapThreshold={0.03}     // Larger click target area for points
  tapThreshold={15}        // More forgiving tap detection on touch
/>

Custom Renderers

Custom Point Renderer

Completely customize how route points are displayed:

import { ImageRoute } from 'map-routing';

function CustomPointRenderer({ spot, index }) {
  const { point, visited, active } = spot;

  return (
    <div
      style={{
        position: 'absolute',
        left: `${point.x * 100}%`,
        top: `${point.y * 100}%`,
        transform: 'translate(-50%, -50%)',
        width: active ? '20px' : '12px',
        height: active ? '20px' : '12px',
        borderRadius: '50%',
        backgroundColor: visited ? '#00ff00' : '#ff0000',
        border: active ? '3px solid yellow' : '2px solid white',
        boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
        transition: 'all 0.3s ease',
        zIndex: active ? 10 : 5
      }}
    >
      {point.data && (
        <div style={{
          position: 'absolute',
          top: '-30px',
          left: '50%',
          transform: 'translateX(-50%)',
          whiteSpace: 'nowrap',
          padding: '4px 8px',
          backgroundColor: 'rgba(0,0,0,0.8)',
          color: 'white',
          borderRadius: '4px',
          fontSize: '12px'
        }}>
          {point.data}
        </div>
      )}
    </div>
  );
}

function App() {
  return (
    <ImageRoute
      src="/map.jpg"
      route={routeWithLabels}
      renderPoint={CustomPointRenderer}
    />
  );
}

Custom Path Renderer

Customize how path segments between points are drawn:

function CustomPathRenderer({ from, to }) {
  const { point: fromPoint, visited: fromVisited } = from;
  const { point: toPoint, visited: toVisited } = to;

  const pathVisited = fromVisited && toVisited;

  // Calculate SVG path
  const pathD = `M ${fromPoint.x * 100} ${fromPoint.y * 100}
                 L ${toPoint.x * 100} ${toPoint.y * 100}`;

  return (
    <svg
      style={{
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none'
      }}
      viewBox="0 0 100 100"
      preserveAspectRatio="none"
    >
      <path
        d={pathD}
        stroke={pathVisited ? '#00ff00' : '#cccccc'}
        strokeWidth={pathVisited ? '0.5' : '0.3'}
        strokeDasharray={pathVisited ? 'none' : '2,2'}
        fill="none"
      />
    </svg>
  );
}

function App() {
  return (
    <ImageRoute
      src="/map.jpg"
      route={route}
      renderPath={CustomPathRenderer}
    />
  );
}

Custom Extra Overlay

Add additional overlays like labels, legends, or custom UI:

function CustomOverlay({ spots, progress }) {
  const activeSpot = spots.find(s => s.active);

  return (
    <div style={{
      position: 'absolute',
      top: '20px',
      right: '20px',
      backgroundColor: 'rgba(255,255,255,0.9)',
      padding: '15px',
      borderRadius: '8px',
      boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
      minWidth: '200px'
    }}>
      <h3 style={{ margin: '0 0 10px 0' }}>Route Info</h3>
      <p>Progress: {progress?.toFixed(1)}s</p>
      {activeSpot && activeSpot.point.data && (
        <p>Current: {activeSpot.point.data}</p>
      )}
      <p>Visited: {spots.filter(s => s.visited).length}/{spots.length}</p>
    </div>
  );
}

function App() {
  return (
    <ImageRoute
      src="/map.jpg"
      route={route}
      progress={currentTime}
      renderExtra={CustomOverlay}
    />
  );
}

Advanced Video Synchronization

Bidirectional Sync

Create a fully synchronized experience where clicking the map seeks the video and video playback updates the map:

import { ImageRoute, useRouteVideoSync } from 'map-routing';
import { useRef } from 'react';

function FullVideoSync() {
  const videoRef = useRef(null);

  const route = [
    { x: 0.2, y: 0.3, marked: 0 },
    { x: 0.5, y: 0.5, start: 0, end: 30 },
    { x: 0.8, y: 0.7, marked: 30 }
  ];

  const { progress, setProgress } = useRouteVideoSync({
    videoRef,
    route,
    onProgressChange: (time) => {
      console.log('Video time updated:', time);
    }
  });

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
      <video
        ref={videoRef}
        src="/video.mp4"
        controls
        style={{ width: '100%', maxHeight: '400px' }}
      />

      <ImageRoute
        src="/map.jpg"
        route={route}
        progress={progress}
        onProgressChange={setProgress}  // Clicking map seeks video
        style={{ width: '100%', height: '500px' }}
      />
    </div>
  );
}

Multiple Video Sources

Sync a single map with multiple video angles:

function MultiVideoSync() {
  const video1Ref = useRef(null);
  const video2Ref = useRef(null);
  const [currentTime, setCurrentTime] = useState(0);

  const route = [...]; // Your route data

  // Sync first video
  useRouteVideoSync({
    videoRef: video1Ref,
    route,
    onProgressChange: setCurrentTime
  });

  // Sync second video
  useRouteVideoSync({
    videoRef: video2Ref,
    route,
    onProgressChange: setCurrentTime
  });

  const handleMapClick = (time) => {
    // Seek both videos
    if (video1Ref.current) video1Ref.current.currentTime = time;
    if (video2Ref.current) video2Ref.current.currentTime = time;
  };

  return (
    <div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
        <video ref={video1Ref} src="/angle1.mp4" controls />
        <video ref={video2Ref} src="/angle2.mp4" controls />
      </div>

      <ImageRoute
        src="/map.jpg"
        route={route}
        progress={currentTime}
        onProgressChange={handleMapClick}
        style={{ width: '100%', height: '600px', marginTop: '20px' }}
      />
    </div>
  );
}

Data Format Examples

Simple Route (No Video Sync)

const simpleRoute = [
  { x: 0.1, y: 0.2 },
  { x: 0.3, y: 0.4 },
  { x: 0.5, y: 0.6 },
  { x: 0.7, y: 0.8 }
];

Route with Timestamps

const timedRoute = [
  { x: 0.2, y: 0.3, marked: 0 },              // Start marker at 0s
  { x: 0.4, y: 0.4, start: 0, end: 10 },      // First segment: 0-10s
  { x: 0.5, y: 0.6, start: 10, end: 25 },     // Second segment: 10-25s
  { x: 0.7, y: 0.7, start: 25, end: 40 },     // Third segment: 25-40s
  { x: 0.8, y: 0.8, marked: 40 }              // End marker at 40s
];

Route with Custom Data

const routeWithMetadata = [
  {
    x: 0.2,
    y: 0.3,
    marked: 0,
    data: "Trail Start - Parking Area"
  },
  {
    x: 0.4,
    y: 0.5,
    start: 0,
    end: 15,
    data: "Forest Path"
  },
  {
    x: 0.6,
    y: 0.6,
    start: 15,
    end: 30,
    data: "Creek Crossing"
  },
  {
    x: 0.8,
    y: 0.8,
    marked: 30,
    data: "Summit - 2,500ft"
  }
];

Complex Multi-Stop Route

const complexRoute = [
  { x: 0.15, y: 0.2, marked: 0, data: "Start" },
  { x: 0.25, y: 0.3, start: 0, end: 5 },
  { x: 0.35, y: 0.35, marked: 5, data: "Waypoint 1" },
  { x: 0.45, y: 0.4, start: 5, end: 12 },
  { x: 0.55, y: 0.5, marked: 12, data: "Waypoint 2" },
  { x: 0.65, y: 0.6, start: 12, end: 20 },
  { x: 0.75, y: 0.7, marked: 20, data: "Waypoint 3" },
  { x: 0.85, y: 0.8, start: 20, end: 28 },
  { x: 0.9, y: 0.85, marked: 28, data: "End" }
];

Tips & Best Practices

Performance

User Experience

Responsive Design

Accessibility