v3.0.0

<bmx-geo-editor>

Edits map features - points, lines and polygons with holes, and several of each in one feature - and their attributes, in the page, with nothing sent anywhere. Draw with snapping to vertices and edges; select, move and reshape (drag vertices, drag a midpoint to add one, delete them); cut holes and add parts; split along a drawn line, reshape with one, merge, separate parts, buffer by metres and simplify; topology checks (crossings, overlaps, gaps between neighbours) with fixes. The attribute table is a BMX data grid with typed fields (text, numbers, whole numbers, yes/no, dates), in step with the map both ways; fields are added, renamed, retyped and removed in the panel. Lengths and areas are geodesic, on the WGS84 ellipsoid.

15 properties · 4 events · 26 methods · 5 parts

Example

Try it with your own map files in confidence: they are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.

Show markup
<bmx-geo-editor id="ex-geo" label="Hollow Farm - field boundaries"></bmx-geo-editor>
<p class="note" id="ex-geo-out" role="status" style="margin: 12px 0 0"></p>

<bmx-geo-editor id="ex-grid" label="Street lights - British National Grid" table="false" style="margin-block-start: 24px"></bmx-geo-editor>
<p class="note" id="ex-grid-out" role="status" style="margin: 12px 0 0"></p>

<script type="module">
  await customElements.whenDefined('bmx-geo-editor');

  // An invented farm: field boundaries in longitude and latitude, one with a pond cut out of it,
  // drawn over a background of invented roads, a stream and woodland. Hill Field was drawn over the
  // edge of Top Piece: the Issues tab shows the overlap and fixes it.
  const at = (x, y) => [-1.75 + x * 0.0016, 52.05 + y * 0.001];
  const field = (id, name, crop, organic, surveyed, ring, holes = []) => ({
    type: 'Feature',
    id,
    properties: { name, crop, organic, surveyed },
    geometry: { type: 'Polygon', coordinates: [ring.map(([x, y]) => at(x, y)), ...holes.map(h => h.map(([x, y]) => at(x, y)))] },
  });
  const editor = document.getElementById('ex-geo');
  editor.background = [
    {
      name: 'Roads, stream and woods',
      data: {
        type: 'FeatureCollection',
        features: [
          { type: 'Feature', properties: { fill: 'rgba(76, 145, 65, 0.28)', stroke: 'rgba(76, 145, 65, 0.6)' }, geometry: { type: 'Polygon', coordinates: [[at(-1, 7), at(3, 8), at(4, 11), at(-1, 11), at(-1, 7)]] } },
          { type: 'Feature', properties: { fill: 'rgba(76, 145, 65, 0.28)', stroke: 'rgba(76, 145, 65, 0.6)' }, geometry: { type: 'Polygon', coordinates: [[at(11, -2), at(14, -2), at(14, 2), at(12, 1), at(11, -2)]] } },
          { type: 'Feature', properties: { stroke: '#7fb2e5', 'stroke-width': 3 }, geometry: { type: 'LineString', coordinates: [at(-1, 2.5), at(2, 3.2), at(5, 2.4), at(8, 3.6), at(11, 3), at(14, 4)] } },
          { type: 'Feature', properties: { stroke: '#d9a441', 'stroke-width': 4 }, geometry: { type: 'LineString', coordinates: [at(-1, -1), at(14, -1.2)] } },
          { type: 'Feature', properties: { stroke: '#c9ccd1', 'stroke-width': 2 }, geometry: { type: 'LineString', coordinates: [at(6.5, -1.1), at(6.4, 4), at(6.8, 9)] } },
        ],
      },
    },
  ];
  editor.layer = {
    type: 'FeatureCollection',
    name: 'Hollow Farm',
    features: [
      field(1, 'Long Ground', 'Winter wheat', false, '2026-05-14', [[0, 0], [6, 0], [6, 2.4], [0, 2]], [[[2, 0.6], [2.8, 0.6], [2.8, 1.2], [2, 1.2]]]),
      field(2, 'Brook Meadow', 'Permanent grass', true, '2026-06-02', [[0, 2.6], [6, 2.9], [6, 6], [0, 6.4]]),
      field(3, 'Top Piece', 'Spring barley', false, '2026-04-21', [[7, 0], [12, 0.2], [12, 3.2], [7, 3.4]]),
      field(4, 'Hill Field', 'Oilseed rape', false, null, [[7, 3.3], [12, 3.1], [12.5, 7], [7, 7.5]]),
      field(5, 'Little Close', 'Permanent grass', true, '2026-06-02', [[0, 6.8], [3, 6.6], [3, 8.6], [0, 9]]),
    ],
  };

  const out = document.getElementById('ex-geo-out');
  const report = async () => {
    const layer = await editor.getLayer();
    let area = 0;
    for (const f of layer.features) area += (await editor.measure(f.id))?.area ?? 0;
    out.textContent = `${layer.features.length} fields, ${(area / 10000).toFixed(2)} ha in all (geodesic, on the WGS84 ellipsoid).`;
  };
  editor.addEventListener('bmxChange', report);
  await report();

  // Street lights as a council keeps them: a CSV of eastings and northings on the British National Grid.
  // The editor reads the grid from the column names, draws the lights on the map and keeps the numbers as they are.
  const grid = document.getElementById('ex-grid');
  const rows = ['Asset,Easting,Northing,Lamp,Height (m),Installed'];
  for (let i = 0; i < 18; i += 1) rows.push(`SL-${String(i + 1).padStart(3, '0')},${(431200 + (i % 6) * 42.5).toFixed(1)},${(206400 + Math.floor(i / 6) * 55 + (i % 2) * 6).toFixed(1)},${i % 3 ? 'LED 35W' : 'LED 50W'},${i % 3 ? 6 : 8},2025-0${1 + (i % 9)}-15`);
  await grid.load(rows.join('\n'));
  grid.coordinates = 'degrees';
  const gridOut = document.getElementById('ex-grid-out');
  grid.addEventListener('bmxSelect', e => {
    const f = e.detail.features[0];
    gridOut.textContent = f ? `${f.properties.Asset}: easting ${f.geometry.coordinates[0]}, northing ${f.geometry.coordinates[1]} - ${f.properties.Lamp}` : '';
  });
</script>

Reads and writes GeoJSON, Shapefiles (with their .dbf table, .prj system and character set, loose or zipped), KML and KMZ, GPX and CSV (coordinate columns or WKT). Coordinates stay in the system they came in - longitude and latitude, Web Mercator, UTM, British National Grid, the Irish grids, Lambert-93 or any Transverse Mercator, Mercator or Lambert Conformal Conic a .prj describes - and can be converted to another. The map is drawn over the page's own GeoJSON as background layers; no map tiles are fetched.

<bmx-geo-editor src="/data/fields.geojson"></bmx-geo-editor>

Properties

PropertyAttributeTypeDefaultDescription
background background BmxGeoEditorBackground[] | Record<string, unknown> | string — Layers drawn under the edited one: a list of { name, src | data, fill, stroke }, GeoJSON, or the address of a GeoJSON file.
coordinates coordinates string 'layer' How the pointer's coordinates are shown: layer (the layer's own system), degrees, dms, or an EPSG code such as EPSG:27700. Two-way.
gapLimit gap-limit number 10000 Gaps between polygons up to this many square metres are reported (larger ones are taken as meant).
label label string — A heading for the editor (the layer's name when not given).
layer layer BmxGeoLayer | Record<string, unknown> | string | null null The layer: features, fields and coordinate system, as an object, GeoJSON or JSON text. Two-way: bmxChange reports every edit.
overlapTolerance overlap-tolerance number 0.01 Overlaps between polygons smaller than this many square metres are not reported.
panel panel boolean true Show the side panel. Two-way.
readonly readonly boolean false Show the layer without changing it.
snap snap boolean true Snap new and moved vertices to the vertices and edges nearby. Two-way.
snapTolerance snap-tolerance number 10 How near, in pixels, a vertex or edge must be to snap to.
src src string — An address to read a map file from (the page's own server): GeoJSON, a zipped Shapefile, KML, KMZ, GPX or CSV.
strings strings Partial<BmxGeoEditorStrings> | string — Words for translation: an object or JSON.
table table boolean true Show the attribute table under the map. Two-way.
tool tool BmxGeoEditorTool 'select' The tool in use. Two-way.
toolbar toolbar boolean true Show the toolbar.

Events

EventDetailDescription
bmxChange { readonly layer: BmxGeoLayer; } Fired after every change, with the layer.
bmxError { readonly message: string; } Fired when something cannot be read or written.
bmxLoad { readonly format: string; readonly features: number; readonly notes: readonly string[]; } Fired when a file has been read, with what was found and noted.
bmxSelect { readonly ids: readonly string[]; readonly features: readonly BmxGeoFeature[]; } Fired when the selection changes, with the selected features' ids.

Methods

MethodSignatureDescription
addFeature addFeature(feature: { id?: string; geometry?: unknown; properties?: Record<string, unknown>; } | BmxGeoGeometry) => Promise<string> Adds a feature (a GeoJSON Feature or a geometry), in the layer's system. Resolves with its id.
buffer buffer(metres: number, ids?: readonly string[]) => Promise<string[]> Adds, for each feature (the selection when none are given), a polygon of everything within metres of it (negative: a polygon shrunk), with its attributes. Resolves with the new ids.
check check() => Promise<BmxGeoIssue[]> Everything wrong with the features.
checkTopology checkTopology(options?: BmxGeoTopologyOptions) => Promise<BmxGeoTopologyIssue[]> How the layer's features sit: crossings, overlaps, gaps between neighbours, duplicates.
explode explode(ids?: readonly string[]) => Promise<string[]> Makes each part of multi-part features (the selection when none are given) a feature of its own. Resolves with the ids.
export export(kind: BmxGeoEditorExport, options?: { crs?: "wgs84" | "keep"; }) => Promise<Blob> A file of the layer: GeoJSON, a Shapefile (ZIP), KML, KMZ, GPX, CSV, or a picture of the map (PNG).
fit fit() => Promise<void> Shows the whole layer (and the background layers when the layer is empty).
fixTopology fixTopology(issue: BmxGeoTopologyIssue) => Promise<void> Fixes a topology issue: cleans a crossing polygon, cuts an overlap from the later feature, fills a gap from the neighbour sharing the longest edge.
getLayer getLayer() => Promise<BmxGeoLayer> The layer as it stands (a copy).
getSelection getSelection() => Promise<string[]> The selected features' ids.
load load(data: Blob | readonly Blob[] | FileList | BmxGeoLayer | Record<string, unknown> | string) => Promise<void> Opens a map file or files (a File, a list of Files such as a Shapefile's parts, a ZIP or KMZ), or a layer or GeoJSON object or text.
measure measure(id: string) => Promise<{ length: number; area: number; } | null> A feature's geodesic length (or perimeter) in metres and area in square metres.
merge merge(ids?: readonly string[]) => Promise<string | null> Merges features (the selection when none are given) of one kind into the first: polygons dissolved, lines joined, points gathered. Resolves with its id, or null.
redo redo() => Promise<boolean>
removeFeatures removeFeatures(ids: readonly string[]) => Promise<number> Removes features by id. Resolves with how many went.
reproject reproject(crs: string) => Promise<void> Converts the layer into another coordinate system ('EPSG:27700', WKT ...).
reshape reshape(id: string, line: readonly BmxGeoPosition[]) => Promise<boolean> Reshapes a line or polygon with a drawn line (positions in the layer's system) that starts on it (and for a polygon ends on it).
save save(kind?: BmxGeoEditorExport, fileName?: string) => Promise<void> Downloads a file of the layer (see export).
select select(ids: readonly string[]) => Promise<void> Selects features by id (an empty list clears the selection).
setFocus setFocus() => Promise<void>
setLayer setLayer(layer: BmxGeoLayer | Record<string, unknown> | string) => Promise<void> Replaces the layer (a layer object, GeoJSON or JSON text); undo history is cleared.
simplify simplify(metres: number, ids?: readonly string[]) => Promise<number> Simplifies features (the selection when none are given) within metres. Resolves with how many vertices went.
split split(line: readonly BmxGeoPosition[], ids?: readonly string[]) => Promise<string[]> Splits features along a line (positions in the layer's system): the given ones, the selection, or every one it crosses. Resolves with the ids of the pieces.
undo undo() => Promise<boolean>
updateFeature updateFeature(id: string, patch: { geometry?: BmxGeoGeometry | null; properties?: Record<string, unknown>; }) => Promise<boolean> Changes a feature's geometry or attributes (attributes merged).
zoomTo zoomTo(ids?: readonly string[]) => Promise<void> Shows features (by id; the selection when none are given) or everything.

CSS shadow parts

PartDescription
panel The side panel (feature, fields, layer, issues).
stage The map.
status The line under the map.
table The attribute table under the map.
toolbar The bar of commands.

CSS custom properties

PropertyDescription
--bmx-geo-editor-background The map's background (where nothing is drawn).
--bmx-geo-editor-background-fill Areas of the background layers.
--bmx-geo-editor-background-stroke Lines and outlines of the background layers.
--bmx-geo-editor-fill The edited layer's areas.
--bmx-geo-editor-handle Vertex handles and the line being drawn.
--bmx-geo-editor-height The height of the map and table together.
--bmx-geo-editor-highlight Selected features.
--bmx-geo-editor-highlight-fill Selected areas.
--bmx-geo-editor-issue An issue's overlap or gap, and the ring round its place.
--bmx-geo-editor-issue-fill An issue's overlap or gap, filled.
--bmx-geo-editor-panel-width The side panel's width.
--bmx-geo-editor-point-stroke The ring round each point.
--bmx-geo-editor-radius Corner radius of the frame.
--bmx-geo-editor-stroke The edited layer's lines, outlines and points.
--bmx-geo-editor-table-height The attribute table's height.