# Maps: Geographic Annotation with Points, Lines, Polygons and Regions

Source: https://www.potatoannotator.com/docs/annotation-types/geo-annotation

*New in v2.12.0*

`geo_annotation` puts a map on the annotation page. Annotators draw labelled points, lines and polygons on it, pin places, or give labels to the regions of an outline map. The map is [OpenLayers](https://openlayers.org/) 10.9.0, which Potato serves itself, so no map library loads from the internet.

```yaml
annotation_schemes:
  - annotation_type: geo_annotation
    name: land_cover
    description: "Outline the land cover"
    task: draw
    tools: [polygon, freehand, point]
    labels:
      - {name: Water, color: "#0072B2"}
      - {name: Forest, color: "#009E73"}
    basemap:
      geojson: maps/regions.geojson
```

The Potato repository has two runnable examples. `examples/advanced/dialect-map/` is a draw-a-map dialect survey on a fictional country that uses all three tasks ([Perceptual Dialectology](/docs/guides/perceptual-dialectology)), and `examples/image/geo-annotation/` outlines land cover over georeferenced images, one image per item, with a suggested polygon on the first item.

## Tasks

| `task` | What the annotator does | What is stored |
|---|---|---|
| `draw` (default) | Draws points, lines, polygons and freehand polygons, and gives each a label, plus a typed name with `feature_text` | One GeoJSON feature per drawing |
| `pin` | Places one point for each label. Each label is a question, such as "Where were you born?" | One point per label |
| `classify_region` | Picks a label, then clicks regions of the basemap outline to give them that label. Clicking a region again with the same label clears it | One feature per labelled region, with `geometry: null` and the region ID |

## The basemap

**Every scheme needs a basemap**, in `basemap` or per item through `basemap_field`, and without one `potato validate` and the server refuse the config. Potato does not fall back to an internet map, so a study loads no map from the internet unless its config names one.

| Source | Config | Notes |
|---|---|---|
| GeoJSON outline | `basemap: {geojson: maps/regions.geojson}` | A file under `task_dir`, embedded in the page. Keep it simplified, a few hundred KB at most, because it is part of every annotation page |
| Georeferenced image | `basemap: {image: /media/area.png, extent: [min_lon, min_lat, max_lon, max_lat]}` | A PNG or JPEG in plate carrée (equirectangular) projection covering `extent` |
| GeoTIFF or Cloud Optimized GeoTIFF | `basemap: {geotiff: /media/area.tif}` | Read with geotiff.js 3.0.5, which ships with Potato. The file must be in EPSG:4326 or EPSG:3857, and the browser needs WebGL |
| XYZ tiles | `basemap: {tiles: "https://tiles.example.org/{z}/{x}/{y}.png", attribution: "..."}` | Your own tile server or a provider you have an agreement with. `attribution` is required for an external URL, and `{-y}` is accepted for TMS numbering |

A GeoJSON outline can be combined with one raster source, and the outline is drawn on top.

The OpenStreetMap Foundation's tile servers run on donated resources, and their [tile usage policy](https://operations.osmfoundation.org/policies/tiles/) forbids heavy use. A study with many annotators panning a map is heavy use, so Potato accepts the URL with a warning in the log. Host your own tiles or use a commercial provider.

`basemap_field` names an item field that holds the item's basemap: a GeoJSON FeatureCollection, a string URL (`.tif` and `.tiff` are read as GeoTIFF, anything else as GeoJSON), or an object with the same keys as `basemap`. The server checks each item's basemap against the same rules before sending the item, and an item may name an external server only when the scheme's own `basemap` names a URL on that host.

For `classify_region`, each outline feature is a region. Its ID is the `region_id_property` property (default `id`), then the GeoJSON feature ID, then `r<index>`, and its name is the `region_name_property` property (default `name`).

## Options

| Key | Default | Meaning |
|---|---|---|
| `task` | `draw` | `draw`, `pin` or `classify_region` |
| `labels` | required | Strings or `{name, color, description}`. Colours default to an Okabe-Ito palette. Quote `Yes`, `No` and `On`, which YAML otherwise loads as booleans |
| `tools` | `[point, line, polygon, freehand]` | `draw` only. A select-and-move tool is always added |
| `feature_text` | off | `true`, or `{prompt, required, placeholder}`: a text box per feature, stored as `properties.text` |
| `basemap` | none | `geojson`, `image`, `extent`, `geotiff`, `tiles`, `attribution`, `max_zoom`, `region_id_property`, `region_name_property` |
| `basemap_field` | none | Item field with a per-item basemap |
| `preannotation_field` | none | Item field with GeoJSON features to start from (not for `classify_region`) |
| `coordinate_order` | `lonlat` | `latlon` if the preannotation data or the outline writes latitude first. Output is always `[lon, lat]` |
| `projection` | `EPSG:3857` | Display projection, `EPSG:3857` (Web Mercator) or `EPSG:4326`. Storage is always EPSG:4326 |
| `center`, `zoom` | fit the basemap | Initial view, with `center: [lon, lat]` |
| `height` | `440` | Map height in pixels, from 200 to 1200 |
| `snapping` | `false` | Snaps new vertices to existing features and to the outline |
| `min_features`, `max_features` | 1 if required, none | `draw` only. Drawing tools are disabled at the maximum |
| `require_all_regions` | `false` | `classify_region`: every region must get a label before Next |
| `label_requirement` | none | `{required: true}` blocks Next until the task's own condition holds: `min_features` features for `draw`, a pin for every label for `pin`, at least one region (or all) for `classify_region` |

## Using the map

To draw, choose a label, then a tool. A point is one click. A line is a click per point and a double-click on the last. A polygon is a click per corner, finished by clicking the first corner, double-clicking or pressing Enter, with Backspace removing the last corner and Esc cancelling. Freehand is press and drag. The select tool reshapes a feature by its outline or a vertex, and Delete removes it. Every feature appears in the list under the map with a label menu, the name box, Show and Delete.

For pins, the list shows one row per label, and clicking the map places the active one. For regions, pick a label button and click regions.

From the keyboard, Tab to the map, then the arrow keys pan and `+` and `-` zoom. Add point at map centre and Use map centre place points without a mouse, and every feature's label, name and deletion are reachable through the list.

Features from `preannotation_field` are marked "(suggested)". They are not saved as the annotator's answer until the annotator edits one or presses Accept suggestions, so an item the annotator only opened does not count as labelled.

## Stored data

The answer is a GeoJSON FeatureCollection in EPSG:4326 following [RFC 7946](https://www.rfc-editor.org/rfc/rfc7946): positions are `[longitude, latitude]`, exterior rings run counter-clockwise and holes clockwise, coordinates have 7 decimals (about 1 cm), and longitudes are wrapped into -180 to 180.

```json
{"type": "FeatureCollection", "features": [
  {"type": "Feature", "id": "f1",
   "geometry": {"type": "Polygon", "coordinates": [[[-29.0, -38.3], [-28.2, -38.3], [-28.6, -37.5], [-29.0, -38.3]]]},
   "properties": {"label": "Very different", "text": "Coast talk", "tool": "polygon"}}
]}
```

The scheme also works on survey pages, where there is no item, so `basemap_field` and `preannotation_field` do nothing there.

## Export

```bash
# Every feature as GeoJSON, one file per item (geojson/<item>.geojson)
python -m potato.export --config config.yaml --format geojson --output out/
# ... or one file for the whole project
python -m potato.export --config config.yaml --format geojson --output out/ --option layout=single

# Heat maps of overlapping polygons, plus CSVs of names and region labels
python -m potato.export --config config.yaml --format geo_overlap --output out/
```

The [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) export normalises every feature again and adds `item_id`, `user_id`, `schema` and `feature_id` to its properties. Features that cannot be normalised, such as a position out of range or a ring with no area, are left out and listed in the export warnings. `classify_region` answers get their region geometry back from the outline. `geo_overlap` is described in [Perceptual Dialectology](/docs/guides/perceptual-dialectology#overlap-heat-maps).

## Limits

- A polygon that crosses the 180° meridian is not split, so draw two polygons.
- A GeoTIFF in a projection other than EPSG:4326 or EPSG:3857 does not show, and neither does any GeoTIFF without WebGL. The page says so, and the outline still works.
- A GeoTIFF is drawn without interpolation, so zooming in shows the real pixel boundaries.
- For pixel coordinates on an ordinary image rather than geographic ones, use [image annotation](/docs/annotation-types/image-annotation).

## Further Reading

- [Perceptual Dialectology](/docs/guides/perceptual-dialectology) - Draw-a-map dialect surveys and overlap heat maps
- [Camera Traps](/docs/guides/camera-traps) - Detector review with Darwin Core occurrence export
- [Air-Gapped Annotation](/docs/guides/air-gapped-annotation) - Running Potato with no internet access

For implementation details, see the [source documentation](https://github.com/davidjurgens/potato/blob/master/docs/annotation-types/spatial/geo_annotation.md).
