@molecule/app-map-drawing-react
v1.0.2
Published
Map polygon / circle / pin / line drawing primitives for geofence editing — composes with @molecule/app-maps for the underlying map backend
Downloads
259
Maintainers
Readme
@molecule/app-map-drawing-react
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Map drawing — toolbar + interaction surface for authoring geofences
(polygons, circles, pins, lines) on top of any @molecule/app-maps backend.
Exports <MapDrawing>, <MapDrawingToolbar>, geometry helpers
(haversineDistanceMeters, closeRing, identityBackend, pointInRect),
and the MapShape / MapDrawingProps / MapDrawingBackend types.
Composes with @molecule/app-maps via two props: mapSlot (render the map
element underneath the drawing overlay) and mapBackend (a 3-function
projection adapter: project, unproject, distanceMeters). There is no
prebuilt adapter — build one from your map instance as in the example.
Used by fleet-management (delivery zones), property-management (parcel boundaries), and venue-booking (event footprints).
Quick Start
import { MapDrawing, haversineDistanceMeters } from '@molecule/app-map-drawing-react'
import type { MapDrawingBackend, MapShape } from '@molecule/app-map-drawing-react'
import type { MapInstance } from '@molecule/app-maps'
// Adapt an app-maps MapInstance into a drawing backend:
const backendFor = (map: MapInstance): MapDrawingBackend => ({
project: ([lng, lat]) => map.project({ lat, lng }),
unproject: (p) => { const c = map.unproject(p); return [c.lng, c.lat] },
distanceMeters: haversineDistanceMeters,
})
<MapDrawing
tools={['polygon', 'circle', 'pin']}
height={500}
mapBackend={backendFor(mapInstance)}
onChange={(shapes: MapShape[]) => saveZones(shapes)}
/>Type
feature
Installation
npm install @molecule/app-map-drawing-react @molecule/app-react @molecule/app-ui react
npm install -D @types/reactAPI
Interfaces
GeoJsonLineString
GeoJSON LineString — sequence of two-or-more positions.
interface GeoJsonLineString {
/** Geometry type discriminator. */
type: 'LineString'
/** Ordered list of positions — `[lng, lat]` pairs. */
coordinates: [number, number][]
}GeoJsonPoint
GeoJSON Point — [lng, lat] (longitude first, per the spec).
interface GeoJsonPoint {
/** Geometry type discriminator. */
type: 'Point'
/** Single position — `[lng, lat]`. */
coordinates: [number, number]
}GeoJsonPolygon
GeoJSON Polygon — outer ring + optional holes. The drawing surface
only authors the outer ring; holes are preserved verbatim if present
on initialShapes.
interface GeoJsonPolygon {
/** Geometry type discriminator. */
type: 'Polygon'
/**
* Array of linear rings. The first ring is the outer boundary; any
* subsequent rings are holes. Each ring is a closed sequence of
* `[lng, lat]` positions where the first and last positions are
* identical.
*/
coordinates: [number, number][][]
}MapDrawingBackend
Backend the drawing surface delegates projection + great-circle
distance to. Production callers pass an adapter wired to
@molecule/app-maps's MapInstance (project / unproject); test
callers can pass a stub backend that uses identity projection.
The backend abstraction means the component never depends on a specific map provider — swap Mapbox for Google Maps for Leaflet by swapping the backend, not the drawing surface.
interface MapDrawingBackend {
/**
* Project a `[lng, lat]` position to a screen-space pixel coordinate
* inside the drawing surface's bounding box.
*
* @param coordinates - `[lng, lat]` position.
* @returns Pixel offset relative to the drawing surface.
*/
project(coordinates: [number, number]): ScreenPoint
/**
* Unproject a screen-space pixel coordinate back to `[lng, lat]`.
*
* @param point - Pixel offset relative to the drawing surface.
* @returns Geographic position in `[lng, lat]` order.
*/
unproject(point: ScreenPoint): [number, number]
/**
* Distance in meters between two `[lng, lat]` positions. Used to
* compute circle radii from drag distance. Production backends
* should use the Haversine great-circle formula; the default
* fallback uses an equirectangular approximation that is fine for
* small drag deltas at non-polar latitudes.
*
* @param a - First position.
* @param b - Second position.
* @returns Distance in meters.
*/
distanceMeters(a: [number, number], b: [number, number]): number
}MapDrawingProps
MapDrawing component props.
interface MapDrawingProps {
/** Initial shapes to seed the surface with. */
initialShapes?: MapShape[]
/** Called whenever the shape list changes (add / edit / delete). */
onChange: (shapes: MapShape[]) => void
/** Drawing tools to expose. Defaults to the four built-in tools. */
tools?: DrawingTool[]
/** Externally controlled active tool. When omitted, the component manages tool state internally. */
activeTool?: ActiveTool
/** Called when the active tool changes (drives the toolbar regardless of internal/external state). */
onActiveToolChange?: (tool: ActiveTool) => void
/**
* Map backend used to project between geographic and screen coordinates.
* Defaults to an identity backend that treats `lng → x`, `lat → y` so
* the component can be exercised in tests without a real map provider.
* In production, callers wire this up against a `MapInstance` from
* `@molecule/app-maps`.
*/
mapBackend?: MapDrawingBackend
/** Optional render slot for the map background (e.g. `<MapView />`). */
mapSlot?: ReactNode
/** Width of the surface (CSS) — defaults to `100%`. */
width?: number | string
/** Height of the surface (CSS) — defaults to `400`. */
height?: number | string
/** Extra classes merged onto the root element. */
className?: string
}MapDrawingToolbarProps
MapDrawingToolbar component props.
interface MapDrawingToolbarProps {
/** Drawing tools to expose (in declaration order). */
tools: DrawingTool[]
/** Currently active tool. */
activeTool: ActiveTool
/** Called when the user clicks a tool button. */
onActiveToolChange: (tool: ActiveTool) => void
/** Called when the user clicks the delete-selected button. */
onDeleteSelected: () => void
/** Whether anything is currently selected (drives delete-button enabled state). */
hasSelection: boolean
}MapShape
Drawn shape carried by the component. The kind discriminator is
kept independent of the GeoJSON type because circles share the
Point geometry (a radius lives in properties).
interface MapShape {
/** Stable identifier for the shape (used as a React key). */
id: string
/** Logical shape kind. Drives toolbar selection + rendering style. */
kind: 'polygon' | 'circle' | 'pin' | 'line'
/** GeoJSON geometry. */
geometry: GeoJsonGeometry
/**
* Free-form caller properties carried alongside the geometry. The
* `radiusMeters` key is reserved for circle shapes and is required
* when `kind === 'circle'`.
*/
properties?: Record<string, unknown> & { radiusMeters?: number }
}ScreenPoint
Screen-space point in CSS pixels relative to the drawing surface's
bounding box. (0, 0) is the top-left corner.
interface ScreenPoint {
/** Horizontal offset in CSS pixels. */
x: number
/** Vertical offset in CSS pixels. */
y: number
}Types
ActiveTool
Active tool — either one of the drawing tools or one of the action
modes (select).
type ActiveTool = DrawingTool | 'select'DrawingTool
Tools the toolbar may show. The fixed set is the four drawing tools
plus two action tools (select, delete). The component renders
the four drawing tools by default (tools prop) and always shows
select + delete because they have no opt-out semantic.
type DrawingTool = 'polygon' | 'circle' | 'pin' | 'line'GeoJsonGeometry
Union of GeoJSON geometry kinds the drawing surface understands.
type GeoJsonGeometry = GeoJsonPoint | GeoJsonLineString | GeoJsonPolygonShapeSelection
Selection set — keyed by shape id. Stored as a Set<string> so
delete operations stay O(1) regardless of how many shapes are drawn.
type ShapeSelection = ReadonlySet<string>Functions
closeRing(vertices)
Snap an open ring (the in-progress polygon vertex list) into a
GeoJSON-compliant closed ring by repeating the first point at the
end. Returns null when the ring has fewer than three vertices,
since polygons require a minimum of three distinct points plus the
closing repeat.
function closeRing(vertices: [number, number][]): [number, number][] | nullvertices— Open ring of[lng, lat]pairs.
Returns: Closed ring, or null when there are not enough points.
haversineDistanceMeters(a, b)
Great-circle distance between two [lng, lat] positions in meters
using the Haversine formula (uses the WGS84 equatorial radius,
6378137 m), so the
radius computed during drag agrees with what a geographic backend
will project back to the user.
function haversineDistanceMeters(a: [number, number], b: [number, number]): numbera— First position.b— Second position.
Returns: Distance in meters.
MapDrawing(props)
Map-drawing surface for geofence editing. Renders a toolbar plus an interaction layer overlaid on top of the (optional) map slot. The component handles four drawing tools — polygon, circle, pin, line — plus a select / delete action group.
Drawing semantics:
- polygon / line: click adds vertices; double-click finalises.
- circle: pointer-down sets the center; pointer-up commits with radius equal to the great-circle distance from center to release.
- pin: each pointer-down adds one new pin shape.
- select: pointer-drag draws a marquee; any shape whose anchor point lies inside the marquee is added to the selection.
Pressing Backspace or Delete while focused removes the current
selection. All UI text routes through t() so the component
translates via the companion locale bond.
function MapDrawing(
props: MapDrawingProps,
): ReactElement<unknown, string | JSXElementConstructor<any>>props— Component props.
Returns: Rendered map-drawing surface.
MapDrawingToolbar(props)
Toolbar with one button per drawing tool plus select and delete.
All button labels route through t() so the toolbar translates via
the companion @molecule/app-locales-feature-map-drawing bond.
function MapDrawingToolbar(
props: MapDrawingToolbarProps,
): ReactElement<unknown, string | JSXElementConstructor<any>>props— Toolbar props.
Returns: Toolbar element.
pointInRect(point, a, b)
Check whether a screen-space point lies inside the given selection rectangle (axis-aligned, inclusive bounds).
function pointInRect(point: ScreenPoint, a: ScreenPoint, b: ScreenPoint): booleanpoint— Point under test.a— One corner of the selection rectangle.b— Opposite corner of the selection rectangle.
Returns: true when the point lies inside the rectangle.
toRadians(degrees)
Convert degrees to radians.
function toRadians(degrees: number): numberdegrees— Angle in degrees.
Returns: Angle in radians.
Constants
identityBackend
Identity drawing backend — used when no real map is mounted (tests,
SSR, demo storyboards). Projection is lng → x, lat → y so
fixtures can be authored in pixel space and round-trip cleanly. The
distance metric is Euclidean pixels rather than meters; callers that
care about a real radius should always pass a real backend.
const identityBackend: MapDrawingBackendInjection Notes
Requirements
Peer dependencies:
@molecule/app-react^1.0.1@molecule/app-ui^1.0.1react^18.0.0 || ^19.0.0
Runtime Dependencies
@molecule/app-react@molecule/app-uireactWhen
mapBackendis omitted, the IDENTITY backend is used: lng maps to x pixels and lat to y pixels, and "radiusMeters" is Euclidean pixels. That is intended for tests/storyboards only — always pass a real backend when shapes must be geographic.polygon/line: click adds vertices, double-click finalises (polygons need 3+ vertices); circle: drag from center, release commits; pin: one per click; select: marquee-drag, then Backspace/Delete removes the selection (the surface must have keyboard focus); Escape cancels an in-progress draft.
Toolbar and surface labels route through
t()undermapDrawing.— the registered companion bond is@molecule/app-locales-feature-map-drawing.Note
@molecule/app-mapscurrently ships only a placeholder provider (no real tiles); the drawing overlay works regardless, but themapSlotbackground will be a placeholder until a real map provider is wired.
