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:
- Standalone Points: Use
markedproperty for points without connecting paths (start/end markers) - Path Segments: Use
startandendproperties to define segments with duration
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
- Pan: Right-click and drag to move around the map
- Zoom: Scroll mouse wheel up/down to zoom in/out
- Click: Left-click on route points to jump to that timestamp (if
onProgressChangeis provided)
Touch Gestures
- Pan: Single-finger drag to move the map
- Zoom: Pinch with two fingers to zoom in/out
- Tap: Tap on route points to jump to that timestamp
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
- Keep route arrays under 100 points for optimal performance
- Use memoization for custom renderers to avoid unnecessary re-renders
- Optimize images (use WebP or optimized JPG/PNG)
User Experience
- Provide visual feedback for interactive elements (cursor changes, hover states)
- Add loading states while images are loading
- Include instructions for first-time users (right-click to pan, scroll to zoom)
- Use the
dataproperty to add helpful labels to points
Responsive Design
- Set container dimensions using CSS (percentage or viewport units)
- Test on mobile devices to ensure touch gestures work well
- Consider different aspect ratios when designing routes
Accessibility
- Provide alternative text descriptions for maps
- Ensure sufficient color contrast in custom renderers
- Consider keyboard navigation for users who can't use a mouse