diff --git a/docs/GEOJSON.md b/docs/GEOJSON.md new file mode 100644 index 0000000..cf0d20b --- /dev/null +++ b/docs/GEOJSON.md @@ -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' }, + }, + ], +}; + + + + +``` + +### Custom Rendering + +Use render callbacks to customize how features are rendered: + +```tsx + ( + + + + )} + renderPolygon={(props, feature) => ( + + )} +/> +``` + +## 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 | `` | +| MultiPoint | Multiple `` | +| LineString | `` | +| MultiLineString | Multiple `` | +| Polygon | `` (with holes) | +| MultiPolygon | Multiple `` | +| 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. diff --git a/example/shared/src/Home.tsx b/example/shared/src/Home.tsx index 67ba56d..3a1572d 100644 --- a/example/shared/src/Home.tsx +++ b/example/shared/src/Home.tsx @@ -2,8 +2,9 @@ import { useRef, useState, useCallback } from 'react'; import { StyleSheet, View, - Text, + TextInput, Platform, + useColorScheme, useWindowDimensions, } from 'react-native'; import { @@ -12,6 +13,7 @@ import { type MapProviderType, type MapCameraEvent, type MapPressEvent, + type GeoJSON, } from '@lugg/maps'; import { TrueSheet, @@ -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, @@ -34,6 +36,17 @@ 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, @@ -41,7 +54,7 @@ const bottomEdgeInsets = (bottom: number) => ({ right: 0, }); -export function Home() { +export const Home = () => { const apiKey = process.env.GOOGLE_MAPS_API_KEY; return ( @@ -53,18 +66,23 @@ export function Home() { ); -} +}; -function HomeContent() { +const HomeContent = () => { const mapRef = useRef(null); const sheetRef = useRef(null); + const geojsonSheetRef = useRef(null); const { height: screenHeight } = useWindowDimensions(); + const isDark = useColorScheme() === 'dark'; const locationPermission = useLocationPermission(); const { animatedPosition } = useReanimatedTrueSheet(); const [provider, setProvider] = useState('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(null); + const [geojsonUrl, setGeojsonUrl] = useState(''); + const [loadingGeojson, setLoadingGeojson] = useState(false); const lastCoordinate = useRef({ latitude: 37.78, longitude: -122.43 }); const statusLockRef = useRef(false); @@ -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] ); @@ -129,7 +150,7 @@ function HomeContent() { : gesture ? ' (gesture)' : ''; - setStatusText(pos + suffix); + setStatus({ text: pos + suffix, error: false }); }, [] ); @@ -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 ( {showMap && ( @@ -186,6 +225,7 @@ function HomeContent() { ref={mapRef} provider={provider} markers={markers} + geojson={geojson} animatedPosition={animatedPosition} userLocationEnabled={locationPermission} onReady={handleMapReady} @@ -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 }); }} /> )} @@ -223,48 +263,117 @@ function HomeContent() { onDidPresent={handleSheetPresent} onDetentChange={handleDetentChange} > - {statusText} + + {status.text} + -