From 9fed705ec10a0169ebf5fdd3262f6948e588b1e3 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 02:59:57 +0800 Subject: [PATCH 1/8] feat: add GeoJson component for rendering GeoJSON data --- docs/GEOJSON.md | 98 ++++++++++++++ src/components/GeoJson.tsx | 251 ++++++++++++++++++++++++++++++++++++ src/components/index.ts | 2 + src/components/index.web.ts | 2 + src/geojson.types.ts | 66 ++++++++++ src/index.ts | 9 ++ src/index.web.ts | 9 ++ 7 files changed, 437 insertions(+) create mode 100644 docs/GEOJSON.md create mode 100644 src/components/GeoJson.tsx create mode 100644 src/geojson.types.ts 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/src/components/GeoJson.tsx b/src/components/GeoJson.tsx new file mode 100644 index 0000000..9e1c7b8 --- /dev/null +++ b/src/components/GeoJson.tsx @@ -0,0 +1,251 @@ +import React, { useMemo, type ReactElement } from 'react'; +import type { ColorValue } from 'react-native'; +import type { + Feature, + FeatureCollection, + GeoJSON, + Geometry, + Position, +} from '../geojson.types'; +import type { Coordinate } from '../types'; +import { Marker, type MarkerProps } from './Marker'; +import { Polygon, type PolygonProps } from './Polygon'; +import { Polyline, type PolylineProps } from './Polyline'; + +export interface GeoJsonProps { + geojson: GeoJSON; + strokeColor?: ColorValue; + strokeWidth?: number; + fillColor?: ColorValue; + zIndex?: number; + renderMarker?: (props: MarkerProps, feature: Feature) => ReactElement | null; + renderPolyline?: ( + props: PolylineProps, + feature: Feature + ) => ReactElement | null; + renderPolygon?: ( + props: PolygonProps, + feature: Feature + ) => ReactElement | null; +} + +function toCoordinate(position: Position): Coordinate { + return { latitude: position[1], longitude: position[0] }; +} + +function toCoordinates(positions: Position[]): Coordinate[] { + return positions.map(toCoordinate); +} + +function normalizeFeatures(geojson: GeoJSON): Feature[] { + switch (geojson.type) { + case 'FeatureCollection': + return (geojson as FeatureCollection).features; + case 'Feature': + return [geojson as Feature]; + default: + return [ + { type: 'Feature', geometry: geojson as Geometry, properties: null }, + ]; + } +} + +interface StyleProps { + strokeColor?: ColorValue; + strokeWidth?: number; + fillColor?: ColorValue; +} + +function resolveStyle( + props: GeoJsonProps, + properties: Record | null +): StyleProps { + return { + strokeColor: properties?.stroke ?? props.strokeColor, + strokeWidth: properties?.['stroke-width'] ?? props.strokeWidth, + fillColor: properties?.fill ?? props.fillColor, + }; +} + +function renderGeometry( + geometry: Geometry, + feature: Feature, + style: StyleProps, + props: GeoJsonProps, + keyPrefix: string +): ReactElement[] { + const elements: ReactElement[] = []; + + switch (geometry.type) { + case 'Point': { + const markerProps: MarkerProps = { + coordinate: toCoordinate(geometry.coordinates), + title: feature.properties?.title, + description: feature.properties?.description, + zIndex: props.zIndex, + }; + elements.push( + props.renderMarker ? ( + props.renderMarker(markerProps, feature) ?? ( + + ) + ) : ( + + ) + ); + break; + } + case 'MultiPoint': { + for (let i = 0; i < geometry.coordinates.length; i++) { + const markerProps: MarkerProps = { + coordinate: toCoordinate(geometry.coordinates[i]!), + title: feature.properties?.title, + description: feature.properties?.description, + zIndex: props.zIndex, + }; + const key = `${keyPrefix}-${i}`; + elements.push( + props.renderMarker ? ( + props.renderMarker(markerProps, feature) ?? ( + + ) + ) : ( + + ) + ); + } + break; + } + case 'LineString': { + const polylineProps: PolylineProps = { + coordinates: toCoordinates(geometry.coordinates), + strokeColors: style.strokeColor ? [style.strokeColor] : undefined, + strokeWidth: style.strokeWidth, + zIndex: props.zIndex, + }; + elements.push( + props.renderPolyline ? ( + props.renderPolyline(polylineProps, feature) ?? ( + + ) + ) : ( + + ) + ); + break; + } + case 'MultiLineString': { + for (let i = 0; i < geometry.coordinates.length; i++) { + const polylineProps: PolylineProps = { + coordinates: toCoordinates(geometry.coordinates[i]!), + strokeColors: style.strokeColor ? [style.strokeColor] : undefined, + strokeWidth: style.strokeWidth, + zIndex: props.zIndex, + }; + const key = `${keyPrefix}-${i}`; + elements.push( + props.renderPolyline ? ( + props.renderPolyline(polylineProps, feature) ?? ( + + ) + ) : ( + + ) + ); + } + break; + } + case 'Polygon': { + const outer = toCoordinates(geometry.coordinates[0]!); + const holes = + geometry.coordinates.length > 1 + ? geometry.coordinates.slice(1).map(toCoordinates) + : undefined; + const polygonProps: PolygonProps = { + coordinates: outer, + holes, + strokeColor: style.strokeColor, + strokeWidth: style.strokeWidth, + fillColor: style.fillColor, + zIndex: props.zIndex, + }; + elements.push( + props.renderPolygon ? ( + props.renderPolygon(polygonProps, feature) ?? ( + + ) + ) : ( + + ) + ); + break; + } + case 'MultiPolygon': { + for (let i = 0; i < geometry.coordinates.length; i++) { + const rings = geometry.coordinates[i]!; + const outer = toCoordinates(rings[0]!); + const holes = + rings.length > 1 ? rings.slice(1).map(toCoordinates) : undefined; + const polygonProps: PolygonProps = { + coordinates: outer, + holes, + strokeColor: style.strokeColor, + strokeWidth: style.strokeWidth, + fillColor: style.fillColor, + zIndex: props.zIndex, + }; + const key = `${keyPrefix}-${i}`; + elements.push( + props.renderPolygon ? ( + props.renderPolygon(polygonProps, feature) ?? ( + + ) + ) : ( + + ) + ); + } + break; + } + case 'GeometryCollection': { + for (let i = 0; i < geometry.geometries.length; i++) { + elements.push( + ...renderGeometry( + geometry.geometries[i]!, + feature, + style, + props, + `${keyPrefix}-${i}` + ) + ); + } + break; + } + } + + return elements; +} + +export function GeoJson(props: GeoJsonProps) { + const { geojson } = props; + + const elements = useMemo(() => { + const features = normalizeFeatures(geojson); + const result: ReactElement[] = []; + + for (let i = 0; i < features.length; i++) { + const feature = features[i]!; + if (!feature.geometry) continue; + + const key = feature.id != null ? String(feature.id) : String(i); + const style = resolveStyle(props, feature.properties); + result.push( + ...renderGeometry(feature.geometry, feature, style, props, key) + ); + } + + return result; + }, [geojson, props]); + + return <>{elements}; +} diff --git a/src/components/index.ts b/src/components/index.ts index 276c24b..59f0ec4 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,3 +8,5 @@ export type { PolylineEasing, PolylineAnimatedOptions, } from './Polyline'; +export { GeoJson } from './GeoJson'; +export type { GeoJsonProps } from './GeoJson'; diff --git a/src/components/index.web.ts b/src/components/index.web.ts index 8583c34..909560e 100644 --- a/src/components/index.web.ts +++ b/src/components/index.web.ts @@ -4,3 +4,5 @@ export { Polyline } from './Polyline.web'; export type { MarkerProps } from './Marker'; export type { PolygonProps } from './Polygon'; export type { PolylineProps } from './Polyline'; +export { GeoJson } from './GeoJson'; +export type { GeoJsonProps } from './GeoJson'; diff --git a/src/geojson.types.ts b/src/geojson.types.ts new file mode 100644 index 0000000..1a8caad --- /dev/null +++ b/src/geojson.types.ts @@ -0,0 +1,66 @@ +/** + * GeoJSON types per RFC 7946 + * Note: GeoJSON positions use [longitude, latitude] order + */ + +export type Position = + | [longitude: number, latitude: number] + | [longitude: number, latitude: number, altitude: number]; + +export interface Point { + type: 'Point'; + coordinates: Position; +} + +export interface MultiPoint { + type: 'MultiPoint'; + coordinates: Position[]; +} + +export interface LineString { + type: 'LineString'; + coordinates: Position[]; +} + +export interface MultiLineString { + type: 'MultiLineString'; + coordinates: Position[][]; +} + +export interface Polygon { + type: 'Polygon'; + coordinates: Position[][]; +} + +export interface MultiPolygon { + type: 'MultiPolygon'; + coordinates: Position[][][]; +} + +export interface GeometryCollection { + type: 'GeometryCollection'; + geometries: Geometry[]; +} + +export type Geometry = + | Point + | MultiPoint + | LineString + | MultiLineString + | Polygon + | MultiPolygon + | GeometryCollection; + +export interface Feature { + type: 'Feature'; + id?: string | number; + geometry: G; + properties: Record | null; +} + +export interface FeatureCollection { + type: 'FeatureCollection'; + features: Feature[]; +} + +export type GeoJSON = Geometry | Feature | FeatureCollection; diff --git a/src/index.ts b/src/index.ts index 8781e5c..e7b8ffc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,8 @@ export type { PolylineEasing, PolylineAnimatedOptions, } from './components'; +export { GeoJson } from './components'; +export type { GeoJsonProps } from './components'; export type { MapViewProps, MapViewRef, @@ -33,3 +35,10 @@ export type { EdgeInsets, PressEventPayload, } from './types'; +export type { + GeoJSON, + Feature, + FeatureCollection, + Geometry, + Position, +} from './geojson.types'; diff --git a/src/index.web.ts b/src/index.web.ts index d6e87bb..4d9fb89 100644 --- a/src/index.web.ts +++ b/src/index.web.ts @@ -7,6 +7,8 @@ export { Polygon } from './components/index.web'; export type { PolygonProps } from './components/index.web'; export { Polyline } from './components/index.web'; export type { PolylineProps } from './components/index.web'; +export { GeoJson } from './components/index.web'; +export type { GeoJsonProps } from './components/index.web'; export type { MapViewProps, MapViewRef, @@ -20,3 +22,10 @@ export type { Point, EdgeInsets, } from './types'; +export type { + GeoJSON, + Feature, + FeatureCollection, + Geometry, + Position, +} from './geojson.types'; From 15989a2d4c97b23ebb2f9d81286d99b5c492f0b4 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 03:06:41 +0800 Subject: [PATCH 2/8] refactor: extract component types into Component.types.ts files --- src/components/GeoJson.tsx | 27 ++++------- src/components/GeoJson.types.ts | 23 +++++++++ src/components/Marker.tsx | 81 +++----------------------------- src/components/Marker.types.ts | 75 +++++++++++++++++++++++++++++ src/components/Marker.web.tsx | 2 +- src/components/Polygon.tsx | 34 +------------- src/components/Polygon.types.ts | 33 +++++++++++++ src/components/Polygon.web.tsx | 2 +- src/components/Polyline.tsx | 60 +++-------------------- src/components/Polyline.types.ts | 55 ++++++++++++++++++++++ src/components/Polyline.web.tsx | 2 +- src/components/index.ts | 12 +++-- src/components/index.web.ts | 8 ++-- 13 files changed, 224 insertions(+), 190 deletions(-) create mode 100644 src/components/GeoJson.types.ts create mode 100644 src/components/Marker.types.ts create mode 100644 src/components/Polygon.types.ts create mode 100644 src/components/Polyline.types.ts diff --git a/src/components/GeoJson.tsx b/src/components/GeoJson.tsx index 9e1c7b8..ebaeab6 100644 --- a/src/components/GeoJson.tsx +++ b/src/components/GeoJson.tsx @@ -8,26 +8,15 @@ import type { Position, } from '../geojson.types'; import type { Coordinate } from '../types'; -import { Marker, type MarkerProps } from './Marker'; -import { Polygon, type PolygonProps } from './Polygon'; -import { Polyline, type PolylineProps } from './Polyline'; +import type { GeoJsonProps } from './GeoJson.types'; +import { Marker } from './Marker'; +import type { MarkerProps } from './Marker.types'; +import { Polygon } from './Polygon'; +import type { PolygonProps } from './Polygon.types'; +import { Polyline } from './Polyline'; +import type { PolylineProps } from './Polyline.types'; -export interface GeoJsonProps { - geojson: GeoJSON; - strokeColor?: ColorValue; - strokeWidth?: number; - fillColor?: ColorValue; - zIndex?: number; - renderMarker?: (props: MarkerProps, feature: Feature) => ReactElement | null; - renderPolyline?: ( - props: PolylineProps, - feature: Feature - ) => ReactElement | null; - renderPolygon?: ( - props: PolygonProps, - feature: Feature - ) => ReactElement | null; -} +export type { GeoJsonProps } from './GeoJson.types'; function toCoordinate(position: Position): Coordinate { return { latitude: position[1], longitude: position[0] }; diff --git a/src/components/GeoJson.types.ts b/src/components/GeoJson.types.ts new file mode 100644 index 0000000..34dfa62 --- /dev/null +++ b/src/components/GeoJson.types.ts @@ -0,0 +1,23 @@ +import type { ReactElement } from 'react'; +import type { ColorValue } from 'react-native'; +import type { Feature, GeoJSON } from '../geojson.types'; +import type { MarkerProps } from './Marker.types'; +import type { PolygonProps } from './Polygon.types'; +import type { PolylineProps } from './Polyline.types'; + +export interface GeoJsonProps { + geojson: GeoJSON; + strokeColor?: ColorValue; + strokeWidth?: number; + fillColor?: ColorValue; + zIndex?: number; + renderMarker?: (props: MarkerProps, feature: Feature) => ReactElement | null; + renderPolyline?: ( + props: PolylineProps, + feature: Feature + ) => ReactElement | null; + renderPolygon?: ( + props: PolygonProps, + feature: Feature + ) => ReactElement | null; +} diff --git a/src/components/Marker.tsx b/src/components/Marker.tsx index 558cb19..0c11abd 100644 --- a/src/components/Marker.tsx +++ b/src/components/Marker.tsx @@ -1,80 +1,13 @@ import React from 'react'; -import type { ReactNode } from 'react'; -import { StyleSheet, type NativeSyntheticEvent } from 'react-native'; +import { StyleSheet } from 'react-native'; import LuggMarkerViewNativeComponent from '../fabric/LuggMarkerViewNativeComponent'; -import type { Coordinate, Point, PressEventPayload } from '../types'; +import type { MarkerProps } from './Marker.types'; -export type MarkerPressEvent = NativeSyntheticEvent; -export type MarkerDragEvent = NativeSyntheticEvent; - -export interface MarkerProps { - /** - * Name used for debugging purposes - */ - name?: string; - /** - * Marker position - */ - coordinate: Coordinate; - /** - * Callout title - */ - title?: string; - /** - * Callout description - */ - description?: string; - /** - * Anchor point for custom marker views - */ - anchor?: Point; - /** - * Z-index for marker ordering. Higher values render on top. - */ - zIndex?: number; - /** - * Rotation angle in degrees clockwise from north. - * @default 0 - */ - rotate?: number; - /** - * Scale factor for the marker. - * @default 1 - */ - scale?: number; - /** - * Rasterize custom marker view to bitmap for better performance. - * Set to false if you need live view updates (e.g., animations). - * @platform ios, android - * @default true - */ - rasterize?: boolean; - /** - * Whether the marker can be dragged by the user. - * @default false - */ - draggable?: boolean; - /** - * Called when the marker is pressed - */ - onPress?: (event: MarkerPressEvent) => void; - /** - * Called when marker drag starts - */ - onDragStart?: (event: MarkerDragEvent) => void; - /** - * Called continuously as the marker is dragged - */ - onDragChange?: (event: MarkerDragEvent) => void; - /** - * Called when marker drag ends - */ - onDragEnd?: (event: MarkerDragEvent) => void; - /** - * Custom marker view - */ - children?: ReactNode; -} +export type { + MarkerProps, + MarkerPressEvent, + MarkerDragEvent, +} from './Marker.types'; export class Marker extends React.PureComponent { private getStyle(zIndex: number | undefined) { diff --git a/src/components/Marker.types.ts b/src/components/Marker.types.ts new file mode 100644 index 0000000..ff40ee5 --- /dev/null +++ b/src/components/Marker.types.ts @@ -0,0 +1,75 @@ +import type { ReactNode } from 'react'; +import type { NativeSyntheticEvent } from 'react-native'; +import type { Coordinate, Point, PressEventPayload } from '../types'; + +export type MarkerPressEvent = NativeSyntheticEvent; +export type MarkerDragEvent = NativeSyntheticEvent; + +export interface MarkerProps { + /** + * Name used for debugging purposes + */ + name?: string; + /** + * Marker position + */ + coordinate: Coordinate; + /** + * Callout title + */ + title?: string; + /** + * Callout description + */ + description?: string; + /** + * Anchor point for custom marker views + */ + anchor?: Point; + /** + * Z-index for marker ordering. Higher values render on top. + */ + zIndex?: number; + /** + * Rotation angle in degrees clockwise from north. + * @default 0 + */ + rotate?: number; + /** + * Scale factor for the marker. + * @default 1 + */ + scale?: number; + /** + * Rasterize custom marker view to bitmap for better performance. + * Set to false if you need live view updates (e.g., animations). + * @platform ios, android + * @default true + */ + rasterize?: boolean; + /** + * Whether the marker can be dragged by the user. + * @default false + */ + draggable?: boolean; + /** + * Called when the marker is pressed + */ + onPress?: (event: MarkerPressEvent) => void; + /** + * Called when marker drag starts + */ + onDragStart?: (event: MarkerDragEvent) => void; + /** + * Called continuously as the marker is dragged + */ + onDragChange?: (event: MarkerDragEvent) => void; + /** + * Called when marker drag ends + */ + onDragEnd?: (event: MarkerDragEvent) => void; + /** + * Custom marker view + */ + children?: ReactNode; +} diff --git a/src/components/Marker.web.tsx b/src/components/Marker.web.tsx index a4ab1a4..eca0c67 100644 --- a/src/components/Marker.web.tsx +++ b/src/components/Marker.web.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { AdvancedMarker } from '@vis.gl/react-google-maps'; import { useMapContext } from '../MapProvider.web'; -import type { MarkerProps } from './Marker'; +import type { MarkerProps } from './Marker.types'; const toWebAnchor = (value: number) => `-${value * 100}%`; diff --git a/src/components/Polygon.tsx b/src/components/Polygon.tsx index 30ff29c..d934082 100644 --- a/src/components/Polygon.tsx +++ b/src/components/Polygon.tsx @@ -1,39 +1,9 @@ import React from 'react'; -import type { ColorValue } from 'react-native'; import { StyleSheet } from 'react-native'; import LuggPolygonViewNativeComponent from '../fabric/LuggPolygonViewNativeComponent'; -import type { Coordinate } from '../types'; +import type { PolygonProps } from './Polygon.types'; -export interface PolygonProps { - /** - * Array of coordinates forming the polygon boundary - */ - coordinates: Coordinate[]; - /** - * Array of coordinate arrays representing interior holes - */ - holes?: Coordinate[][]; - /** - * Stroke (outline) color - */ - strokeColor?: ColorValue; - /** - * Stroke width in points - */ - strokeWidth?: number; - /** - * Fill color of the polygon - */ - fillColor?: ColorValue; - /** - * Z-index for layering - */ - zIndex?: number; - /** - * Called when the polygon is tapped - */ - onPress?: () => void; -} +export type { PolygonProps } from './Polygon.types'; export class Polygon extends React.PureComponent { private _cachedZIndex: number | undefined; diff --git a/src/components/Polygon.types.ts b/src/components/Polygon.types.ts new file mode 100644 index 0000000..0fd4cab --- /dev/null +++ b/src/components/Polygon.types.ts @@ -0,0 +1,33 @@ +import type { ColorValue } from 'react-native'; +import type { Coordinate } from '../types'; + +export interface PolygonProps { + /** + * Array of coordinates forming the polygon boundary + */ + coordinates: Coordinate[]; + /** + * Array of coordinate arrays representing interior holes + */ + holes?: Coordinate[][]; + /** + * Stroke (outline) color + */ + strokeColor?: ColorValue; + /** + * Stroke width in points + */ + strokeWidth?: number; + /** + * Fill color of the polygon + */ + fillColor?: ColorValue; + /** + * Z-index for layering + */ + zIndex?: number; + /** + * Called when the polygon is tapped + */ + onPress?: () => void; +} diff --git a/src/components/Polygon.web.tsx b/src/components/Polygon.web.tsx index bf596fc..b952a37 100644 --- a/src/components/Polygon.web.tsx +++ b/src/components/Polygon.web.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { useMapContext } from '../MapProvider.web'; -import type { PolygonProps } from './Polygon'; +import type { PolygonProps } from './Polygon.types'; export function Polygon({ coordinates, diff --git a/src/components/Polyline.tsx b/src/components/Polyline.tsx index e5b2680..94533f6 100644 --- a/src/components/Polyline.tsx +++ b/src/components/Polyline.tsx @@ -1,61 +1,13 @@ import React from 'react'; -import type { ColorValue } from 'react-native'; import { StyleSheet } from 'react-native'; import LuggPolylineViewNativeComponent from '../fabric/LuggPolylineViewNativeComponent'; -import type { Coordinate } from '../types'; +import type { PolylineProps } from './Polyline.types'; -export type PolylineEasing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'; - -export interface PolylineAnimatedOptions { - /** - * Animation duration in milliseconds - * @default 2150 - */ - duration?: number; - /** - * Easing function for the animation - * @default 'linear' - */ - easing?: PolylineEasing; - /** - * Portion of the line visible as trail (0-1) - * 1.0 = full snake effect, 0.2 = short worm - * @default 1.0 - */ - trailLength?: number; - /** - * Delay before animation starts in milliseconds - * @default 0 - */ - delay?: number; -} - -export interface PolylineProps { - /** - * Array of coordinates forming the polyline - */ - coordinates: Coordinate[]; - /** - * Gradient colors along the polyline - */ - strokeColors?: ColorValue[]; - /** - * Line width in points - */ - strokeWidth?: number; - /** - * Animate the polyline with a snake effect - */ - animated?: boolean; - /** - * Animation configuration options - */ - animatedOptions?: PolylineAnimatedOptions; - /** - * Z-index for layering polylines - */ - zIndex?: number; -} +export type { + PolylineProps, + PolylineEasing, + PolylineAnimatedOptions, +} from './Polyline.types'; export class Polyline extends React.PureComponent { private getStyle(zIndex: number | undefined) { diff --git a/src/components/Polyline.types.ts b/src/components/Polyline.types.ts new file mode 100644 index 0000000..0dbff8a --- /dev/null +++ b/src/components/Polyline.types.ts @@ -0,0 +1,55 @@ +import type { ColorValue } from 'react-native'; +import type { Coordinate } from '../types'; + +export type PolylineEasing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'; + +export interface PolylineAnimatedOptions { + /** + * Animation duration in milliseconds + * @default 2150 + */ + duration?: number; + /** + * Easing function for the animation + * @default 'linear' + */ + easing?: PolylineEasing; + /** + * Portion of the line visible as trail (0-1) + * 1.0 = full snake effect, 0.2 = short worm + * @default 1.0 + */ + trailLength?: number; + /** + * Delay before animation starts in milliseconds + * @default 0 + */ + delay?: number; +} + +export interface PolylineProps { + /** + * Array of coordinates forming the polyline + */ + coordinates: Coordinate[]; + /** + * Gradient colors along the polyline + */ + strokeColors?: ColorValue[]; + /** + * Line width in points + */ + strokeWidth?: number; + /** + * Animate the polyline with a snake effect + */ + animated?: boolean; + /** + * Animation configuration options + */ + animatedOptions?: PolylineAnimatedOptions; + /** + * Z-index for layering polylines + */ + zIndex?: number; +} diff --git a/src/components/Polyline.web.tsx b/src/components/Polyline.web.tsx index cb6772f..ae86408 100644 --- a/src/components/Polyline.web.tsx +++ b/src/components/Polyline.web.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useMapContext } from '../MapProvider.web'; -import type { PolylineProps, PolylineEasing } from './Polyline'; +import type { PolylineProps, PolylineEasing } from './Polyline.types'; const DEFAULT_DURATION = 2150; diff --git a/src/components/index.ts b/src/components/index.ts index 59f0ec4..64e062e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,12 +1,16 @@ export { Marker } from './Marker'; -export type { MarkerProps, MarkerPressEvent, MarkerDragEvent } from './Marker'; +export type { + MarkerProps, + MarkerPressEvent, + MarkerDragEvent, +} from './Marker.types'; export { Polygon } from './Polygon'; -export type { PolygonProps } from './Polygon'; +export type { PolygonProps } from './Polygon.types'; export { Polyline } from './Polyline'; export type { PolylineProps, PolylineEasing, PolylineAnimatedOptions, -} from './Polyline'; +} from './Polyline.types'; export { GeoJson } from './GeoJson'; -export type { GeoJsonProps } from './GeoJson'; +export type { GeoJsonProps } from './GeoJson.types'; diff --git a/src/components/index.web.ts b/src/components/index.web.ts index 909560e..b680ca9 100644 --- a/src/components/index.web.ts +++ b/src/components/index.web.ts @@ -1,8 +1,8 @@ export { Marker } from './Marker.web'; export { Polygon } from './Polygon.web'; export { Polyline } from './Polyline.web'; -export type { MarkerProps } from './Marker'; -export type { PolygonProps } from './Polygon'; -export type { PolylineProps } from './Polyline'; +export type { MarkerProps } from './Marker.types'; +export type { PolygonProps } from './Polygon.types'; +export type { PolylineProps } from './Polyline.types'; export { GeoJson } from './GeoJson'; -export type { GeoJsonProps } from './GeoJson'; +export type { GeoJsonProps } from './GeoJson.types'; From 7676719d2bdd529517b47adb28570b68159ee009 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 03:31:40 +0800 Subject: [PATCH 3/8] feat: update example app with GeoJSON loading UI and styling improvements --- example/shared/src/Home.tsx | 155 +++++++++++++++++-- example/shared/src/components/Button.tsx | 27 ++-- example/shared/src/components/Map.tsx | 12 ++ example/shared/src/components/ThemedText.tsx | 35 +++++ example/shared/src/components/index.ts | 1 + 5 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 example/shared/src/components/ThemedText.tsx diff --git a/example/shared/src/Home.tsx b/example/shared/src/Home.tsx index 67ba56d..8a07d8b 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,21 @@ import { } from './markers'; import { useLocationPermission } from './useLocationPermission'; +const GEOJSON_PRESETS = [ + { + name: 'Sample (Mixed)', + url: 'https://gist.githubusercontent.com/wavded/1200773/raw/e122cf709898c09758aecfef349964a8d73a83f3/sample.json', + }, + { + 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, @@ -58,13 +75,18 @@ export function Home() { function 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 +133,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 +154,7 @@ function HomeContent() { : gesture ? ' (gesture)' : ''; - setStatusText(pos + suffix); + setStatus({ text: pos + suffix, error: false }); }, [] ); @@ -178,6 +203,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 +229,7 @@ function HomeContent() { ref={mapRef} provider={provider} markers={markers} + geojson={geojson} animatedPosition={animatedPosition} userLocationEnabled={locationPermission} onReady={handleMapReady} @@ -206,7 +250,7 @@ function HomeContent() { onMarkerDragEnd={(e, m) => formatPressEvent(e, `Drag end(${m.name})`)} onPolygonPress={() => { lockStatus(); - setStatusText('Polygon pressed'); + setStatus({ text: 'Polygon pressed', error: false }); }} /> )} @@ -223,38 +267,105 @@ function HomeContent() { onDidPresent={handleSheetPresent} onDetentChange={handleDetentChange} > - {statusText} + + {status.text} + -