MappiaIO (embed a map, talk to it)

A Mappia map can live inside any page - a dashboard, a store front, a form, a plain HTML file - and work with it. MappiaIO is the small library that connects the two: your page loads https://maps.csr.ufmg.br/mappia_io.js, embeds the calculator in an <iframe>, and from then on its buttons, lists and forms drive the map, while the map tells the page what the user clicked, hovered or drew. The page decides what the map shows (applyQuery); the two exchange plain objects (send one way, ExtjsUtils.QUERY.postMessage the other).

A web page with its own buttons and a message log on the left and an embedded Mappia map on the right. The map's panel says what the page asked; the page shows the cities the map reported.
A page with a map in it, mid-conversation. On the left, your page: its buttons asked the map to show Belo Horizonte, and its log lists every message both ways. On the right, the map: its panel says what the page asked, a click on Rio de Janeiro and the panel's own button reported back to the page. Open it live · read its source

How a message travels

Two windows - your page and the map inside its iframe - can only talk through the browser’s window.postMessage. MappiaIO wraps every message as {mappia_iframe: "<JSON text>"} and handles the start-up, so you only deal with your own objects:

  1. Your page (mappia_io.js)The map (platform and your query)
  2. MappiaIO("mappia", true)mappia__checkDOMThe platform hears the greeting
  3. Queued messages go out; addReadyCallback functions runmappia__confirmDOMThe platform answers: listening
  4. applyQuery(text)mappia__applyQueryThe platform loads your query
  5. A mappia__... string: a platform noticemappia__confirmQueryListeningThe query calls ExtjsUtils.QUERY.setMappiaIoCallback(fn)
  6. The new layers are in placemappia__queryAppliedThe platform finished applying
  7. send({operation, message})your objectfn(object) runs in the query
  8. addOnMessageCallback(fn): fn(object)your objectExtjsUtils.QUERY.postMessage(object)
  • Before the handshake nothing is lost: send and applyQuery called right away wait in a queue and go out, in order, once the map answers.
  • Your messages are plain objects. They travel as JSON, so numbers, text, arrays and nested objects arrive intact; functions and dates do not. By convention an object names an operation and carries a message, plus a requestId when the page waits for an answer.
  • Strings starting with mappia__ belong to the platform. Your page receives them too (mappia__queryApplied is the useful one); never send them.
  • The page and the map may be on different sites - this page, on mappia.earth, talks to maps.csr.ufmg.br - and a message takes about 150 ms to arrive.

Try it

The playground below is a page talking to an embedded map exactly as yours would. Pick any example of this documentation and apply it with applyQuery, edit it and apply it again, or pick MappiaIO - the cities example of this page - and send it messages. The cities example also talks back: click a city, or press Send the cities in view to the page in the map’s panel, and watch the message arrive.

1. Apply a query applyQuery(text)

Query text - edit it freely (Ctrl+Enter applies it)

2. Send a message send(object)

The cities example answers these; other examples ignore them. Its own button in the map's panel talks back.

3. Messages

  1. Start the live map, then apply a query.

A complete page

The page in the picture above is one HTML file you can copy and open as it is. Its parts, in the order a page needs them:

  1. The map and the library: an <iframe> with the calculator, and mappia_io.js from the map server.
  2. The connection: MappiaIO("mappia", true) - true because the page keeps talking to the map after the start-up.
  3. The page’s buttons call send (show a city, add one) or ask - a send with a requestId that resolves when the map answers with the same id (list the cities).
  4. One addOnMessageCallback handles everything the map says: the answers to ask, the city the user clicked, the cities the panel’s button reported, and mappia__queryApplied, which enables the buttons.
  5. addReadyCallback applies the query - applyQueryFromUrl("cities-query.js"), a file next to the page - as soon as the map listens.

The map side is the MappiaIO example of the playground: a query that registers its listener with ExtjsUtils.QUERY.setMappiaIoCallback, answers with ExtjsUtils.QUERY.postMessage, and has its own button in the panel.

The whole page - host-page.html (193 lines)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Your page, with a Mappia map in it</title>
<style>
  * { box-sizing: border-box; }
  html, body { height: 100%; margin: 0; }
  body {
    display: flex; flex-direction: column;
    font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    color: #1f2430; background: #f4f5f7;
  }
  header {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 16px; background: #fff; border-bottom: 1px solid #dfe3e8;
  }
  header h1 { margin: 0; font-size: 16px; }
  #status { font-size: 12px; color: #6b7280; }
  #status.ready { color: #15803d; }
  main { flex: 1; display: flex; min-height: 0; }
  aside {
    width: 340px; flex: none; overflow-y: auto;
    padding: 14px; background: #fff; border-right: 1px solid #dfe3e8;
  }
  section { margin-bottom: 18px; }
  h2 { margin: 0 0 8px; font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: #6b7280; }
  .row { display: flex; gap: 6px; margin-bottom: 6px; }
  select { flex: 1; min-width: 0; padding: 6px; border: 1px solid #dfe3e8; border-radius: 6px; font: inherit; }
  button {
    padding: 6px 10px; border: 1px solid #2563eb; border-radius: 6px;
    background: #2563eb; color: #fff; font: inherit; cursor: pointer;
  }
  button:hover { background: #1d4ed8; }
  button:disabled { background: #cbd2d9; border-color: #cbd2d9; cursor: default; }
  #from-map {
    min-height: 3em; padding: 8px 10px; border-radius: 6px;
    background: #f0fdf4; border: 1px solid #bbf7d0; color: #14532d;
  }
  #log { margin: 0; padding: 0; list-style: none; font: 12px/1.4 ui-monospace, Consolas, monospace; }
  #log li { padding: 4px 0; border-bottom: 1px solid #eef0f3; word-break: break-word; }
  #log .direction { display: inline-block; width: 88px; font-weight: 600; }
  #log .out .direction { color: #2563eb; }
  #log .in .direction { color: #15803d; }
  #log .notice { color: #6b7280; }
  iframe { flex: 1; min-width: 0; border: 0; background: #fff; }
</style>
</head>
<body>
<header>
  <h1>Your page</h1>
  <span id="status">Connecting to the map...</span>
</header>
<main>
  <aside>
    <section>
      <h2>Your buttons drive the map</h2>
      <div class="row">
        <select id="city" aria-label="City"><option>Belo Horizonte</option></select>
        <button id="show" disabled>Show on the map</button>
      </div>
      <div class="row">
        <button id="list" disabled>List the cities</button>
        <button id="add" disabled>Add Recife</button>
      </div>
    </section>
    <section>
      <h2>The map tells your page</h2>
      <div id="from-map">Click a city on the map, or press the button in the map's panel.</div>
    </section>
    <section>
      <h2>Messages</h2>
      <ol id="log"></ol>
    </section>
  </aside>
  <iframe id="mappia" title="Mappia map"></iframe>
</main>

<!--
  The map server. In your page these are two fixed tags:
    <iframe id="mappia" src="https://maps.csr.ufmg.br/calculator/?lang=eng&options=scale"></iframe>
    <script src="https://maps.csr.ufmg.br/mappia_io.js"></script>
  Here they are built from the address, so ?mappia_origin=https://localhost runs this page
  against a local Mappia.
-->
<script>
  var MAPPIA = new URLSearchParams(location.search).get("mappia_origin") || "https://maps.csr.ufmg.br";
  document.getElementById("mappia").src = MAPPIA + "/calculator/?lang=eng&options=scale";
  var library = document.createElement("script");
  library.src = MAPPIA + "/mappia_io.js";
  library.onload = start;
  document.head.appendChild(library);

  function start() {
    // true: keep listening after the handshake - this page talks with the map all along
    var mappia = MappiaIO("mappia", true);
    var citySelect = document.getElementById("city");
    var showButton = document.getElementById("show");
    var listButton = document.getElementById("list");
    var addButton = document.getElementById("add");
    var fromMap = document.getElementById("from-map");
    var waiting = {};
    var lastId = 0;

    // Every message, both ways, in the log.
    function write(className, direction, message) {
      var item = document.createElement("li");
      item.className = className;
      var label = document.createElement("span");
      label.className = "direction";
      label.textContent = direction;
      item.appendChild(label);
      item.appendChild(document.createTextNode(typeof message === "string" ? message : JSON.stringify(message)));
      document.getElementById("log").prepend(item);
    }

    // page -> map
    function send(message) {
      write("out", "page → map", message);
      mappia.send(message);
    }

    // page -> map, and wait for the answer: the query echoes the requestId back.
    function ask(operation, message) {
      var requestId = "request-" + (++lastId);
      return new Promise(function (resolve) {
        waiting[requestId] = resolve;
        send({ operation: operation, message: message, requestId: requestId });
      });
    }

    function refreshCities() {
      return ask("list_cities").then(function (answer) {
        var chosen = citySelect.value;
        citySelect.innerHTML = "";
        answer.message.forEach(function (city) {
          citySelect.add(new Option(city.name, city.name, false, city.name === chosen));
        });
        fromMap.textContent = answer.message.length + " cities on the map.";
      });
    }

    // map -> page: what the query posts with ExtjsUtils.QUERY.postMessage
    mappia.addOnMessageCallback(function (msg) {
      if (typeof msg === "string") { // the platform's own notices: "mappia__..."
        write("notice", "map → page", msg);
        if (msg === "mappia__queryApplied") {
          document.getElementById("status").textContent = "Connected: the map is ready";
          document.getElementById("status").className = "ready";
          [showButton, listButton, addButton].forEach(function (button) { button.disabled = false; });
          refreshCities();
        }
        return;
      }
      write("in", "map → page", msg);
      if (msg.requestId && waiting[msg.requestId]) { // the answer to an ask()
        waiting[msg.requestId](msg);
        delete waiting[msg.requestId];
      } else if (msg.operation === "city_clicked") {
        citySelect.value = msg.message.name;
        fromMap.textContent = "You clicked " + msg.message.name + " on the map: " +
          msg.message.population.toLocaleString("en") + " inhabitants.";
      } else if (msg.operation === "cities_in_view") {
        fromMap.textContent = msg.message.cities.length + " cities in view at zoom " + msg.message.zoom +
          ": " + msg.message.cities.join(", ") + ".";
      } else if (msg.operation === "city_shown") {
        fromMap.textContent = msg.message.found ? "The map is showing " + msg.message.name + "." : msg.message.name + " is not on the map.";
      } else if (msg.operation === "city_added") {
        fromMap.textContent = msg.message.name + " added: " + msg.message.count + " cities on the map.";
        refreshCities();
      }
    });

    // Your page's buttons
    showButton.addEventListener("click", function () {
      send({ operation: "show_city", message: { name: citySelect.value } });
    });
    listButton.addEventListener("click", refreshCities);
    addButton.addEventListener("click", function () {
      addButton.disabled = true; // once: a second Recife would be a second point
      send({ operation: "add_city", message: { name: "Recife", lon: -34.88, lat: -8.05, population: 1488920 } });
    });

    // The query lives next to this page; it is applied as soon as the map listens.
    mappia.addReadyCallback(function () {
      mappia.applyQueryFromUrl("cities-query.js");
    });
  }
</script>
</body>
</html>

Build it step by step

1. Embed a map and apply a query

applyQuery sends query text - the same text you would type in the editor - into the map. Called before the map has finished loading, it waits: MappiaIO queues it until the map answers the handshake.

<iframe id="mappia" src="https://maps.csr.ufmg.br/calculator/?lang=eng&options=scale"
        width="100%" height="600"></iframe>
<script src="https://maps.csr.ufmg.br/mappia_io.js"></script>
<script>
  // true: keep listening after the handshake (needed to receive messages later)
  var mappia = MappiaIO("mappia", true);
  mappia.applyQuery('[{ name: "CSR:estados", title: "States", visibility: true }]');
</script>

The query does not have to be saved on the server: it can be built by your page, kept in your own repository, or fetched - mappia.applyQueryFromUrl("/queries/states.js") fetches the text and applies it.

2. One map, many queries

The same connection can apply one query after another, and every apply replaces what the map shows. The map confirms each one with the message mappia__queryApplied, which is how the page knows the new layers are in place. Every “Run it live” button of this documentation works this way.

<button data-query="states">States</button>
<button data-query="elevation">Elevation</button>
<span id="status"></span>
<script>
  var queries = {
    states: '[{ name: "CSR:estados", title: "States", visibility: true }]',
    elevation: '[{ name: "CSR:altimetria", title: "Elevation", opacity: 0.8, visibility: true }]'
  };
  var mappia = MappiaIO("mappia", true);
  mappia.addOnMessageCallback(function (msg) {
    if (msg === "mappia__queryApplied") {
      document.getElementById("status").textContent = "Map updated";
    }
  });
  document.querySelectorAll("button[data-query]").forEach(function (button) {
    button.addEventListener("click", function () {
      document.getElementById("status").textContent = "Loading...";
      mappia.applyQuery(queries[button.getAttribute("data-query")]);
    });
  });
</script>

3. Your page’s buttons drive the map

A button, a list, a form field of your page sends an object with send. Its shape is yours to choose; by convention it names an operation and carries a message:

<select id="city"><option>Belo Horizonte</option><option>Manaus</option></select>
<button id="show">Show on the map</button>
<script>
  document.getElementById("show").addEventListener("click", function () {
    mappia.send({ operation: "show_city", message: { name: document.getElementById("city").value } });
  });
</script>

Inside the map, the query registers the function that receives them with ExtjsUtils.QUERY.setMappiaIoCallback. The function has to be reachable by name, so it is declared as a query global (the complete query is in the playground above - pick MappiaIO):

ExtjsUtils.QUERY.setQueryGlobalProperties({
  citiesLayer: null, // set by the layer's onAdded
  onPageMessage: function (msg) {
    var layer = window.citiesLayer;
    if (!msg || !msg.operation || !layer) return;
    if (msg.operation === "show_city") {
      var city = layer.features.filter(function (f) { return f.attributes.name === msg.message.name; })[0];
      if (city) layer.map.setCenter(city.geometry.getBounds().getCenterLonLat(), 7);
      ExtjsUtils.QUERY.postMessage({ operation: "city_shown", message: { name: msg.message.name, found: !!city } });
    }
  }
}) && ExtjsUtils.QUERY.setMappiaIoCallback(window.onPageMessage) && [
  { title: "Cities", name: "cities", source: "file", type: "json", fromProj: "EPSG:4326", json: { /* ... */ },
    onAdded: function (event) { window.citiesLayer = (event && event.layer) || this; } }
]

The page can also drive the map’s own widgets: a message that moves a slider recalculates the layer exactly as a drag would, so a field of your page can set a threshold on the map.

// map side, in onPageMessage
if (msg.operation === "set_threshold") {
  Ext.getCmp("threshold").setValue(0, msg.message.value, false, true); // the slider of the layer panel
}

4. The map’s buttons and clicks report to your page

ExtjsUtils.QUERY.postMessage(object) goes the other way. Anything in the query can call it: a click on a feature, a hover, a drawing finished, a button of the layer panel.

// map side: a button in the layer panel, and a click on a city
descriptionHtml: "{{button|id=send_view|text=Send the cities in view to the page|handler=sendViewToPage}}",
onClick: function (feature) {
  ExtjsUtils.QUERY.postMessage({ operation: "city_clicked", message: feature.attributes });
},
// ...and sendViewToPage, a query global:
sendViewToPage: function () {
  var view = window.citiesLayer.map.getExtent();
  var inView = window.citiesLayer.features.filter(function (f) {
    return view.containsLonLat(f.geometry.getBounds().getCenterLonLat());
  });
  ExtjsUtils.QUERY.postMessage({ operation: "cities_in_view",
    message: { cities: inView.map(function (f) { return f.attributes.name; }) } });
}
// page side: one listener for everything the map says
mappia.addOnMessageCallback(function (msg) {
  if (msg && msg.operation === "city_clicked") {
    document.getElementById("details").textContent =
      msg.message.name + ": " + msg.message.population + " inhabitants";
  } else if (msg && msg.operation === "cities_in_view") {
    document.getElementById("details").textContent = msg.message.cities.join(", ");
  }
});

5. Ask and wait for the answer

Messages are one-way. For a question with an answer, send an id and have the query echo it back, then match the two in the page:

var waiting = {}, lastId = 0;
mappia.addOnMessageCallback(function (msg) {
  if (msg && msg.requestId && waiting[msg.requestId]) {
    waiting[msg.requestId](msg);
    delete waiting[msg.requestId];
  }
});
function ask(operation, message) {
  var requestId = "request-" + (++lastId);
  return new Promise(function (resolve) {
    waiting[requestId] = resolve;
    mappia.send({ operation: operation, message: message, requestId: requestId });
  });
}

ask("list_cities").then(function (answer) {
  console.log(answer.message.length + " cities on the map", answer.message);
});
// map side: answer with the same requestId
ExtjsUtils.QUERY.postMessage({ operation: "cities", requestId: msg.requestId, message: names });

6. Start in the right order

A page that sends its starting state - the filters of the URL, the record being edited - needs the map listening and the query applied first. addReadyCallback runs once the map has answered the handshake; apply the query there, then send the state:

var mappia = MappiaIO("mappia", true);
mappia.addOnMessageCallback(onMapMessage);
mappia.addReadyCallback(function () {
  mappia.applyQueryFromUrl("/js/my-map-query.js").then(function () {
    mappia.send({ operation: "setFilters", message: currentFilters });
    if (sharedView) mappia.send({ operation: "flyTo", message: sharedView }); // a shared link reopens the same view
  });
});

Messages sent right after applyQuery reach the new query: it registers its listener while it is applied, and the platform holds the messages until then.

7. Let the map report on its own

The map does not have to wait to be asked. A long job reports its progress, a pan reports what is now in view, a request to your server reports that it started and ended - the page keeps its own list, counters and spinners in step:

// map side: progress of a download, tagged with the request it belongs to
ExtjsUtils.OFFLINE.downloadArea({
  name: msg.message.name, extent: msg.message.extent,
  onProgress: function (progress) {
    ExtjsUtils.QUERY.postMessage({ operation: "download.progress", requestId: msg.requestId, message: progress });
  }
}).then(function (area) {
  ExtjsUtils.QUERY.postMessage({ operation: "download.result", requestId: msg.requestId, message: area });
});
// page side: progress updates the bar; the result settles the request
mappia.addOnMessageCallback(function (msg) {
  if (msg && msg.operation === "download.progress") {
    progressBar.value = msg.message.done / msg.message.total;
  }
});

8. Send a file

A file the user picked in your page can go to the map as it is - for the query to read, import or upload:

fileInput.addEventListener("change", function () {
  mappia.send({ operation: "importFromFile", message: { file: fileInput.files[0], name: "My area" } });
});

A message that carries a File or Blob is not turned into JSON: the browser copies the file along with it. This needs the next release of mappia_io.js; the version on maps.csr.ufmg.br today still turns a file into {}.

In production

MappiaIO is how Mappia maps are put to work inside other products. Three of them:

LarBH: a list of listings with prices on the left, a map of Belo Horizonte with clusters of listings on the right

LarBH - real-estate search in Belo Horizonte

The whole search is a page around a Mappia map. Its query starts with no layers at all: it draws the listings from code, coloured by price, and the page and the map keep each other in step - the map understands 17 operations from the page and sends 11 back. The count "111.197 imóveis na área", the listing column and the neighbourhood list are built from what the map reports after every pan and zoom. A listing's detail has a second, small map with the places nearby: one page, two maps, a connection each.

Page → mapMap → page
setFilters - the page's filtersviewportData - the listings, counts and neighbourhoods in view
flyTo, flyToExtent - the search box, a shared linkpinClicked - opens the listing in the page
highlightListing - the list row under the pointerhoverCluster - marks the same listings in the list
showNearbyPois, selectPoiCategory - places near a listing, on its small mapopenGroup, groupClusterDeselected - a cluster's listings
setEscala - the price colour scalefetchStart, fetchEnd - the page's loading bar
setLastVisit, markLastViewed - what is new since the last visitmapPointerDown - closes the page's address suggestions
// how it starts (simplified)
var mappia = new MappiaIO("mappia-frame", true);
mappia.addOnMessageCallback(onTenantMessage);
mappia.addReadyCallback(function () {
  // the query lives in the site's own code
  fetch("/js/imoveis-tenant-query.js")
    .then(function (r) { return r.text(); })
    .then(function (text) {
      mappia.applyQuery(text);
      mappia.send({ operation: "setEscala", message: { escala: priceScale } });
      mappia.send({ operation: "setFilters", message: filters });
    });
});
The offline areas page: a panel with the service worker status, a live feed of tiles and the map's layers on the left, the embedded map with a property boundary over satellite imagery on the right

Offline map areas - a whole tool outside the platform

Choosing which layers and which area to download for use without a network, estimating the size, downloading with progress, switching areas on and off, importing a zip: a complete tool, built as an ordinary page around the map (MappiaExplorer's examples/offline-areas-demo). The page holds every control; the map does the work with ExtjsUtils.OFFLINE and answers - 23 operations, each a question with a requestId.

  • The layer list is the map's answer to offlineAreas.listAvailableLayers; the area is offlineAreas.getImovelView - the property's own boundary.
  • offlineAreas.download answers many times: .progress while it runs, then .result or .error, all with the request's id.
  • The tile feed under the status is pushed by the map without being asked (offlineAreas.mapCacheDebug): how each tile was served, as it happens.
  • Importing sends the zip the user picked, as a file (offlineAreas.importFromFile).

This documentation

Every Run it live button of these pages and the playground above load the calculator in an iframe and send the example with applyQuery, waiting for mappia__queryApplied before saying the map is ready (assets/js/mappia-live.js of this site).

Things to know

  • Pass true as the second argument whenever the page keeps talking to the map. With false MappiaIO stops listening after the handshake, and nothing from the map arrives.
  • One listener, many operations. Register one addOnMessageCallback and branch on operation, as the complete page does; each registered function receives every message.
  • The page also receives the platform’s own notices, as plain strings starting with mappia__: mappia__queryApplied after every applyQuery, mappia__confirmQueryListening when a query registers its callback. Use the first; skip the rest. Never send them yourself.
  • applyQuery returns no promise. Wait for mappia__queryApplied to know the layers are in place (applyQueryFromUrl returns one, but it settles when the text was sent).
  • A query’s message callback outlives it. When a later query registers no callback of its own, messages still reach the previous one - so a handler should check that what it works on still exists, as onPageMessage does with citiesLayer above.
  • noopener and noreferrer break the connection, on the iframe or in window.open. MappiaIO works with a separate window too: MappiaIO(window.open(url), true).
  • Drop the connection with the map: when your page removes the iframe (a closed panel, a route change), call remove() so the old connection stops receiving messages.

Reference: the parameters used here

Generated from the platform source. Every entry, searchable, is in the API reference; the raw data is api.json. Open an entry for its description, parameters and example; # links to it.

MappiaIO · your page and the map

MappiaIO10 entries
Connects your page to an embedded Mappia map: apply queries, send messages, receive the map's.
Connects your page to a Mappia map embedded in it, in both directions. Your page loads https://maps.csr.ufmg.br/mappia_io.js, connects with MappiaIO(iframe, true), loads queries with applyQuery, sends objects with send and receives with addOnMessageCallback. Inside the map, the query receives with ExtjsUtils.QUERY.setMappiaIoCallback and answers with ExtjsUtils.QUERY.postMessage. Your page's buttons, lists and forms can drive the map (filter, zoom, highlight, edit a record), and the map tells the page what the user clicked, hovered or drew.

MappiaIO

function(communicationTarget, keepAfterReady) : Object# Connects your page to a Mappia map in an iframe (or in a window you opened) and returns the connection: applyQuery loads a query into the map, send delivers an object to the query running there, …

Connects your page to a Mappia map in an iframe (or in a window you opened) and returns the connection: applyQuery loads a query into the map, send delivers an object to the query running there, addOnMessageCallback receives what the query sends back with ExtjsUtils.QUERY.postMessage. Load the library from the map server (<script src="https://maps.csr.ufmg.br/mappia_io.js">).

On creation it greets the map (mappia__checkDOM) until the map answers it is listening (mappia__confirmDOM); everything sent before that is queued and delivered in order, so applyQuery and send may be called right away. Messages travel through window.postMessage wrapped as {mappia_iframe: <JSON text>}, so the page and the map may be on different sites.

Do not open the map with noopener or noreferrer (on the iframe or in window.open): they cut the window link the messages travel on. window.open(url, "_blank", "width=1280,height=860") works; window.open(url, "_blank", "noopener,noreferrer") does not.

communicationTarget String|HTMLIFrameElement|Window
The map to talk to: the id of its iframe, the iframe element itself, or the window returned by window.open.
keepAfterReady Boolean
true keeps listening after the handshake - needed by any page that receives messages from the map or applies more than one query. With false the connection stops listening once the map has answered.

Returns The connection, whose methods return it again so calls can be chained.

<iframe id="mappia" src="https://maps.csr.ufmg.br/calculator/?lang=eng&options=scale"></iframe>
<script src="https://maps.csr.ufmg.br/mappia_io.js"></script>
<script>
  var mappia = MappiaIO("mappia", true);
  mappia.addOnMessageCallback(function (msg) { console.log("from the map:", msg); });
  mappia.applyQuery('[{ name: "CSR:estados", title: "States", visibility: true }]');
  mappia.send({ operation: "hello", message: "from the page" });
</script>

addOnMessageCallback

function(callback) : Object# Registers a function that receives every message from the map.

Registers a function that receives every message from the map. What the query posts with ExtjsUtils.QUERY.postMessage(object) arrives as that object; the platform's own notices arrive as strings starting with mappia__ - mappia__queryApplied once a query sent with applyQuery is in place, mappia__confirmQueryListening when a query registers its callback. Several functions may be registered; each gets every message, about 150 ms after it was posted. Needs a connection created with keepAfterReady true.

callback function
function (message), called for each message.

Returns The connection.

mappia.addOnMessageCallback(function (msg) {
  if (msg === "mappia__queryApplied") return showStatus("Map ready");
  if (msg && msg.operation === "city_clicked") showDetails(msg.message);
});

addOnRemoveCallback

function(callback) : Object# Registers a function run when remove stops the connection, to tidy up whatever the page attached to it.

Registers a function run when remove stops the connection, to tidy up whatever the page attached to it.

callback function
Called with no arguments.

Returns The connection.

addReadyCallback

function(callback) : Object# Runs callback once the map has answered the handshake - at once when it already has.

Runs callback once the map has answered the handshake - at once when it already has. The place to start a conversation that needs the map listening: apply the first query there, then send the page's starting state (filters, the record being edited...).

callback function
Called with no arguments.

Returns The connection.

mappia.addReadyCallback(function () {
  fetch("/my-map-query.js").then(function (r) { return r.text(); }).then(function (text) {
    mappia.applyQuery(text);
    mappia.send({ operation: "setFilters", message: currentFilters });
  });
});

applyQuery

function(queryContent) : Object# Loads a query into the map - the same text you would type in the Mappia editor, of any length and not saved on the server - replacing the layers the map shows.

Loads a query into the map - the same text you would type in the Mappia editor, of any length and not saved on the server - replacing the layers the map shows. The map confirms with the message mappia__queryApplied once the new layers are in place. A query that did not run (a syntax or runtime error, or a value that is not a list of layers) is confirmed too, but the message {action: "mappia__queryError", error: {name, message, line, column}} comes first. One connection can apply query after query; called before the handshake, it waits in the queue.

queryContent String
The query text.

Returns The connection (not a promise: listen for mappia__queryApplied).

mappia.addOnMessageCallback(function (msg) {
  if (msg && msg.action === "mappia__queryError") console.warn("The query did not run:", msg.error.message, "line", msg.error.line);
  else if (msg === "mappia__queryApplied") console.log("applied");
});
mappia.applyQuery('[{ name: "CSR:altimetria", title: "Elevation", visibility: true }]');

applyQueryFromUrl

function(queryUrl) : Promise.<Object># Fetches the query text at queryUrl and applies it as applyQuery does - for a query kept as a file in your own site or repository.

Fetches the query text at queryUrl and applies it as applyQuery does - for a query kept as a file in your own site or repository. The URL is read with fetch, so one on another site must allow cross-origin reads.

queryUrl String
Address of a file holding query text, such as [{ name: "CSR:estados" }].

Returns Resolves with the connection once the text was fetched and sent - not when the map applied it: wait for the mappia__queryApplied message for that.

mappia.applyQueryFromUrl("/queries/my-map.js");

isDomLoaded

function() : Boolean# Whether the map has answered the handshake.

Whether the map has answered the handshake. Until it has, send and applyQuery queue their messages and deliver them, in order, as soon as it answers.

Returns true once the map is listening.

postMessage

function(jsStr)helper# Sends a message to the Mappia/iframe, allowing communication between the Mappia/iframe and the parent window.

Written as ExtjsUtils.QUERY.postMessage

Sends a message to the Mappia/iframe, allowing communication between the Mappia/iframe and the parent window. (Compatible with MappiaIO library) The 'QUERY.setMappiaIoCallback' is responsible to interpret the sent message.

jsStr Object|String
Object to be sent from inside Mappia/iframe to parent window. Objects are JSON-stringified; the parent receives { mappia_iframe: jsStr }.
ExtjsUtils.QUERY.postMessage({a:1,b:2})

remove

function() : Object# Stops listening to the map and runs the functions registered with addOnRemoveCallback.

Stops listening to the map and runs the functions registered with addOnRemoveCallback. A connection created with keepAfterReady false calls it by itself right after the handshake; call it yourself when the page drops the map (a closed panel, a route change) so an old connection does not keep receiving messages.

Returns The connection.

send

function(message, forceSend) : Object# Delivers message to the query running in the map, which receives it in the function it registered with ExtjsUtils.QUERY.setMappiaIoCallback.

Delivers message to the query running in the map, which receives it in the function it registered with ExtjsUtils.QUERY.setMappiaIoCallback. The shape is yours to choose; by convention an object naming an operation and carrying a message, plus a requestId when the page waits for an answer. Objects travel as JSON (functions and dates do not survive); a message carrying a File or Blob ({operation: "import", file: file}) is sent as the object itself, so the file reaches the query intact. Never send strings starting with mappia__: the platform reserves them.

message Object|String
What to deliver to the query.
forceSend Boolean
Sends at once even before the handshake, skipping the queue. Pages normally leave it out.

Returns The connection.

mappia.send({ operation: "show_city", message: { name: "Belo Horizonte" } });
mappia.send({ operation: "list_cities", requestId: "r1" }); // the query answers with the same requestId

QUERY · setup calls and the running query

QUERY3 entries
ExtjsUtils.QUERY: calls before the list (setQueryGlobalProperties, addRemoteWMSServer, decorate, setMappiaIoCallback) and changes to the running query from your functions (addLayer, removeLayer, postMessage).
View the complete Query API here.

getMappiaMessage

function(wrappedJsMsg) : Object|String|nullhelper# Internal — extracts the Mappia payload from a raw window "message" event: returns event.data.mappia_iframe, JSON-parsed when possible (a plain string is returned as is), or null when the event …

Written as ExtjsUtils.QUERY.getMappiaMessage

Internal — extracts the Mappia payload from a raw window "message" event: returns event.data.mappia_iframe, JSON-parsed when possible (a plain string is returned as is), or null when the event does not carry a mappia_iframe field (i.e. it was not sent through MappiaIO / QUERY.postMessage). Used by queryState.initMessageHandler; tenant code normally does not call it.

wrappedJsMsg MessageEvent
The DOM message event received on window.

Returns The parsed payload, or null when the event is not a Mappia message.

window.addEventListener("message", function(evt) {
  var msg = ExtjsUtils.QUERY.getMappiaMessage(evt);
  if (msg) console.log("Mappia message", msg);
});

setMappiaIoCallback

function(onMsgCallback) : Booleanhelper# Registers the function that receives the messages the parent window (embedding site or the MappiaIO library) sends to this Mappia page/iframe with postMessage({mappia_iframe: ...}).

Written as ExtjsUtils.QUERY.setMappiaIoCallback

Registers the function that receives the messages the parent window (embedding site or the MappiaIO library) sends to this Mappia page/iframe with postMessage({mappia_iframe: ...}). The callback gets the payload already unwrapped and JSON-parsed (an object or a string); protocol messages are filtered out, and messages received while a query is loading are queued and delivered afterwards. Registering posts CONFIRM_QUERY_LISTENING back to the parent, so the host knows it can start sending. A string with a function body is accepted as the callback. Use MappiaIO.postMessage to answer the parent.

onMsgCallback function
Callback function(jsStr) receiving the payload (Object|String) sent by the parent window.

Returns Always true, so the call can be chained with && QUERY_DESCRIPTION.

ExtjsUtils.QUERY.setMappiaIoCallback(function(msg) {
  if (msg && msg.type === "geojson") {
    ExtjsUtils.QUERY.addLayer({ name: "Received", source: "file", type: "json", json: msg.geojson, visibility: true });
    ExtjsUtils.QUERY.postMessage({ type: "loaded" });
  }
}) && QUERY_DESCRIPTION

setQueryGlobalProperties

function(globalProperties) : Booleanhelper# Defines globals for the query: every key of globalProperties becomes a window property (a value, an object or a function) that layer definitions, markup widgets (handler=, onMark=...) and …

Written as ExtjsUtils.QUERY.setQueryGlobalProperties

Defines globals for the query: every key of globalProperties becomes a window property (a value, an object or a function) that layer definitions, markup widgets (handler=, onMark=...) and other query code can reference by name. The names are recorded and the globals are deleted when another query loads. A key that already exists on window and was not created by the query is refused with "Global variable can't be redefined" in the console (the platform's own globals are protected; redefining one of the query's own keys is fine). runNow is the only key the platform itself invokes: right after the globals are registered QUERY.runNow is called once (see that entry). Chain it with && before the layer array so the globals exist when the layers are evaluated; QUERY_DESCRIPTION in the examples stands for that array.

globalProperties Object
Object whose keys become globals; each value may be a value, an object or a function.

Returns Always true, so the call can be chained with && QUERY_DESCRIPTION.

ExtjsUtils.QUERY.setQueryGlobalProperties({
  globalCount: 0,
  onLayerButton: function(btn) { console.log("clicked", btn); },
  runNow: function() { ExtjsUtils.ZOOM.limitZoomLevel(17); }
}) && [
  { name: "CSR:estados", visibility: true, descriptionHtml: "{{button|id=b1|text=Go|handler=onLayerButton}}" }
]