google_map_utils

A drawing, editing & measurement toolkit for google_maps_flutter. Built on map_utils_core algorithms.

pub package likes pub points


What is this?

google_map_utils is an addon for google_maps_flutter — it does not replace it. Drop it into any existing Google Maps project and instantly unlock interactive drawing, shape editing, snapping, measurement, and GeoJSON support.

Same drawing state, same algorithms, same GeoJSON — just a different map engine. If you need the flutter_map binding instead, see flutter_map_utils.


Features

Category Capabilities
Drawing Polygon, polyline, rectangle, circle, freehand — tap or drag to draw
Editing Drag vertices, insert midpoints, delete vertices (long-press), drag whole shapes
Hole Cutting Draw holes inside existing polygons
Selection Tap-to-select with hit-testing on edges and fills
Snapping Vertex, midpoint, edge, intersection, grid, perpendicular — priority-based
Measurement Distance & area with metric/imperial labels, per-segment display
Undo / Redo Full command-pattern history with configurable depth
GeoJSON Import & export with round-trip fidelity (string or Map)
Geometry Point-in-polygon, centroid, area, length, bounding box, self-intersection detection
Styles Fill, stroke, opacity, selected/hover states, presets, JSON serialization
UI Widgets Toolbar, info panel — or build your own
Cross-platform Android, iOS, Web

Installation

Add to your pubspec.yaml:

dependencies:
  google_map_utils: ^0.0.2

Then run:

flutter pub get

Note: This package depends on google_maps_flutter ^2.10.0. It is pulled in automatically.

This package re-exports everything from map_utils_core — including LatLng and all geo types — so you don't need to add any coordinate package separately.

Platform setup: You still need a Google Maps API key configured per the google_maps_flutter setup guide.


Quick Start

Option A: All-in-one widget

The fastest way to get a full drawing editor on screen:

import 'package:google_map_utils/google_map_utils.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

final drawingState = DrawingState();

GoogleMapGeometryEditor(
  drawingState: drawingState,
  initialCameraPosition: CameraPosition(
    target: LatLng(51.5, -0.1),
    zoom: 12,
  ),
)

This gives you a Google Map with a toolbar, drawing layer, selection, editing handles, info panel, and measurement — all wired together.

Option B: Manual composition

For full control, compose the pieces yourself inside a Stack:

final drawingState = DrawingState();
final controller = GmDrawingController(drawingState: drawingState);
final renderer = GmShapeRenderer(
  drawingState: drawingState,
  onShapeTap: (id) => drawingState.selectShape(id),
);

Stack(
  children: [
    GoogleMap(
      initialCameraPosition: CameraPosition(
        target: LatLng(51.5, -0.1),
        zoom: 12,
      ),
      onMapCreated: controller.onMapCreated,
      onTap: controller.handleTap,
      onLongPress: controller.handleLongPress,
      onCameraMove: (_) => controller.onCameraChanged(),
      polygons: renderer.polygons,
      polylines: renderer.polylines,
      circles: renderer.circles,
      scrollGesturesEnabled: !drawingState.shouldAbsorbMapGestures,
      zoomGesturesEnabled: !drawingState.shouldAbsorbMapGestures,
      rotateGesturesEnabled: !drawingState.shouldAbsorbMapGestures,
      tiltGesturesEnabled: !drawingState.shouldAbsorbMapGestures,
    ),

    // Freehand drawing (only when in freehand mode)
    if (drawingState.activeMode == DrawingMode.freehand)
      GmFreehandOverlay(controller: controller),

    // Vertex editing handles (only when a shape is selected)
    if (drawingState.selectedShape != null && !drawingState.isDrawing)
      GmVertexOverlay(controller: controller),

    // Whole-shape dragging
    if (drawingState.selectedShape != null && !drawingState.isDrawing)
      GmShapeDragger(controller: controller),

    // Toolbar
    DrawingToolbar(drawingState: drawingState),
  ],
)

Drawing Shapes

Polygon

drawingState.setMode(DrawingMode.polygon);
// User taps on the map to add vertices.
// Long-press to finish the polygon.
// Or call: controller.finishDrawing();

Polyline

drawingState.setMode(DrawingMode.polyline);
// Each tap adds a point. Long-press to finish.

Rectangle

drawingState.setMode(DrawingMode.rectangle);
// First tap = corner A, second tap = corner B → auto-finishes.

Circle

drawingState.setMode(DrawingMode.circle);
// First tap = center, second tap = radius → auto-finishes.

Freehand

drawingState.setMode(DrawingMode.freehand);
// The GmFreehandOverlay captures pointer drag → collects screen coordinates
// → batch-converts to LatLng on pointer-up → simplifies → smooths → commits.

Editing Shapes

drawingState.setMode(DrawingMode.select);

// Tap a shape to select it.
// GmVertexOverlay renders draggable handles at every vertex.
// Drag a vertex to move it.
// Tap an edge midpoint to insert a new vertex.
// Long-press a vertex to delete it (min vertex count enforced).
// GmShapeDragger lets you drag the whole shape.

Cutting Holes

// First, select a polygon:
drawingState.selectShape(polygonId);
drawingState.setMode(DrawingMode.hole);
drawingState.selectShape(polygonId); // re-select after mode change

// Each tap adds a hole vertex. Long-press to finish.
// The hole is cut from the selected polygon.

Measurement

final measureState = GmMeasurementState();

// In your GoogleMapGeometryEditor:
GoogleMapGeometryEditor(
  drawingState: drawingState,
  measurementState: measureState,
  initialCameraPosition: ...,
)

// Or add manually to your Stack:
// 1. Add the measurement polyline:
polylines.addAll(GmMeasurementOverlay.buildMeasurementPolyline(measureState));
// 2. Add the label overlay:
GmMeasurementOverlay(
  measurementState: measureState,
  controller: controller,
  unit: GmMeasurementUnit.metric, // or .imperial
)

// Tap on the map in measure mode to add points:
drawingState.setMode(DrawingMode.measure);

// Read values:
print(measureState.totalDistanceMeters); // 1234.5
print(measureState.areaSquareMeters);    // 56789.0
print(measureState.segmentDistances);    // [500.2, 734.3]

GeoJSON Export / Import

// Export all shapes to GeoJSON string
final geojson = GeoJsonUtils.toGeoJsonString(drawingState.shapes);

// Export to Map
final map = GeoJsonUtils.toFeatureCollection(drawingState.shapes);

// Import from GeoJSON string
final shapes = GeoJsonUtils.fromGeoJsonString(geojsonString);
drawingState.loadShapesFromJson(shapes.map((s) => s.toJson()).toList());

Snapping

final snapEngine = SnappingEngine(
  config: SnapConfig(
    tolerancePixels: 15,
    priorities: [SnapType.vertex, SnapType.midpoint, SnapType.grid],
    gridSpacing: 0.001,
  ),
);

final result = snapEngine.snap(
  candidatePoint: someLatLng,
  shapes: drawingState.shapes,
);

if (result != null) {
  // Use result.point — the snapped coordinate
  // result.type — which snap type matched
}

Shape Styles

// Custom style
final style = ShapeStyle(
  fillColor: Color(0x5500FF00),
  borderColor: Color(0xFF00FF00),
  borderWidth: 3.0,
  fillOpacity: 0.3,
  strokeType: StrokeType.dashed,
  selectedOverride: ShapeStyle(
    borderColor: Color(0xFFFF0000),
    borderWidth: 4.0,
  ),
);

drawingState.defaultStyle = style;

// Or use presets
drawingState.defaultStyle = ShapeStylePresets.zone;     // blue fill
drawingState.defaultStyle = ShapeStylePresets.warning;   // red fill
drawingState.defaultStyle = ShapeStylePresets.route;     // line-only

Undo / Redo

drawingState.undo();
drawingState.redo();

print(drawingState.canUndo); // true
print(drawingState.canRedo); // false

// History depth is configurable:
final state = DrawingState(maxHistoryDepth: 50);

Serialization

// Save shapes to JSON
final json = drawingState.shapesToJson();

// Restore shapes from JSON
drawingState.loadShapesFromJson(json);

// Clear everything
drawingState.clearAll();

Architecture

┌──────────────────────────────────────────┐
│              Stack                        │
│  ┌────────────────────────────────────┐  │
│  │           GoogleMap(               │  │
│  │             polygons: renderer.*   │  │  ← native Google Maps shapes
│  │             polylines: renderer.*  │  │
│  │             circles: renderer.*    │  │
│  │           )                        │  │
│  ├────────────────────────────────────┤  │
│  │       GmFreehandOverlay            │  │  ← freehand drawing preview
│  ├────────────────────────────────────┤  │
│  │        GmVertexOverlay             │  │  ← vertex/edge handles
│  ├────────────────────────────────────┤  │
│  │        GmShapeDragger              │  │  ← whole-shape drag
│  ├────────────────────────────────────┤  │
│  │     GmMeasurementOverlay           │  │  ← measurement labels
│  ├────────────────────────────────────┤  │
│  │     DrawingToolbar + ShapeInfoPanel│  │  ← shared UI widgets
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘
         │
    DrawingState (ChangeNotifier)
         │
    UndoRedoManager ← command pattern history

State management: DrawingState is a plain ChangeNotifier. Use it with Provider, Riverpod, Bloc, ListenableBuilder — whatever you prefer.

Coordinate conversion: Google Maps uses async coordinate conversion (getLatLng / getScreenCoordinate). All overlays handle this internally — you don't need to think about it.


API Reference

Core (re-exported from map_utils_core)

Class Purpose
DrawingState Central state: shapes, selection, mode, undo/redo
DrawingMode Enum: none, polygon, polyline, rectangle, circle, freehand, select, measure, hole
DrawableShape Sealed base class — DrawablePolygon, DrawablePolyline, DrawableCircle, DrawableRectangle
ShapeStyle Fill color, stroke color, stroke width, opacity, selected/hover states, JSON serializable
UndoRedoManager Command-pattern history with AddShapeCommand, RemoveShapeCommand, UpdateShapeCommand
GeometryUtils pointInPolygon, centroid, polygonArea, polylineLength, simplifyPath, smoothPath, and more
GeoJsonUtils toGeoJson, fromGeoJson, toGeoJsonString, fromGeoJsonString, toFeatureCollection
SnappingEngine Priority-based snapping: vertex, midpoint, edge, intersection, grid, perpendicular
SelectionUtils findClosestShape() with configurable tolerance

Google Maps Widgets

Widget / Class Purpose
GoogleMapGeometryEditor All-in-one wrapper that composes everything below
GmShapeRenderer Converts DrawableShape list → Set<Polygon>, Set<Polyline>, Set<Circle>
GmDrawingController Tap routing, coordinate conversion, mode dispatch, circle preview
GmFreehandOverlay Listener-based freehand with live CustomPaint preview
GmVertexOverlay Positioned vertex/edge handles — drag, insert, delete
GmShapeDragger Whole-shape drag via async coordinate conversion
GmMeasurementOverlay Per-segment distance labels + total distance + area
GmMeasurementState Pure ChangeNotifier for measurement points and calculations

Shared UI Widgets (from core)

Widget Purpose
DrawingToolbar Mode selector with undo/redo/delete buttons — fully customizable via buttonBuilder
ShapeInfoPanel Displays selected shape info: type, area, perimeter, coordinates

Extensions

Extension Purpose
StrokeTypeToGmPattern Converts StrokeTypeList<PatternItem> for polylines
LatLngToGm Converts LatLng (from map_utils_core) → google_maps.LatLng
GmLatLngToCore Converts google_maps.LatLngLatLng (from map_utils_core)
LatLngListToGm Converts List<LatLng>List<google_maps.LatLng>
GmGeometryUtils boundingBox()LatLngBounds from a list of points

Supported Shape Types

Shape Drawing Editing GeoJSON Holes
Polygon
Polyline
Rectangle
Circle
Freehand ✅ (as polyline)

Compatibility

Dependency Version
Flutter >= 3.27.0
Dart >= 3.6.0
google_maps_flutter ^2.10.0
map_utils_core ^0.0.2

Switching Between Map Engines

flutter_map_utils and google_map_utils are not interchangeable — they bind to different map SDKs with different APIs. Choose based on your map engine:

You use Package to add
flutter_map flutter_map_utils
google_maps_flutter google_map_utils

Both packages share the same DrawingState, ShapeStyle, GeoJsonUtils, snapping, serialization, and undo/redo logic from map_utils_core. So if you ever need to swap map engines, the migration is minimal:

  1. Replace flutter_map_utils with google_map_utils in your pubspec (or vice versa)
  2. Replace FlutterMapGeometryEditor with GoogleMapGeometryEditor (or vice versa)
  3. Keep all DrawingState, ShapeStyle, GeoJsonUtils, and other core code unchanged

Your shapes, styles, GeoJSON exports, and state management stay identical.


About

Built and maintained by UBXTY Unboxing Technology.

Part of the flutter_map_utils monorepo.


License

MIT — see LICENSE for details.

Libraries

google_map_utils
Drawing, editing & measurement widgets for Google Maps Flutter.