Embeddable Map - API Documentation

This document provides comprehensive API documentation for embedding and interacting with the map application.

Overview

There are two primary ways to use the embeddable map:

Method Use Case Data Size Limit Dynamic Updates
URL Parameters Simple static embeds ~2000 chars (browser URL limit) No
Iframe Message API Dynamic, complex embeds Unlimited Yes

URL Parameters

Configure the map directly via URL parameters when loading the iframe or visiting the page directly.

Parameter Reference

Parameter Format Required Description
geojson URL-encoded GeoJSON No A valid GeoJSON Feature or FeatureCollection to render
zoom Integer (0-19) No Initial zoom level. Default: 13
center lat,lon (WGS84) No Initial map center coordinates. Default: 51.505, -0.09 (London)
bbox minLon,minLat,maxLon,maxLat No Bounding box to fit map to
basemap String No Base map layer provider. Default: osm
highlight String No Feature ID to highlight (from the GeoJSON)

Available Basemaps

ID Description
osm OpenStreetMap (default)
satellite Satellite imagery
terrain Terrain map
dark Dark theme map

Parameter Precedence

  1. If bbox is provided, map fits to bounding box (overrides zoom and center)
  2. If center and/or zoom provided, use those
  3. Otherwise, if GeoJSON has bounds, fit to GeoJSON bounds
  4. Otherwise, use default center (51.505, -0.09) and zoom (13)

URL Parameter Examples

Basic Usage

https://yourdomain.com/map/?geojson=<URL-encoded-GeoJSON>&zoom=14&center=52.52,13.405

Example with Point Feature

https://yourdomain.com/map/?geojson=%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B13.405%2C52.52%5D%7D%2C%22properties%22%3A%7B%22name%22%3A%22Berlin%22%7D%7D&zoom=12

Decodes to:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [13.405, 52.52]
  },
  "properties": {
    "name": "Berlin"
  }
}

Example with Bounding Box

Fit the map to a specific bounding box covering Berlin:

https://yourdomain.com/map/?geojson=...&bbox=13.0,52.3,13.8,52.7

Example with Custom Basemap

Use the satellite basemap:

https://yourdomain.com/map/?geojson=...&basemap=satellite

Example with Feature Highlighting

Highlight a feature with ID "park-001":

https://yourdomain.com/map/?geojson=...&highlight=park-001

Example with All Parameters

https://yourdomain.com/map/?geojson=%7B...%7D&zoom=14&center=52.52,13.405&basemap=terrain&highlight=berlin

Iframe Embedding

Basic Iframe Embed

<iframe
  src="https://yourdomain.com/map/?geojson=<URL-encoded-GeoJSON>&zoom=14"
  width="600"
  height="400"
  frameborder="0"
  allowfullscreen
></iframe>

Encoding GeoJSON for URL

Use JavaScript's encodeURIComponent to properly encode GeoJSON:

const geojson = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      id: "berlin",
      geometry: {
        type: "Point",
        coordinates: [13.405, 52.52]
      },
      properties: {
        name: "Berlin",
        description: "Capital of Germany"
      }
    }
  ]
};

const geojsonUrlParam = encodeURIComponent(JSON.stringify(geojson));

Then use in the iframe:

<iframe
  src="https://yourdomain.com/map/?geojson={{geojsonUrlParam}}&zoom=14"
  width="600"
  height="400"
  frameborder="0"
></iframe>

Responsive Iframe Embed

<div class="map-container">
  <iframe
    src="https://yourdomain.com/map/?geojson=..."
    frameborder="0"
    allowfullscreen
  ></iframe>
</div>

<style>
  .map-container {
    position: relative;
    padding-bottom: 60%; /* Aspect ratio (e.g., 16:9 would be 56.25%) */
    height: 0;
    overflow: hidden;
  }
  .map-container iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
</style>

Iframe Message API

For large GeoJSON data or dynamic updates, use the postMessage API instead of URL parameters.

Connection Setup

<iframe
  id="mapFrame"
  src="https://yourdomain.com/map/"
  width="600"
  height="400"
  frameborder="0"
></iframe>

<script>
  const mapFrame = document.getElementById('mapFrame');
  
  // Wait for map to be ready
  window.addEventListener('message', (event) => {
    // IMPORTANT: Verify origin in production!
    // if (event.origin !== 'https://yourdomain.com') return;
    
    if (event.data.type === 'map:ready') {
      console.log('Map is ready:', event.data.payload);
      // Map is now ready to receive messages
    }
  });
</script>

Messages from Parent to Iframe

Send messages to the map iframe using postMessage:

// Get reference to iframe
const mapFrame = document.getElementById('mapFrame');

// Send to the iframe's contentWindow
mapFrame.contentWindow.postMessage(message, targetOrigin);
Message Type Payload Description
map:updateGeoJson { geojson: FeatureCollection | Feature | Geometry } Update the GeoJSON data displayed on the map
map:setView { center: [lat, lng], zoom: number } Change map view (center and zoom)
map:fitBounds { bounds: [[minLng, minLat], [maxLng, maxLat]] } Fit map to bounding box
map:highlightFeature { featureId: string } Highlight a specific feature by ID
map:getFeatureInfo { featureId: string } Request information about a specific feature
map:clear {} Clear all features from the map

Example: Update GeoJSON

mapFrame.contentWindow.postMessage({
  type: 'map:updateGeoJson',
  payload: {
    geojson: {
      type: "FeatureCollection",
      features: [/* ... */]
    }
  }
}, '*');

Example: Set View

mapFrame.contentWindow.postMessage({
  type: 'map:setView',
  payload: {
    center: [52.52, 13.405],
    zoom: 14
  }
}, '*');

Example: Fit to Bounds

mapFrame.contentWindow.postMessage({
  type: 'map:fitBounds',
  payload: {
    bounds: [[13.0, 52.3], [13.8, 52.7]]
  }
}, '*');

Example: Highlight Feature

mapFrame.contentWindow.postMessage({
  type: 'map:highlightFeature',
  payload: {
    featureId: "berlin"
  }
}, '*');

Example: Request Feature Info

mapFrame.contentWindow.postMessage({
  type: 'map:getFeatureInfo',
  payload: {
    featureId: "berlin"
  }
}, '*');

Example: Clear Map

mapFrame.contentWindow.postMessage({
  type: 'map:clear',
  payload: {}
}, '*');

Messages from Iframe to Parent

Listen for messages from the map iframe:

window.addEventListener('message', (event) => {
  // Verify origin in production!
  // if (event.origin !== 'https://yourdomain.com') return;
  
  const data = event.data;
  
  switch (data.type) {
    case 'map:ready':
      // Map has initialized
      break;
    case 'map:featureClicked':
      // A feature was clicked
      break;
    case 'map:viewportChanged':
      // Map viewport changed
      break;
    case 'map:featureInfo':
      // Response to getFeatureInfo request
      break;
    case 'map:error':
      // An error occurred
      break;
  }
});
Message Type Payload Description
map:ready { version: string, leafletVersion: string } Map has initialized and is ready to receive messages
map:featureClicked { id?: string, featureId?: string, properties: Record<string, any>, geometry: Geometry, coordinates: [lng, lat] } A feature was clicked on the map
map:viewportChanged { center: [lng, lat], zoom: number, bounds: [[minLng, minLat], [maxLng, maxLat]] } Map viewport changed (panning or zooming)
map:featureInfo { featureId: string, found: boolean, feature?: Feature, error?: string } Response to getFeatureInfo request
map:error { message: string, code: string, details?: any } Error occurred

Example: Handle Feature Clicks

window.addEventListener('message', (event) => {
  if (event.data.type === 'map:featureClicked') {
    const { id, featureId, properties, coordinates } = event.data.payload;
    console.log(`Feature ${featureId || id} clicked:`);
    console.log(`  Name: ${properties.name || 'Unknown'}`);
    console.log(`  Coordinates: [${coordinates.join(', ')}]`);
  }
});

Example: Handle Viewport Changes

window.addEventListener('message', (event) => {
  if (event.data.type === 'map:viewportChanged') {
    const { center, zoom, bounds } = event.data.payload;
    console.log(`Map moved to [${center.join(', ')}] at zoom ${zoom}`);
    console.log(`Bounds: ${bounds[0].join(', ')} to ${bounds[1].join(', ')}`);
  }
});

Example: Handle Feature Info Response

window.addEventListener('message', (event) => {
  if (event.data.type === 'map:featureInfo') {
    const { featureId, found, feature, error } = event.data.payload;
    if (found) {
      console.log(`Feature ${featureId}:`, feature);
    } else {
      console.log(`Feature ${featureId} not found:`, error);
    }
  }
});

Example: Handle Errors

window.addEventListener('message', (event) => {
  if (event.data.type === 'map:error') {
    const { message, code, details } = event.data.payload;
    console.error(`Map error [${code}]: ${message}`, details);
  }
});

TypeScript Types

For TypeScript users embedding the map:

// GeoJSON types (from @types/geojson)
import type { Feature, FeatureCollection, Geometry } from 'geojson';

// Messages from Parent to Iframe
type ParentToIframeMessage =
  | { type: 'map:updateGeoJson'; payload: { geojson: FeatureCollection | Feature | Geometry } }
  | { type: 'map:setView'; payload: { center: [number, number]; zoom: number } }
  | { type: 'map:fitBounds'; payload: { bounds: [[number, number], [number, number]] } }
  | { type: 'map:highlightFeature'; payload: { featureId: string } }
  | { type: 'map:getFeatureInfo'; payload: { featureId: string } }
  | { type: 'map:clear'; payload: {} };

// Messages from Iframe to Parent
type IframeToParentMessage =
  | { type: 'map:ready'; payload: { version: string; leafletVersion: string } }
  | { type: 'map:featureClicked'; payload: { id?: string; featureId?: string; properties: Record<string, any>; geometry: Geometry; coordinates: [number, number] } }
  | { type: 'map:viewportChanged'; payload: { center: [number, number]; zoom: number; bounds: [[number, number], [number, number]] } }
  | { type: 'map:featureInfo'; payload: { featureId: string; found: boolean; feature?: Feature; error?: string } }
  | { type: 'map:error'; payload: { message: string; code: 'INVALID_GEOJSON' | 'MISSING_PARAMETER' | 'RENDER_ERROR' | 'UNKNOWN'; details?: any } };

// Combined type for message handling
type MapMessage = ParentToIframeMessage | IframeToParentMessage;

// Example usage with type checking
const mapFrame = document.getElementById('mapFrame') as HTMLIFrameElement;

window.addEventListener('message', (event: MessageEvent<MapMessage>) => {
  switch (event.data.type) {
    case 'map:ready':
      // event.data.payload.version is string
      break;
    case 'map:featureClicked':
      // event.data.payload.properties is Record<string, any>
      break;
    case 'map:error':
      // event.data.payload.code is one of the known error codes
      break;
  }
});

// Sending messages with type safety
function sendMapMessage(frame: HTMLIFrameElement, message: ParentToIframeMessage): void {
  frame.contentWindow?.postMessage(message, '*');
}

sendMapMessage(mapFrame, {
  type: 'map:updateGeoJson',
  payload: { geojson: myGeoJson }
});

GeoJSON Support

The app supports all standard GeoJSON geometry types:

GeoJSON Type Leaflet Layer Description
Point CircleMarker Single point/location
MultiPoint Multiple CircleMarkers Multiple points
LineString Polyline Connected line segments
MultiLineString Multiple Polylines Multiple line strings
Polygon Polygon Closed shape with holes
MultiPolygon Multiple Polygons Multiple polygons
GeometryCollection LayerGroup Collection of geometries
Feature Depends on geometry type Geometry + properties
FeatureCollection LayerGroup Collection of Features

Feature Properties

GeoJSON Features can include properties that the app uses for rendering and interaction:

Property Type Usage
id String/Number Unique identifier for the feature (used for highlighting)
name String Display name (used in popups)
description String Additional info (used in popups)
stroke String CSS color for stroke (lines/polygons)
fill String CSS color for fill (polygons)
fillOpacity Number Fill opacity (0-1)
strokeWidth Number Stroke width in pixels
Any custom property Any Available in popups and click events

Example GeoJSON Feature

{
  "type": "Feature",
  "id": "park-001",
  "geometry": {
    "type": "Polygon",
    "coordinates": [[[13.3, 52.4], [13.5, 52.4], [13.5, 52.6], [13.3, 52.6], [13.3, 52.4]]]
  },
  "properties": {
    "name": "City Park",
    "description": "A beautiful urban park",
    "category": "recreation",
    "fill": "#4CAF50",
    "fillOpacity": 0.5,
    "stroke": "#2E7D32",
    "strokeWidth": 2
  }
}

Example FeatureCollection

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "berlin",
      "geometry": {
        "type": "Point",
        "coordinates": [13.405, 52.52]
      },
      "properties": {
        "name": "Berlin",
        "population": 3750000
      }
    },
    {
      "type": "Feature",
      "id": "paris",
      "geometry": {
        "type": "Point",
        "coordinates": [2.3522, 48.8566]
      },
      "properties": {
        "name": "Paris",
        "population": 2160000
      }
    }
  ]
}

Coordinate Order

Important: GeoJSON uses [longitude, latitude] order (x, y), which matches the standard.

{
  "type": "Point",
  "coordinates": [13.405, 52.52]  // [lng, lat] - Berlin
}

The app automatically converts to Leaflet's [lat, lng] format internally.


Complete Example: Interactive Map Embed

Here's a complete HTML page that demonstrates all the Message API features:

<!DOCTYPE html>
<html>
<head>
  <title>Map Embedding Example</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }
    h1, h2 {
      color: #333;
    }
    .map-container {
      position: relative;
      padding-bottom: 60%;
      height: 0;
      overflow: hidden;
      margin: 20px 0;
      border: 1px solid #ddd;
      border-radius: 8px;
    }
    .map-container iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
    .controls {
      display: flex;
      gap: 10px;
      margin: 20px 0;
      flex-wrap: wrap;
    }
    button {
      padding: 10px 16px;
      background: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    button:hover {
      background: #45a049;
    }
    #status {
      padding: 12px;
      background: #f8f9fa;
      border: 1px solid #dee2e6;
      border-radius: 4px;
      margin: 20px 0;
    }
    .feature-list {
      margin-top: 20px;
    }
    .feature-item {
      padding: 8px 12px;
      border: 1px solid #eee;
      margin: 4px 0;
      border-radius: 4px;
      cursor: pointer;
    }
    .feature-item:hover {
      background: #f5f5f5;
    }
  </style>
</head>
<body>
  <h1>Map Embedding Demo</h1>
  <p>This page demonstrates how to embed and interact with the map using the Message API.</p>
  
  <div class="map-container">
    <iframe id="mapFrame" src="https://yourdomain.com/map/" frameborder="0"></iframe>
  </div>
  
  <div id="status">Waiting for map to load...</div>
  
  <div class="controls">
    <button onclick="updateGeoJson()">Load Cities</button>
    <button onclick="fitToGermany()">Fit to Germany</button>
    <button onclick="highlightFeature('berlin')">Highlight Berlin</button>
    <button onclick="highlightFeature('munich')">Highlight Munich</button>
    <button onclick="clearMap()">Clear Map</button>
    <button onclick="setBasemap('satellite')">Satellite View</button>
    <button onclick="setBasemap('osm')">Street View</button>
  </div>
  
  <div class="feature-list" id="featureList"></div>

  <script>
    const mapFrame = document.getElementById('mapFrame');
    const statusEl = document.getElementById('status');
    const featureListEl = document.getElementById('featureList');
    
    let currentFeatures = [];
    
    // Listen for messages from the map
    window.addEventListener('message', (event) => {
      // In production, verify the origin!
      // if (event.origin !== 'https://yourdomain.com') return;
      
      const data = event.data;
      
      if (data.type === 'map:ready') {
        statusEl.textContent = `Map ready (v${data.payload.version}, Leaflet ${data.payload.leafletVersion})`;
        // Load initial data
        updateGeoJson();
      }
      
      if (data.type === 'map:featureClicked') {
        const { id, featureId, properties, coordinates } = data.payload;
        statusEl.textContent = `Clicked: ${properties.name || featureId || id} at [${coordinates.join(', ')}]`;
      }
      
      if (data.type === 'map:viewportChanged') {
        const { center, zoom, bounds } = data.payload;
        console.log('Viewport:', { center, zoom, bounds });
      }
      
      if (data.type === 'map:featureInfo') {
        const { featureId, found, feature, error } = data.payload;
        if (found) {
          console.log('Feature info:', feature);
        } else {
          console.log('Feature not found:', featureId, error);
        }
      }
      
      if (data.type === 'map:error') {
        statusEl.textContent = `Error: ${data.payload.message} (${data.payload.code})`;
        console.error('Map error:', data.payload);
      }
    });
    
    function sendMessage(type, payload) {
      mapFrame.contentWindow.postMessage({ type, payload }, '*');
    }
    
    function updateGeoJson() {
      currentFeatures = [
        {
          type: "Feature",
          id: "berlin",
          geometry: {
            type: "Point",
            coordinates: [13.405, 52.52]
          },
          properties: {
            name: "Berlin",
            description: "Capital of Germany",
            population: 3750000
          }
        },
        {
          type: "Feature",
          id: "munich",
          geometry: {
            type: "Point",
            coordinates: [11.582, 48.135]
          },
          properties: {
            name: "Munich",
            description: "Capital of Bavaria",
            population: 1500000
          }
        },
        {
          type: "Feature",
          id: "hamburg",
          geometry: {
            type: "Point",
            coordinates: [9.993, 53.551]
          },
          properties: {
            name: "Hamburg",
            description: "Port city in northern Germany",
            population: 1900000
          }
        }
      ];
      
      sendMessage('map:updateGeoJson', { 
        geojson: { type: "FeatureCollection", features: currentFeatures } 
      });
      
      // Update feature list
      featureListEl.innerHTML = '<h3>Features:</h3>' + 
        currentFeatures.map(f => 
          `<div class="feature-item" onclick="fitToFeature('${f.id}')">
            <strong>${f.properties.name}</strong> - ${f.properties.description}
          </div>`
        ).join('');
      
      statusEl.textContent = 'Loaded 3 German cities';
    }
    
    function fitToGermany() {
      sendMessage('map:fitBounds', { 
        bounds: [[5.8, 47.2], [15.1, 55.1]] 
      });
      statusEl.textContent = 'Fitted to Germany bounds';
    }
    
    function highlightFeature(featureId) {
      sendMessage('map:highlightFeature', { featureId });
      statusEl.textContent = `Highlighted: ${featureId}`;
    }
    
    function clearMap() {
      sendMessage('map:clear', {});
      statusEl.textContent = 'Map cleared';
    }
    
    function setBasemap(basemap) {
      // Note: basemap can also be set via URL parameter
      statusEl.textContent = `Changed basemap to: ${basemap}`;
      // In a real implementation, you might need to reload the iframe
      // or use a custom message type for basemap changes
    }
    
    function fitToFeature(featureId) {
      const feature = currentFeatures.find(f => f.id === featureId);
      if (feature) {
        const [lng, lat] = feature.geometry.coordinates;
        sendMessage('map:setView', { 
          center: [lat, lng], 
          zoom: 12 
        });
        highlightFeature(featureId);
      }
    }
  </script>
</body>
</html>

Tips and Best Practices

URL Parameter vs Message API

Aspect URL Parameters Message API
Simplicity ✅ Very simple ⚠️ Requires JavaScript
Data Size ❌ Limited by URL length (~2000 chars) ✅ Unlimited
Dynamic Updates ❌ No ✅ Yes
Complex Interactions ❌ No ✅ Yes
Caching ✅ Browser may cache ✅ No caching issues

Use URL parameters for:

Use Message API for:

Encoding Tips

Always use encodeURIComponent for URL parameters:

// Correct
const url = `https://map.example.com/?geojson=${encodeURIComponent(JSON.stringify(geojson))}`;

// Wrong (will break with special characters)
const url = `https://map.example.com/?geojson=${JSON.stringify(geojson)}`;

Feature Identification

Features are identified using this priority:

  1. feature.id (GeoJSON standard)
  2. feature.properties.featureId
  3. feature.properties.id

Ensure your features have unique IDs if you plan to use highlighting or feature info requests.

Origin Security

Important: In production, always verify the message origin:

window.addEventListener('message', (event) => {
  // Verify the origin matches your expected domain
  if (event.origin !== 'https://yourdomain.com') return;
  
  // Now it's safe to process the message
  if (event.data.type === 'map:ready') {
    // ...
  }
});

Coordinate System

The app automatically converts between these formats, so always use GeoJSON's [lng, lat] in your data.


Troubleshooting

Map doesn't load in iframe

Messages not received

Features not rendering

URL too long


See Also