Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/navigation/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: "Turn-by-turn navigation for React Native with the Mapbox Navigatio
image: /img/navigation/social-card.jpg
---

import PhoneVideo from '@site/src/components/PhoneVideo';

<head>
<script type="application/ld+json">
{JSON.stringify({
Expand All @@ -28,6 +30,15 @@ image: /img/navigation/social-card.jpg

It requests routes, runs an active-guidance session and reports route progress. The Navigation SDK draws the route line, the puck and the navigation camera on a regular `@rnmapbox/maps` `MapView`, so your existing layers, sources and annotations keep working on the same map.

<PhoneVideo
src="/img/navigation/navigation-demo"
poster="/img/navigation/navigation-demo.webp"
width={324}
height={720}
label="Simulated drive to the Ferry Building in San Francisco with the Mapbox Navigation SDK UI: maneuver banner, route line and a camera following the puck"
caption="<NavigationView> with a simulated route, on Android."
/>

:::caution Alpha
`@rnmapbox/navigation` is in early alpha. The current release is `0.1.0-alpha.1`, published to npm under the `next` tag, and APIs may change between releases.
:::
Expand Down
4 changes: 0 additions & 4 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,6 @@ const config = {
label: 'Discord',
href: 'https://discord.com/channels/1004826913229000704/1092426983696248893',
},
{
label: 'Gitter',
href: 'https://twitter.com/docusaurus',
},
],
},
{
Expand Down
65 changes: 0 additions & 65 deletions src/components/HomepageFeatures/index.js

This file was deleted.

11 changes: 0 additions & 11 deletions src/components/HomepageFeatures/styles.module.css

This file was deleted.

99 changes: 99 additions & 0 deletions src/components/LiveMapDemo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, {useEffect, useRef, useState} from 'react';
import clsx from 'clsx';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';

const DEMO_ROUTE = '/example-app/V10/GlobeProjection';
const TILE_REQUEST = /api\.mapbox\.com\/(v4|tiles)\//;
const REVEAL_TIMEOUT_MS = 8000;

// The example app's token is URL-restricted, so tiles can fail (e.g. on localhost or previews).
// Tile response codes are only exposed via Resource Timing in Chromium; elsewhere we reveal after a timeout.
function tileStatus(frame) {
let entries;
try {
entries = frame.contentWindow.performance.getEntriesByType('resource');
} catch {
return undefined;
}
const tile = entries.find((e) => TILE_REQUEST.test(e.name) && e.responseStatus);
return tile?.responseStatus;
}

// The demo is the example app's web export (a ~5 MB bundle), so it only loads after an explicit click.
export default function LiveMapDemo() {
const [state, setState] = useState('poster');
const frameRef = useRef(null);
const demoUrl = useBaseUrl(DEMO_ROUTE);
const posterUrl = useBaseUrl('/img/home/live-demo-globe.webp');

useEffect(() => {
if (state !== 'loading') {
return undefined;
}
const started = Date.now();
const timer = setInterval(() => {
const status = tileStatus(frameRef.current);
if (status >= 200 && status < 300) {
setState('live');
} else if (status >= 400) {
setState('failed');
} else if (Date.now() - started > REVEAL_TIMEOUT_MS) {
setState('live');
}
}, 250);
return () => clearInterval(timer);
}, [state]);

const showFrame = state === 'loading' || state === 'live';

return (
<figure className={styles.demo}>
<div className={styles.phone}>
<div className={styles.screen} aria-busy={state === 'loading'}>
{showFrame && (
<iframe
ref={frameRef}
className={styles.frame}
src={demoUrl}
title="Live @rnmapbox/maps globe projection example"
allow="fullscreen"
/>
)}
{state !== 'live' && (
<div className={clsx(styles.cover, state === 'loading' && styles.coverLoading)}>
<img
className={styles.poster}
src={posterUrl}
width={300}
height={620}
fetchpriority="high"
alt="Satellite map of northern Europe rendered by the @rnmapbox/maps globe projection example"
/>
{state === 'poster' && (
<a
className={styles.play}
href={demoUrl}
onClick={(e) => {
e.preventDefault();
setState('loading');
}}>
<span className={styles.playIcon} aria-hidden="true">▶</span>
Run the live map
</a>
)}
{state === 'loading' && <span className={styles.badge}>Loading live map…</span>}
{state === 'failed' && <span className={styles.badge}>Live map unavailable here</span>}
</div>
)}
</div>
</div>
<figcaption className={styles.caption}>
The @rnmapbox/maps example app, built for the web.{' '}
<a href={demoUrl} target="_blank" rel="noopener">
Open full screen
</a>
</figcaption>
</figure>
);
}
119 changes: 119 additions & 0 deletions src/components/LiveMapDemo/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
.demo {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
}

.phone {
padding: 12px;
border-radius: 44px;
background: #1c1f26;
box-shadow:
0 0 0 2px #3a3f4b,
0 30px 60px -20px rgba(0, 30, 80, 0.45);
}

.screen {
position: relative;
width: 300px;
height: 620px;
border-radius: 32px;
overflow: hidden;
background: #0b1a2b;
}

.poster,
.frame {
display: block;
width: 100%;
height: 100%;
border: 0;
}

.poster {
object-fit: cover;
}

.cover {
position: absolute;
inset: 0;
}

.coverLoading .poster {
filter: saturate(0.6) brightness(0.85);
}

.badge {
position: absolute;
left: 50%;
bottom: 72px;
transform: translateX(-50%);
white-space: nowrap;
padding: 0.45rem 0.9rem;
border-radius: 999px;
background: rgba(11, 26, 43, 0.85);
color: #fff;
font-size: 0.85rem;
}

.play {
position: absolute;
left: 50%;
bottom: 72px;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
padding: 0.7rem 1.2rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.95);
color: #0b1a2b;
font-weight: 600;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
transition: transform 0.15s ease;
}

.play:hover {
color: #0b1a2b;
text-decoration: none;
transform: translateX(-50%) scale(1.04);
}

.playIcon {
display: inline-grid;
place-items: center;
width: 1.6rem;
height: 1.6rem;
border-radius: 50%;
background: var(--mbx-blue);
color: #fff;
font-size: 0.7rem;
}

.caption {
margin-top: 1rem;
max-width: 324px;
font-size: 0.85rem;
text-align: center;
color: var(--ifm-color-emphasis-700);
}

@media screen and (max-width: 996px) {
.phone {
padding: 10px;
border-radius: 36px;
}

.screen {
width: 240px;
height: 496px;
border-radius: 26px;
}

.play,
.badge {
bottom: 56px;
}
}
53 changes: 53 additions & 0 deletions src/components/PhoneVideo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {useEffect, useRef} from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';

// Plays only while on screen, so the video isn't downloaded until it's scrolled into view.
// Reduced-motion users and no-JS visitors see the poster.
export default function PhoneVideo({src, poster, width, height, label, caption}) {
const videoRef = useRef(null);
const webm = useBaseUrl(`${src}.webm`);
const mp4 = useBaseUrl(`${src}.mp4`);
const posterUrl = useBaseUrl(poster);

useEffect(() => {
const video = videoRef.current;
if (!video || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
return undefined;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
video.play().catch(() => {});
} else {
video.pause();
}
},
{threshold: 0.25},
);
observer.observe(video);
return () => observer.disconnect();
}, []);

return (
<figure className={styles.demo}>
<div className={styles.phone}>
<video
ref={videoRef}
className={styles.screen}
poster={posterUrl}
width={width}
height={height}
muted
loop
playsInline
preload="none"
aria-label={label}>
<source src={webm} type="video/webm" />
<source src={mp4} type="video/mp4" />
</video>
</div>
{caption && <figcaption className={styles.caption}>{caption}</figcaption>}
</figure>
);
}
Loading
Loading