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
98 changes: 98 additions & 0 deletions docs/GEOJSON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# GeoJson

Renders [GeoJSON](https://geojson.org/) data on the map using Marker, Polyline, and Polygon components.

## Usage

```tsx
import { MapView, GeoJson } from '@lugg/maps';

const geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-122.4194, 37.7749],
},
properties: { title: 'San Francisco' },
},
{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[
[-122.428, 37.784],
[-122.422, 37.784],
[-122.422, 37.779],
[-122.428, 37.779],
[-122.428, 37.784],
]],
},
properties: { fill: 'rgba(66, 133, 244, 0.3)', stroke: '#4285F4' },
},
],
};

<MapView style={{ flex: 1 }}>
<GeoJson geojson={geojson} />
</MapView>
```

### Custom Rendering

Use render callbacks to customize how features are rendered:

```tsx
<GeoJson
geojson={data}
renderMarker={(props, feature) => (
<Marker {...props} title={feature.properties?.name}>
<CustomPin />
</Marker>
)}
renderPolygon={(props, feature) => (
<Polygon {...props} fillColor={feature.properties?.color} />
)}
/>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `geojson` | `GeoJSON` | **required** | GeoJSON object (FeatureCollection, Feature, or Geometry) |
| `strokeColor` | `ColorValue` | - | Default stroke color for polylines and polygons |
| `strokeWidth` | `number` | - | Default stroke width |
| `fillColor` | `ColorValue` | - | Default fill color for polygons |
| `zIndex` | `number` | - | Z-index for all rendered components |
| `renderMarker` | `(props, feature) => ReactElement` | - | Custom marker renderer |
| `renderPolyline` | `(props, feature) => ReactElement` | - | Custom polyline renderer |
| `renderPolygon` | `(props, feature) => ReactElement` | - | Custom polygon renderer |

## Geometry Mapping

| GeoJSON Type | Renders As |
|---|---|
| Point | `<Marker>` |
| MultiPoint | Multiple `<Marker>` |
| LineString | `<Polyline>` |
| MultiLineString | Multiple `<Polyline>` |
| Polygon | `<Polygon>` (with holes) |
| MultiPolygon | Multiple `<Polygon>` |
| GeometryCollection | Recursive rendering |

## Feature Properties (simplestyle-spec)

Per-feature styling via `feature.properties` overrides component-level props:

| Property | Maps To |
|---|---|
| `title` | Marker `title` |
| `description` | Marker `description` |
| `stroke` | Polyline `strokeColors[0]` / Polygon `strokeColor` |
| `stroke-width` | Polyline/Polygon `strokeWidth` |
| `fill` | Polygon `fillColor` |

Precedence: render callback > feature properties > component props.
159 changes: 145 additions & 14 deletions example/shared/src/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useRef, useState, useCallback } from 'react';
import {
StyleSheet,
View,
Text,
TextInput,
Platform,
useColorScheme,
useWindowDimensions,
} from 'react-native';
import {
Expand All @@ -12,6 +13,7 @@ import {
type MapProviderType,
type MapCameraEvent,
type MapPressEvent,
type GeoJSON,
} from '@lugg/maps';
import {
TrueSheet,
Expand All @@ -24,7 +26,7 @@ import {
useReanimatedTrueSheet,
} from '@lodev09/react-native-true-sheet/reanimated';

import { Button, Map } from './components';
import { Button, Map, ThemedText } from './components';
import { randomFrom, randomLetter } from './utils';
import {
MARKER_COLORS,
Expand All @@ -34,14 +36,25 @@ import {
} from './markers';
import { useLocationPermission } from './useLocationPermission';

const GEOJSON_PRESETS = [
{
name: 'California Counties',
url: 'https://raw.githubusercontent.com/codeforgermany/click_that_hood/main/public/data/california-counties.geojson',
},
{
name: 'San Francisco Neighborhoods',
url: 'https://raw.githubusercontent.com/codeforgermany/click_that_hood/main/public/data/san-francisco.geojson',
},
];

const bottomEdgeInsets = (bottom: number) => ({
top: 0,
left: 0,
bottom,
right: 0,
});

export function Home() {
export const Home = () => {
const apiKey = process.env.GOOGLE_MAPS_API_KEY;

return (
Expand All @@ -53,18 +66,23 @@ export function Home() {
</ReanimatedTrueSheetProvider>
</TrueSheetProvider>
);
}
};

function HomeContent() {
const HomeContent = () => {
const mapRef = useRef<MapView>(null);
const sheetRef = useRef<TrueSheet>(null);
const geojsonSheetRef = useRef<TrueSheet>(null);
const { height: screenHeight } = useWindowDimensions();
const isDark = useColorScheme() === 'dark';
const locationPermission = useLocationPermission();
const { animatedPosition } = useReanimatedTrueSheet();
const [provider, setProvider] = useState<MapProviderType>('apple');
const [showMap, setShowMap] = useState(true);
const [markers, setMarkers] = useState(INITIAL_MARKERS);
const [statusText, setStatusText] = useState('Loading...');
const [status, setStatus] = useState({ text: 'Loading...', error: false });
const [geojson, setGeojson] = useState<GeoJSON | null>(null);
const [geojsonUrl, setGeojsonUrl] = useState('');
const [loadingGeojson, setLoadingGeojson] = useState(false);
const lastCoordinate = useRef({ latitude: 37.78, longitude: -122.43 });
const statusLockRef = useRef(false);

Expand Down Expand Up @@ -111,7 +129,10 @@ function HomeContent() {
const lng = coordinate.longitude.toFixed(5);
const px = point.x.toFixed(0);
const py = point.y.toFixed(0);
setStatusText(`${label}: ${lat}, ${lng} (${px}, ${py})`);
setStatus({
text: `${label}: ${lat}, ${lng} (${px}, ${py})`,
error: false,
});
},
[lockStatus]
);
Expand All @@ -129,7 +150,7 @@ function HomeContent() {
: gesture
? ' (gesture)'
: '';
setStatusText(pos + suffix);
setStatus({ text: pos + suffix, error: false });
},
[]
);
Expand Down Expand Up @@ -178,6 +199,24 @@ function HomeContent() {
});
};

const loadGeojson = async (url: string) => {
if (!url.trim()) return;
setLoadingGeojson(true);
lockStatus();
setStatus({ text: 'Loading GeoJSON...', error: false });
try {
const res = await fetch(url.trim());
const data = await res.json();
setGeojson(data);
setStatus({ text: 'GeoJSON loaded', error: false });
geojsonSheetRef.current?.dismiss();
} catch (e: any) {
setStatus({ text: `GeoJSON: ${e.message}`, error: true });
} finally {
setLoadingGeojson(false);
}
};

return (
<View style={styles.container}>
{showMap && (
Expand All @@ -186,6 +225,7 @@ function HomeContent() {
ref={mapRef}
provider={provider}
markers={markers}
geojson={geojson}
animatedPosition={animatedPosition}
userLocationEnabled={locationPermission}
onReady={handleMapReady}
Expand All @@ -206,7 +246,7 @@ function HomeContent() {
onMarkerDragEnd={(e, m) => formatPressEvent(e, `Drag end(${m.name})`)}
onPolygonPress={() => {
lockStatus();
setStatusText('Polygon pressed');
setStatus({ text: 'Polygon pressed', error: false });
}}
/>
)}
Expand All @@ -223,55 +263,146 @@ function HomeContent() {
onDidPresent={handleSheetPresent}
onDetentChange={handleDetentChange}
>
<Text style={styles.statusText}>{statusText}</Text>
<ThemedText
style={[styles.statusText, status.error && styles.statusError]}
>
{status.text}
</ThemedText>
<View style={styles.sheetContent}>
<Button title="Add Marker" onPress={() => addMarker()} />
<Button
style={styles.sheetButton}
title="Add Marker"
onPress={() => addMarker()}
/>
<Button
style={styles.sheetButton}
title={`Remove Marker (${markers.length})`}
onPress={removeRandomMarker}
disabled={markers.length === 0}
/>
<Button
style={styles.sheetButton}
title="Clear Markers"
onPress={() => setMarkers([])}
disabled={markers.length === 0}
/>
<Button title="Move Camera" onPress={moveToRandomMarker} />
<Button
style={styles.sheetButton}
title="Move Camera"
onPress={moveToRandomMarker}
/>
<Button
style={styles.sheetButton}
title="Fit Markers"
onPress={fitAllMarkers}
disabled={markers.length === 0}
/>
<Button
style={styles.sheetButton}
title={showMap ? 'Hide Map' : 'Show Map'}
onPress={() => setShowMap((prev) => !prev)}
/>
<Button
style={styles.sheetButton}
title={provider === 'google' ? 'Apple Maps' : 'Google Maps'}
disabled={Platform.OS !== 'ios'}
onPress={() =>
setProvider((p) => (p === 'google' ? 'apple' : 'google'))
}
/>
<Button
style={styles.sheetButton}
title={geojson ? 'GeoJSON (loaded)' : 'Load GeoJSON'}
onPress={() => geojsonSheetRef.current?.present()}
/>
</View>
</ReanimatedTrueSheet>

<TrueSheet
ref={geojsonSheetRef}
detents={['auto']}
style={styles.geojsonSheet}
>
<ThemedText variant="title">Load GeoJSON</ThemedText>
<TextInput
style={[styles.urlInput, isDark && styles.urlInputDark]}
placeholder="Enter GeoJSON URL..."
placeholderTextColor={isDark ? '#666' : '#999'}
value={geojsonUrl}
onChangeText={setGeojsonUrl}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
/>
<Button
title={loadingGeojson ? 'Loading...' : 'Fetch'}
onPress={() => loadGeojson(geojsonUrl)}
disabled={loadingGeojson || !geojsonUrl.trim()}
/>
<ThemedText variant="caption">Presets</ThemedText>
{GEOJSON_PRESETS.map((preset) => (
<Button
key={preset.name}
title={preset.name}
onPress={() => {
setGeojsonUrl(preset.url);
loadGeojson(preset.url);
}}
disabled={loadingGeojson}
/>
))}
{geojson && (
<Button
title="Clear GeoJSON"
onPress={() => {
setGeojson(null);
setGeojsonUrl('');
geojsonSheetRef.current?.dismiss();
}}
/>
)}
</TrueSheet>
</View>
);
}
};

const styles = StyleSheet.create({
container: { flex: 1 },
statusText: {
fontSize: 14,
color: '#666',
},
statusError: {
color: '#D32F2F',
},
sheet: {
padding: 24,
gap: 12,
},
sheetContent: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
sheetButton: {
flex: 1,
minWidth: '45%',
},
geojsonSheet: {
padding: 24,
gap: 12,
},
urlInput: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#DDD',
borderRadius: 8,
padding: 12,
fontSize: 14,
backgroundColor: '#FFF',
color: '#000',
},
urlInputDark: {
backgroundColor: '#1C1C1E',
borderColor: '#333',
color: '#FFF',
},
});
Loading