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
5 changes: 4 additions & 1 deletion example/bare/src/screens/DetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MarkerDetailScreen } from '@lugg/shared-example';

import type { RootStackParamList } from '../App';

type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>;

export function DetailScreen({ route }: Props) {
return <MarkerDetailScreen name={route.params.name} />;
const { bottom } = useSafeAreaInsets();

return <MarkerDetailScreen name={route.params.name} bottomInset={bottom} />;
}
5 changes: 4 additions & 1 deletion example/expo/app/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useLocalSearchParams } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MarkerDetailScreen } from '@lugg/shared-example';

export default function DetailScreen() {
const { name } = useLocalSearchParams<{ name: string }>();
return <MarkerDetailScreen name={name ?? ''} />;
const { bottom } = useSafeAreaInsets();

return <MarkerDetailScreen name={name ?? ''} bottomInset={bottom} />;
}
35 changes: 22 additions & 13 deletions example/shared/src/screens/MarkerDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,32 @@ import {

interface MarkerDetailScreenProps {
name: string;
bottomInset?: number;
}

const CARD_HEIGHT = 200;
const CARD_BOTTOM = sizes.xl * 3;

const formatCoordinate = (value: number) => value.toFixed(4);

export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
export const MarkerDetailScreen = ({
name,
bottomInset = 0,
}: MarkerDetailScreenProps) => {
const { colors } = useTheme();
const { width, height } = useWindowDimensions();
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
const [provider, setProvider] = useState<MapProviderType>(
Platform.OS === 'ios' ? 'apple' : 'google'
);

const cardBottom = bottomInset + sizes.lg;
const edgeInsets = {
top: 0,
left: 0,
right: 0,
bottom: cardBottom + CARD_HEIGHT,
};

const marker = INITIAL_MARKERS.find((m) => m.name === name);
const place = PLACES.find((p) => p.name === name);

Expand All @@ -42,8 +53,9 @@ export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
? fittedCamera(
placeCoordinates(place),
provider,
{ width, height: height - CARD_BOTTOM - CARD_HEIGHT },
sizes.xl
{ width, height },
sizes.xl,
edgeInsets
)
: { coordinate, zoom: 15 };

Expand All @@ -58,12 +70,7 @@ export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
staticKey={name}
initialCoordinate={camera.coordinate}
initialZoom={camera.zoom}
edgeInsets={{
top: 0,
left: 0,
right: 0,
bottom: CARD_BOTTOM + CARD_HEIGHT,
}}
edgeInsets={edgeInsets}
>
{place ? (
<>
Expand All @@ -76,7 +83,10 @@ export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
</MapView>
</MapProvider>
<Button
style={styles.providerButton}
style={{
...styles.providerButton,
bottom: edgeInsets.bottom + sizes.lg,
}}
title={provider === 'google' ? 'Apple Maps' : 'Google Maps'}
disabled={Platform.OS !== 'ios'}
onPress={() =>
Expand All @@ -87,6 +97,7 @@ export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
style={[
styles.overlay,
{
bottom: cardBottom,
backgroundColor: colors.backgroundElevated,
shadowColor: colors.shadow,
},
Expand Down Expand Up @@ -115,13 +126,11 @@ const styles = StyleSheet.create({
providerButton: {
position: 'absolute',
right: sizes.lg,
bottom: CARD_BOTTOM + CARD_HEIGHT + sizes.lg,
},
overlay: {
position: 'absolute',
left: sizes.lg,
right: sizes.lg,
bottom: CARD_BOTTOM,
height: CARD_HEIGHT,
padding: sizes.xl,
gap: sizes.sm,
Expand Down
21 changes: 14 additions & 7 deletions example/shared/src/screens/StaticMapsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Marker,
Polyline,
type Coordinate,
type EdgeInsets,
type MapProviderType,
} from '@lugg/maps';

Expand Down Expand Up @@ -157,16 +158,17 @@ const mercatorX = (longitude: number) => (longitude + 180) / 360;
const mercatorY = (latitude: number) =>
(1 - Math.asinh(Math.tan((latitude * Math.PI) / 180)) / Math.PI) / 2;

// Camera that fits the coordinates in the given view size, derived up
// Camera that fits the coordinates in the view's inset viewport, derived up
// front so static maps center their content without imperative commands.
// Matches the native static framing: Google shows the world 256 * 2^zoom
// points wide; Apple fits a square span rect to the view's short side
// (see LuggStaticFittedMapRect)
// points wide; Apple fits a square span rect to the full view's short side
// (see LuggStaticFittedMapRect), and edge insets only shift the center
export const fittedCamera = (
coordinates: Coordinate[],
provider: MapProviderType,
size: { width: number; height: number },
padding: number
padding: number,
insets: Partial<EdgeInsets> = {}
): { coordinate: Coordinate; zoom: number } => {
const xs = coordinates.map((c) => mercatorX(c.longitude));
const ys = coordinates.map((c) => mercatorY(c.latitude));
Expand All @@ -182,10 +184,15 @@ export const fittedCamera = (
longitude: ((minX + maxX) / 2) * 360 - 180,
};

// World fraction per view point at which the bounds fit the padded view
const availWidth =
size.width - (insets.left ?? 0) - (insets.right ?? 0) - padding * 2;
const availHeight =
size.height - (insets.top ?? 0) - (insets.bottom ?? 0) - padding * 2;

// World fraction per view point at which the bounds fit the padded viewport
const scale = Math.max(
(maxX - minX) / Math.max(size.width - padding * 2, 1),
(maxY - minY) / Math.max(size.height - padding * 2, 1)
(maxX - minX) / Math.max(availWidth, 1),
(maxY - minY) / Math.max(availHeight, 1)
);

const zoom =
Expand Down
14 changes: 13 additions & 1 deletion ios/core/AppleStaticMapProvider.mm
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,19 @@ - (void)renderInsetBaseMap {
_poiEnabled, _poiFilterMode, _poiFilterCategories);
mapView.overrideUserInterfaceStyle =
[self snapshotTraitCollection].userInterfaceStyle;
[mapView setVisibleMapRect:self.mapRect
// MKMapView fits the rect into the layout-margin viewport, and mapRect
// is already inset-shifted for the whole view: pass its viewport slice so
// the fit puts mapRect over the full view, matching the overlay projection
MKMapRect mapRect = self.mapRect;
CGSize size = self.projectedSize;
UIEdgeInsets insets = self.edgeInsets;
double scale = mapRect.size.width / size.width;
MKMapRect viewportRect =
MKMapRectMake(mapRect.origin.x + insets.left * scale,
mapRect.origin.y + insets.top * scale,
(size.width - insets.left - insets.right) * scale,
(size.height - insets.top - insets.bottom) * scale);
[mapView setVisibleMapRect:viewportRect
edgePadding:UIEdgeInsetsZero
animated:NO];
_warmupMapView = mapView;
Expand Down
14 changes: 7 additions & 7 deletions skills/maps-usage/references/advanced-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Carry the camera over by storing the last `onCameraIdle` payload and feeding it

## Fitted camera for static maps

Static maps can't animate, so compute the camera before mount instead of calling `fitCoordinates` in `onReady`. This mirrors the native static framing (Google: world is `256 · 2^zoom` points wide; Apple fits a square span to the short side):
Static maps can't animate, so compute the camera before mount instead of calling `fitCoordinates` in `onReady`. This mirrors the native static framing (Google: world is `256 · 2^zoom` points wide; Apple fits a square span to the full view's short side). Pass the same `edgeInsets` you give the `MapView`: the content fits the inset viewport, but the Apple zoom still derives from the full view size because insets only shift the center:

```ts
const mercatorX = (lng: number) => (lng + 180) / 360;
Expand All @@ -233,7 +233,8 @@ export const fittedCamera = (
coordinates: Coordinate[],
provider: MapProviderType,
size: { width: number; height: number },
padding: number
padding: number,
insets: Partial<EdgeInsets> = {}
) => {
const xs = coordinates.map((c) => mercatorX(c.longitude));
const ys = coordinates.map((c) => mercatorY(c.latitude));
Expand All @@ -243,10 +244,9 @@ export const fittedCamera = (
latitude: (Math.atan(Math.sinh(Math.PI * (1 - 2 * centerY))) * 180) / Math.PI,
longitude: ((minX + maxX) / 2) * 360 - 180,
};
const scale = Math.max(
(maxX - minX) / Math.max(size.width - padding * 2, 1),
(maxY - minY) / Math.max(size.height - padding * 2, 1)
);
const availWidth = size.width - (insets.left ?? 0) - (insets.right ?? 0) - padding * 2;
const availHeight = size.height - (insets.top ?? 0) - (insets.bottom ?? 0) - padding * 2;
const scale = Math.max((maxX - minX) / Math.max(availWidth, 1), (maxY - minY) / Math.max(availHeight, 1));
const zoom =
provider === 'apple'
? 0.5 + Math.log2(1 / (scale * Math.min(size.width, size.height) * Math.cos((coordinate.latitude * Math.PI) / 180)))
Expand All @@ -255,7 +255,7 @@ export const fittedCamera = (
};
```

Use with `initialCoordinate={camera.coordinate} initialZoom={camera.zoom}`. For a single point just pass it with a fixed zoom.
Use with `initialCoordinate={camera.coordinate} initialZoom={camera.zoom}`. For a single point just pass it with a fixed zoom. For a detail screen with a bottom card, pass the full window size plus `{ bottom: cardHeight }` as `insets` and the same object as the map's `edgeInsets`.

## Static map list with detail hand-off

Expand Down
Loading