← All @molecule/* packages · App templates
@molecule/app-map-drawing-reactFeature · map-drawing · App (browser) · v1.0.1 · Apache-2.0
Map polygon / circle / pin / line drawing primitives for geofence editing — composes with @molecule/app-maps for the underlying map backend
npm install @molecule/app-map-drawing-react@molecule/app-map-drawing-react is a ready-made map-drawing feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
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)}
/>Works with: @molecule/app-react, @molecule/app-ui
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).
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)}
/>
feature
npm install @molecule/app-map-drawing-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react
GeoJsonLineStringGeoJSON LineString — sequence of two-or-more positions.
interface GeoJsonLineString {
/** Geometry type discriminator. */
type: 'LineString'
/** Ordered list of positions — `[lng, lat]` pairs. */
coordinates: [number, number][]
}
GeoJsonPointGeoJSON Point — [lng, lat] (longitude first, per the spec).
interface GeoJsonPoint {
/** Geometry type discriminator. */
type: 'Point'
/** Single position — `[lng, lat]`. */
coordinates: [number, number]
}
GeoJsonPolygonGeoJSON 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][][]
}
MapDrawingBackendBackend 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
}
MapDrawingPropsMapDrawing 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
}
MapDrawingToolbarPropsMapDrawingToolbar 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
}
MapShapeDrawn 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 }
}
ScreenPointScreen-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
}
ActiveToolActive tool — either one of the drawing tools or one of the action
modes (select).
type ActiveTool = DrawingTool | 'select'
DrawingToolTools 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'
GeoJsonGeometryUnion of GeoJSON geometry kinds the drawing surface understands.
type GeoJsonGeometry = GeoJsonPoint | GeoJsonLineString | GeoJsonPolygon
ShapeSelectionSelection 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>
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][] | null
vertices — 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]): number
a — 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:
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): boolean
point — 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): number
degrees — Angle in degrees.Returns: Angle in radians.
identityBackendIdentity 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: MapDrawingBackend
Peer dependencies:
@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-react
@molecule/app-ui
react
When mapBackend is 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() under mapDrawing. — the
registered companion bond is @molecule/app-locales-feature-map-drawing.
Note @molecule/app-maps currently ships only a placeholder provider (no
real tiles); the drawing overlay works regardless, but the mapSlot
background will be a placeholder until a real map provider is wired.