Fantasy-Map-Generator/EXTERNAL_API_INTEGRATION.md
Claude 20458e39e2
Add external API integration for wiki/web UI synchronization
This commit adds a comprehensive external API system that allows external tools
(wikis, web UIs, etc.) to control and synchronize with Fantasy Map Generator.

New Features:
- External API Bridge (modules/external-api.js)
  - Event-driven architecture with EventEmitter
  - Map lifecycle control (create, load, save)
  - Data access methods (rivers, cultures, states, burgs)
  - Data mutation methods with auto-redraw
  - Export support (SVG, PNG, JSON)
  - Change detection with automatic event emission

- PostMessage Communication Layer
  - Auto-enables when FMG is embedded in iframe
  - Bidirectional message passing
  - Request/response pattern with promise support
  - Automatic event forwarding to parent window

- REST API Server (api-server/)
  - Express.js server with full CRUD operations
  - WebSocket support via Socket.IO for real-time updates
  - File upload support for map and CSV import
  - In-memory storage (can be replaced with database)
  - CORS enabled for cross-origin requests
  - Comprehensive endpoints for all map data

- Client Library (api-server/client.js)
  - Simple JavaScript client for REST API
  - Promise-based async methods
  - Works in browser and Node.js

- Demo Pages (demos/)
  - PostMessage integration demo with full UI
  - REST API demo with interactive testing
  - WebSocket demo for real-time events

- Documentation
  - Comprehensive integration guide (EXTERNAL_API_INTEGRATION.md)
  - API reference with TypeScript interfaces
  - Multiple integration examples
  - Troubleshooting guide

Integration Methods:
1. PostMessage Bridge - For iframe embedding
2. REST API - For server-side integration
3. Direct JavaScript API - For same-origin apps

Use Cases:
- Wiki pages that need to display and control maps
- Web UIs that want to edit map data
- External tools that need to sync with FMG
- Real-time collaborative map editing
- Batch operations and automation

Technical Details:
- Zero dependencies for external-api.js (pure JS)
- Auto-initializes on DOMContentLoaded
- Throttled change detection (500ms debounce)
- Deep cloning for data access (prevents mutations)
- Error handling throughout
- Version tagged (v1.0.0)

Updated Files:
- index.html: Added script tag to load external-api module

All APIs are backward compatible and don't affect existing functionality.
2025-11-04 21:43:06 +00:00

15 KiB

External API Integration Guide

This guide explains how to integrate Fantasy Map Generator (FMG) with external tools like wikis, web UIs, or other applications.

Table of Contents


Overview

The Fantasy Map Generator now includes an External API that allows external applications to:

  • Control map generation and loading
  • Access and modify map data (rivers, cultures, states, burgs, etc.)
  • Listen to real-time map changes
  • Export maps in various formats
  • Synchronize state bidirectionally

Key Features

Event-driven architecture - Subscribe to map changes PostMessage bridge - Embed FMG in iframe and control it REST API server - HTTP endpoints for server-side integration WebSocket support - Real-time bidirectional communication Type-safe data access - Clean API with error handling


Integration Methods

1. PostMessage Bridge (iframe)

Best for: Web-based wikis, browser extensions, web apps on different domains

How It Works

  1. Embed FMG in an <iframe>
  2. Send commands via postMessage()
  3. Receive responses and events automatically

Example

<!-- Your Wiki/Web UI -->
<!DOCTYPE html>
<html>
<body>
  <!-- Embed FMG -->
  <iframe id="mapFrame" src="https://your-fmg-instance.com/index.html"></iframe>

  <script>
    const mapFrame = document.getElementById('mapFrame').contentWindow;

    // Wait for iframe to load
    window.addEventListener('load', () => {
      // Create a new map
      mapFrame.postMessage({
        type: 'CREATE_MAP',
        payload: { seed: 'my-world' },
        requestId: 1
      }, '*');
    });

    // Listen for responses
    window.addEventListener('message', (event) => {
      const { type, payload, requestId } = event.data;

      if (type === 'RESPONSE' && requestId === 1) {
        console.log('Map created!', payload);
      }

      if (type === 'EVENT' && payload.event === 'rivers:updated') {
        console.log('Rivers updated:', payload.data);
      }
    });
  </script>
</body>
</html>

Available Commands

Send these via postMessage():

// Map Lifecycle
{ type: 'CREATE_MAP', payload: { seed, width, height } }
{ type: 'LOAD_MAP', payload: mapData }
{ type: 'SAVE_MAP', payload: { format: 'data' | 'blob' } }

// Data Access
{ type: 'GET_STATE' }
{ type: 'GET_RIVERS' }
{ type: 'GET_CULTURES' }
{ type: 'GET_STATES' }
{ type: 'GET_BURGS' }

// Mutations
{ type: 'UPDATE_RIVERS', payload: [...] }
{ type: 'UPDATE_CULTURES', payload: [...] }
{ type: 'UPDATE_STATES', payload: [...] }
{ type: 'ADD_BURG', payload: {name, x, y, ...} }

// Export
{ type: 'EXPORT_SVG' }
{ type: 'EXPORT_PNG', payload: {width, height} }
{ type: 'EXPORT_JSON', payload: {key} }

Demo

See demos/postmessage-demo.html for a full interactive example.


2. REST API Server

Best for: Server-side integration, microservices, backend systems

Setup

  1. Install dependencies:
cd api-server
npm install
  1. Start the server:
node server.js
# Server runs on http://localhost:3000

Endpoints

Health Check
GET /api/health
Create Map
POST /api/maps
Body: { seed?: string, width?: number, height?: number }
Response: { success: true, mapId: string, pollUrl: string }
Get Map
GET /api/maps/:id
Response: { success: true, map: {...} }
List Maps
GET /api/maps
Response: { success: true, maps: [...] }
Update Map
PUT /api/maps/:id
Body: { data: {...} }
Delete Map
DELETE /api/maps/:id
Get/Update Rivers
GET /api/maps/:id/rivers
PUT /api/maps/:id/rivers
Body: { rivers: [...] }
Import Rivers from CSV
POST /api/maps/:id/rivers/import
Body: multipart/form-data with 'file' field
Get Cultures/States/Burgs
GET /api/maps/:id/cultures
GET /api/maps/:id/states
GET /api/maps/:id/burgs
Add Burg
POST /api/maps/:id/burgs
Body: { name, x, y, cell, population, type }

Example Usage

// Create a new map
const response = await fetch('http://localhost:3000/api/maps', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ seed: 'my-world' })
});

const { mapId } = await response.json();

// Get rivers
const riversResponse = await fetch(`http://localhost:3000/api/maps/${mapId}/rivers`);
const { rivers } = await riversResponse.json();

console.log('Rivers:', rivers);

// Update rivers
await fetch(`http://localhost:3000/api/maps/${mapId}/rivers`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    rivers: [
      { i: 1, name: 'Mystic River', type: 'River', discharge: 100 }
    ]
  })
});

WebSocket Events

Connect to ws://localhost:3000 for real-time updates:

import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

socket.on('connect', () => {
  console.log('Connected!');
});

// Listen to events
socket.on('map:created', (data) => {
  console.log('Map created:', data);
});

socket.on('rivers:updated', (data) => {
  console.log('Rivers updated:', data);
});

// Send events
socket.emit('map:update', {
  mapId: 'map_123',
  updates: { /* ... */ }
});

Demo

See demos/rest-api-demo.html and demos/websocket-demo.html for interactive examples.


3. Direct JavaScript API

Best for: Same-origin applications, browser extensions with host permissions

Access the API

Once FMG is loaded, access the global API:

const api = window.FMG_API;

Methods

Map Lifecycle
// Create new map
const result = await api.createMap({ seed: 'my-seed' });
if (result.success) {
  console.log('Map created:', result.state);
}

// Load map from file or data
const file = document.getElementById('fileInput').files[0];
await api.loadMap(file);

// Or load from string
await api.loadMap(mapDataString);

// Save map
const saved = await api.saveMap('data'); // or 'blob'
console.log('Map data:', saved.data);
Data Access
// Get complete state
const state = api.getMapState();
console.log('Current state:', state);

// Get specific data
const rivers = api.getRivers();
const cultures = api.getCultures();
const states = api.getStates();
const burgs = api.getBurgs();
const religions = api.getReligions();
const markers = api.getMarkers();
const grid = api.getGrid();

// Get any data by key
const data = api.getData('rivers');
Mutations
// Update rivers
api.updateRivers([
  { i: 1, name: 'New River', type: 'River', discharge: 50 },
  // ... more rivers
]);

// Update cultures
api.updateCultures([...]);

// Update states
api.updateStates([...]);

// Update burgs
api.updateBurgs([...]);

// Add a new burg
const result = api.addBurg({
  name: 'New City',
  x: 500,
  y: 400,
  cell: 1234,
  population: 10,
  type: 'city',
  culture: 1,
  state: 1
});

if (result.success) {
  console.log('New burg ID:', result.id);
}
Export
// Export SVG
const svg = api.exportSVG();
console.log('SVG:', svg);

// Export PNG (returns blob)
const pngBlob = await api.exportPNG(2048, 2048);

// Export JSON
const json = api.exportJSON(); // All data
const riversJson = api.exportJSON('rivers'); // Specific key
Events
// Subscribe to events
const unsubscribe = api.on('map:changed', (state) => {
  console.log('Map changed:', state);
});

// Unsubscribe
unsubscribe();

// Or manually
api.off('map:changed', callback);

// Subscribe once
api.once('map:created', (state) => {
  console.log('Map created:', state);
});

// Emit custom events
api.emit('custom:event', { myData: 'test' });

API Reference

Data Structures

Map State

interface MapState {
  seed: string | null;
  mapId: string | null;
  timestamp: number;
  pack: {
    cultures: Culture[];
    states: State[];
    burgs: Burg[];
    rivers: River[];
    religions: Religion[];
    provinces: Province[];
    markers: Marker[];
  } | null;
  grid: {
    spacing: number;
    cellsX: number;
    cellsY: number;
    features: Feature[];
  } | null;
  options: object | null;
}

River

interface River {
  i: number;              // ID
  name: string;           // Name
  type: string;           // 'River', 'Lake', etc.
  discharge: number;      // m³/s
  length: number;         // Distance
  width: number;          // Visual width
  basin: number;          // Parent river ID
  // ... more properties
}

Culture

interface Culture {
  i: number;              // ID
  name: string;           // Name
  base: number;           // Name base ID
  shield: string;         // Shield type
  expansionism: number;   // Expansion rate
  color: string;          // Color
  // ... more properties
}

State

interface State {
  i: number;              // ID
  name: string;           // Name
  color: string;          // Color
  expansionism: number;   // Expansion rate
  capital: number;        // Capital burg ID
  culture: number;        // Culture ID
  // ... more properties
}

Burg

interface Burg {
  i: number;              // ID
  name: string;           // Name
  x: number;              // X coordinate
  y: number;              // Y coordinate
  cell: number;           // Cell ID
  population: number;     // Population
  type: string;           // 'city', 'town', etc.
  culture: number;        // Culture ID
  state: number;          // State ID
  // ... more properties
}

Events

Available Events

Map Events

  • map:created - New map created
  • map:loaded - Map loaded from file
  • map:changed - Map modified (throttled)

Data Events

  • rivers:updated - Rivers data updated
  • cultures:updated - Cultures data updated
  • states:updated - States data updated
  • burgs:updated - Burgs data updated
  • burg:added - New burg added

Event Payload

All events include relevant data:

api.on('rivers:updated', (rivers) => {
  console.log('Updated rivers:', rivers);
});

api.on('map:created', (state) => {
  console.log('Map state:', state);
});

Examples

Example 1: Wiki Integration

Embed FMG in a wiki page and sync data:

<div id="wiki-map-section">
  <iframe id="fmg" src="/fmg/index.html" width="100%" height="600"></iframe>

  <script>
    // Store map state in wiki
    window.addEventListener('message', (event) => {
      if (event.data.type === 'EVENT' && event.data.payload.event === 'map:changed') {
        // Save to wiki database
        saveToWiki(event.data.payload.data);
      }
    });

    // Load stored map on page load
    fetch('/wiki/api/map-data')
      .then(r => r.json())
      .then(mapData => {
        document.getElementById('fmg').contentWindow.postMessage({
          type: 'LOAD_MAP',
          payload: mapData
        }, '*');
      });
  </script>
</div>

Example 2: Custom River Editor

Create a custom UI to edit rivers:

// Get current rivers
const rivers = await window.FMG_API.getRivers();

// Show in custom UI
renderRiversTable(rivers);

// Update a river
rivers[0].name = 'Renamed River';
rivers[0].discharge = 150;

// Save changes
window.FMG_API.updateRivers(rivers);

// Listen for updates
window.FMG_API.on('rivers:updated', (updatedRivers) => {
  renderRiversTable(updatedRivers);
});

Example 3: Batch Operations via REST API

// Create multiple maps
async function createMapBatch(seeds) {
  const maps = [];

  for (const seed of seeds) {
    const res = await fetch('http://localhost:3000/api/maps', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ seed })
    });

    const { mapId } = await res.json();
    maps.push(mapId);
  }

  return maps;
}

// Use it
const mapIds = await createMapBatch(['world1', 'world2', 'world3']);
console.log('Created maps:', mapIds);

Example 4: Real-time Collaboration

Multiple users editing the same map:

// User A's browser
const socket = io('http://localhost:3000');

// Listen for changes from other users
socket.on('rivers:updated', (data) => {
  // Update local view
  window.FMG_API.updateRivers(data.rivers);
});

// When local user makes changes
window.FMG_API.on('rivers:updated', (rivers) => {
  // Broadcast to other users
  socket.emit('rivers:updated', {
    mapId: currentMapId,
    rivers
  });
});

Troubleshooting

PostMessage not working

Problem: Messages not received in iframe

Solution:

  1. Ensure iframe has loaded: iframe.addEventListener('load', ...)
  2. Wait 1-2 seconds after load for API to initialize
  3. Check origin in postMessage: use '*' or specific origin
  4. Open browser console to check for errors

CORS errors with REST API

Problem: Access-Control-Allow-Origin errors

Solution:

  • REST API server has CORS enabled by default
  • If using custom server, add CORS middleware
  • For production, configure specific origins

API not available

Problem: window.FMG_API is undefined

Solution:

  1. Ensure external-api.js is loaded in index.html
  2. Wait for DOMContentLoaded event
  3. Check browser console for script errors

Events not firing

Problem: Event listeners not receiving events

Solution:

  1. Subscribe to events BEFORE making changes
  2. Check event names (case-sensitive)
  3. Ensure change detection is enabled (automatic)

WebSocket disconnects

Problem: Socket disconnects unexpectedly

Solution:

  1. Check server is running
  2. Implement reconnection logic
  3. Handle disconnect event and reconnect

Advanced Configuration

Disable PostMessage Bridge

If you don't need iframe integration:

// In your fork, remove from external-api.js:
// PostMessageBridge.enable();

Custom Event Throttling

Adjust change detection throttle:

// In external-api.js, modify debounce time:
const observer = new MutationObserver(debounce(() => {
  // ...
}, 500)); // Change from 500ms to your preference

Enable Debug Logging

// Add to external-api.js for verbose logging:
const DEBUG = true;

if (DEBUG) {
  eventEmitter.on('*', (event, data) => {
    console.log('[FMG API]', event, data);
  });
}

Support

For issues, questions, or feature requests:


License

MIT License - same as Fantasy Map Generator


Happy Mapping! 🗺️