# Map Editor Interactive map editor on OpenStreetMap data: markers, polygons, polylines, circles, rectangles and tracks, with vertex editing, snap, undo/redo, free-text notes, address search, measurement, and round-trip import/export in **GeoJSON**, **KML** and **GPX**. Built on top of the [`gadgets/map`](https://cdn.sgapps.io/components/application-prototype-thirdparty/docs/#/gadgets/map.md) module (OpenStreetMap tiles via Leaflet, in-house editing + provider layer). The editor adds a save/export toolbar, a per-feature note panel, and the `/online/webapp/map-editor` window-manager wrapper so the whole thing drops into an iframe or an in-page custom element with one line. ## Live Demo ## Embed ```html ``` ### Open with a file The editor opens whatever file-manager path is passed as the first argument. The file extension picks the parser: | Extension | Format | Round-trip | |---|---|---| | `.geojson`, `.json` (GeoJSON content) | RFC 7946 FeatureCollection | ✅ full — circles survive as `Point + properties.radius_m`, rectangles as `Polygon + properties.shape = "rectangle"` | | `.kml` | OGC KML 2.2 Placemarks | ✅ points, lines, polygons; circles / rectangles kept via `` hints | | `.gpx` | GPX 1.1 (waypoints / tracks / routes) | points → ``, lines → ``, polygons → `` (ring, lossy on area semantics) | ```html ``` ### In-page (no iframe) The CDN-registered wrapper `b2b.editor.map-editor` creates an in-page custom element and connects to the window-manager app behind the scenes. ```html ``` --- ## Events Reference Everything below is sent through the standard webapp socket: ```js var socket = new WindowSocket(iframe.contentWindow); socket.start(); socket.fire("webapp::instance::request", eventName, ...args, callback); ``` The map also emits events upward through `api-event::instance:*`; see the full list under **Map events (upstream)** below. --- ### `toGeoJSON` -- Export the current map Returns everything on the map as an RFC 7946 FeatureCollection. Circles become `Point + properties.radius_m`; rectangles become `Polygon + properties.shape = "rectangle"`; notes are serialised as `properties.note`. ```js socket.fire("webapp::instance::request", "toGeoJSON", function (err, fc) { console.log(fc.features.length, "features"); }); ``` --- ### `loadGeoJSON` -- Replace or merge features Loads a FeatureCollection into the map. Options: `{ replace: true, fit: true }`. | Arg | Type | |---|---| | `fc` | FeatureCollection | | `opts` | `{replace?, fit?}` | | `callback` | `(err, acceptedCount)` | ```js socket.fire("webapp::instance::request", "loadGeoJSON", fc, { replace: true, fit: true }, function (err, n) { console.log("loaded", n); }); ``` --- ### `exportAs` -- Serialise to a format string | Arg | Type | |---|---| | `format` | `"geojson"` \| `"kml"` \| `"gpx"` | | `callback` | `(err, text: string)` | ```js socket.fire("webapp::instance::request", "exportAs", "kml", function (err, kmlText) { var blob = new Blob([kmlText], { type: "application/vnd.google-earth.kml+xml" }); var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "map.kml"; a.click(); }); ``` --- ### `addMarker`, `removeMarker`, `clearMarkers` ```js socket.fire("webapp::instance::request", "addMarker", { id: "pos-1", lat: 47.0105, lng: 28.8638, label: "HQ", color: "#d93025" }, function (err, id) { /* id = "pos-1" */ }); ``` Marker shape: `{ id, lat, lng, label?, title?, color?, size?, popup?, draggable?, opacity?, properties? }`. --- ### `drawPolygon`, `drawPolyline` Enter drawing mode: a click per vertex, double-click or **Done** to finish. ```js socket.fire("webapp::instance::request", "drawPolygon", "zone-a", { editable: true, color: "#d93025" }, function (err, id) { /* id when drawing starts */ }); ``` Finished shapes emit `event:shape-finished` upward. Click on the map to place vertices, then double-click (or press **Done** in the map toolbar) to finish. --- ### `setPolygon`, `setPolyline`, `setCircle`, `setRectangle` Place or replace a shape from code. Coordinates are latitude first. ```js socket.fire("webapp::instance::request", "setPolygon", "yard", [[47.01, 28.86], [47.02, 28.87], [47.03, 28.85]], { editable: true, color: "#d93025" }, function (err, id) {}); socket.fire("webapp::instance::request", "setCircle", "radius", 47.0105, 28.8638, 5000, { label: "5 km", color: "#188038" }, function (err, id) {}); socket.fire("webapp::instance::request", "setRectangle", "bbox", { south: 46.9, west: 28.7, north: 47.1, east: 29.0 }, {}, function (err, id) {}); ``` --- ### `getShapes`, `getShape`, `removeShape`, `clearShapes` ```js socket.fire("webapp::instance::request", "getShapes", function (err, list) { /* [{id, kind, label}] */ }); socket.fire("webapp::instance::request", "getShape", "yard", "pairs", function (err, data) { /* {coords: [[lat,lng],...]} or {lat,lng,radius} or {bounds} */ }); ``` --- ### `setView`, `getView`, `fitAll` ```js socket.fire("webapp::instance::request", "setView", 47.0105, 28.8638, 15, function (err) {}); socket.fire("webapp::instance::request", "getView", function (err, v) { /* {lat, lng, zoom, bounds: {south, west, north, east}} */ }); ``` --- ### `setMode`, `getMode` Modes: `view`, `pick-point`, `edit-polygon`, `edit-line`, `draw-polygon`, `draw-line`, `measure`. --- ### `geocode`, `reverse` Address search against the configured Nominatim provider (OSM default). Serialised at module level (≥ 1.1 s apart across every map on the page). ```js socket.fire("webapp::instance::request", "geocode", "Str. Alba Iulia 75, Chisinau", function (err, hits) { /* [{lat, lng, label, name, type, bbox, address, osm}] */ }); socket.fire("webapp::instance::request", "reverse", 47.0105, 28.8638, function (err, hit) { /* {lat, lng, label, address, osm} */ }); ``` Error codes to branch on: `ERR_NO_GEOCODER`, `ERR_NETWORK`, `ERR_TIMEOUT`, `ERR_PARSE`, `ERR_NO_RESULT`, `ERR_HTTP_`. --- ### `setProviders` -- swap tile / geocoding / routing endpoints ```js socket.fire("webapp::instance::request", "setProviders", { tiles: ["osm", { url: "https://tiles.example.com/{z}/{x}/{y}.png", attribution: "© Example" }], geocode: { url: "https://nominatim.example.com", countryCodes: "md,ro", language: "ro" }, route: { url: "https://osrm.example.com", profile: "driving", attribution: "Routing: OSRM" } }, function (err) {}); ``` Shorthand `{ tiles: "blank" }` renders the tile-less coordinate graticule (no network traffic). --- ### `undo`, `redo` One step of vertex editing. Returns a boolean. --- ### `setNote`, `getNote` Attach a free-text description to any marker or shape (`properties.note` in the GeoJSON export). ```js socket.fire("webapp::instance::request", "setNote", "pos-1", "Main warehouse — gate 2", function (err) {}); socket.fire("webapp::instance::request", "getNote", "pos-1", function (err, note) { /* string or null */ }); ``` The in-app **Note…** button opens a picker over the current markers and shapes. --- ### `save`, `saveAs` Writes the current map back to the file the editor was opened with (via `app.fs()`): ```js socket.fire("webapp::instance::request", "save", function (err) {}); ``` Save-As asks for a filename via `file-selector`: ```js socket.fire("webapp::instance::request", "saveAs", "kml", null, function (err) {}); ``` Or save directly to a known path: ```js socket.fire("webapp::instance::request", "saveAs", "gpx", "/home/me/trail.gpx", function (err) {}); ``` --- ## Map events (upstream) These are forwarded through `api-event::instance:*` and arrive on the socket as: ```js socket.on("api-event::instance:event:polygon-changed", function (e) { if (e.phase !== "commit") return; // 'drag' fires per mouse move console.log("polygon", e.id, e.area_m2); }); ``` | Event | Payload | |---|---| | `ready` | `app` | | `event:click` | `{lat, lng}` | | `event:point-changed` | `{id, lat, lng}` | | `event:marker-moved` | `{id, lat, lng}` | | `event:marker-click` | `{id, lat, lng, label}` | | `event:polygon-changed` | `{id, coords, phase, points, area_m2}` -- `phase` is `drawing`, `drag` (live) or `commit` | | `event:polyline-changed` | `{id, coords, phase, points, length_m}` | | `event:shape-finished` | `{id, kind, coords}` | | `event:shape-click` | `{id, lat, lng}` | | `event:view-changed` | `{lat, lng, zoom, bounds}` (debounced) | | `event:mode-changed` | `{mode}` | | `event:layer-changed` | `{index, name}` | | `event:search-results` | `{query, results}` | | `event:search-selected` | the result row | | `event:measure` | `{points, length_m, area_m2, coords}` | | `event:route-done` | the route | | `event:track` | `{id, progress, walked_m, total_m, lat, lng, done}` | | `event:locate` | `{lat, lng, accuracy_m}` | | `event:error` | `{code, message, error}` | **Persist on `commit`, not on `drag`:** a vertex drag emits `polygon-changed` with `phase: "drag"` once per mouse move; wait for `phase: "commit"` before writing to a database. --- ## Use Cases ### Delivery zone editor ```js socket.fire("webapp::instance::request", "setPolygon", "zone", existingCoords, { editable: true, color: "#d93025" }); socket.on("api-event::instance:event:polygon-changed", function (e) { if (e.phase === "commit") form.zoneGeoJson.value = JSON.stringify(e.coords); }); ``` ### Geofence check against a drawn zone Load the GeoJSON that comes back from `toGeoJSON()` into any point-in-polygon helper on the server, or (if the host also has `gadgets/map/geo`) run the check client-side with the same semantics the module uses. ### Store locator + address search ```js socket.fire("webapp::instance::request", "geocode", userInput, function (err, hits) { if (hits.length) { socket.fire("webapp::instance::request", "setView", hits[0].lat, hits[0].lng, 16, function () {}); } }); ``` ### Blank-canvas map (no OSM tiles) ```js socket.fire("webapp::instance::request", "setProviders", { tiles: "blank" }, function () {}); ``` The coordinate grid replaces the tiles, every vector feature still works, and no requests are sent to any third party. Useful for air-gapped demos and for review builds that must not contact OSM. --- ## Privacy / what reaches third parties By default the editor uses the standard OpenStreetMap tiles (`tile.openstreetmap.org`) and Nominatim geocoding (`nominatim.openstreetmap.org`). Each receives the viewer's IP, the browser's `Referer`, and the tile coordinates or searched text. There is no autocomplete, no prefetch, no cache. Routing has no default provider -- `route()` without a configured server fails with `ERR_NO_ROUTER` and sends nothing. To disable OSM entirely use the `blank` tiles shorthand above. See [`gadgets/map` docs §What reaches third parties](https://cdn.sgapps.io/components/application-prototype-thirdparty/docs/#/gadgets/map.md) for the full policy summary.