Skip to content

Commit af9718c

Browse files
dfallingclaude
andauthored
Fetch and render trip elements on the map (#3)
Adds an Elements GraphQL query bound to the visible map viewport and renders each element with a location as an emoji-icon pin. The query is gated on the initial fly-to so we don't fetch the whole world at the default zoom-1 framing, and re-runs as the user pans/zooms via onRegionDidChange. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c2bf3c7 commit af9718c

3 files changed

Lines changed: 127 additions & 3 deletions

File tree

src/graphql/__generated__/types.ts

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
query Elements($bounds: GeoBounds) {
2+
elements(bounds: $bounds) {
3+
id
4+
name
5+
icon
6+
location {
7+
id
8+
latitude
9+
longitude
10+
}
11+
}
12+
}

src/map/MapScreen.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ import {
33
type CameraRef,
44
LocationManager,
55
Map as MapLibreMap,
6+
Marker,
67
UserLocation,
78
useCurrentPosition,
9+
type ViewStateChangeEvent,
810
} from '@maplibre/maplibre-react-native';
9-
import {useEffect, useRef, useState} from 'react';
10-
import {StyleSheet, View} from 'react-native';
11+
import {useCallback, useEffect, useRef, useState} from 'react';
12+
import type {NativeSyntheticEvent} from 'react-native';
13+
import {StyleSheet, Text, View} from 'react-native';
14+
import {
15+
type ElementsQueryVariables,
16+
useElementsQuery,
17+
} from '../graphql/__generated__/types';
1118

1219
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
1320
const USER_ZOOM = 14;
@@ -42,11 +49,44 @@ export function MapScreen() {
4249
});
4350
}, [position]);
4451

52+
const [bounds, setBounds] = useState<ElementsQueryVariables['bounds']>();
53+
54+
const onRegionDidChange = useCallback(
55+
(event: NativeSyntheticEvent<ViewStateChangeEvent>) => {
56+
// Skip viewport events until we've flown to the user's location, so
57+
// we don't fetch the entire world at the initial zoom-1 framing.
58+
if (!hasCenteredRef.current) return;
59+
const [west, south, east, north] = event.nativeEvent.bounds;
60+
setBounds({left: west, bottom: south, right: east, top: north});
61+
},
62+
[],
63+
);
64+
65+
const {data} = useElementsQuery({
66+
skip: !bounds,
67+
variables: bounds ? {bounds} : undefined,
68+
});
69+
4570
return (
4671
<View style={styles.container}>
47-
<MapLibreMap mapStyle={MAP_STYLE} style={styles.map}>
72+
<MapLibreMap
73+
mapStyle={MAP_STYLE}
74+
style={styles.map}
75+
onRegionDidChange={onRegionDidChange}>
4876
<Camera ref={cameraRef} initialViewState={{center: [0, 20], zoom: 1}} />
4977
<UserLocation animated accuracy />
78+
{data?.elements.map(el =>
79+
el.location ? (
80+
<Marker
81+
key={el.id}
82+
id={el.id}
83+
lngLat={[el.location.longitude, el.location.latitude]}>
84+
<View style={styles.pin}>
85+
{el.icon ? <Text style={styles.pinIcon}>{el.icon}</Text> : null}
86+
</View>
87+
</Marker>
88+
) : null,
89+
)}
5090
</MapLibreMap>
5191
</View>
5292
);
@@ -59,4 +99,19 @@ const styles = StyleSheet.create({
5999
map: {
60100
flex: 1,
61101
},
102+
pin: {
103+
width: 36,
104+
height: 36,
105+
borderRadius: 18,
106+
backgroundColor: '#1d6fe0',
107+
borderWidth: 2,
108+
borderColor: '#ffffff',
109+
alignItems: 'center',
110+
justifyContent: 'center',
111+
},
112+
pinIcon: {
113+
fontSize: 18,
114+
lineHeight: 22,
115+
textAlign: 'center',
116+
},
62117
});

0 commit comments

Comments
 (0)