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.
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
- Embed FMG in an
<iframe> - Send commands via
postMessage() - 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
- Install dependencies:
cd api-server
npm install
- 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 createdmap:loaded- Map loaded from filemap:changed- Map modified (throttled)
Data Events
rivers:updated- Rivers data updatedcultures:updated- Cultures data updatedstates:updated- States data updatedburgs:updated- Burgs data updatedburg: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:
- Ensure iframe has loaded:
iframe.addEventListener('load', ...) - Wait 1-2 seconds after load for API to initialize
- Check origin in postMessage: use
'*'or specific origin - 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:
- Ensure
external-api.jsis loaded in index.html - Wait for DOMContentLoaded event
- Check browser console for script errors
Events not firing
Problem: Event listeners not receiving events
Solution:
- Subscribe to events BEFORE making changes
- Check event names (case-sensitive)
- Ensure change detection is enabled (automatic)
WebSocket disconnects
Problem: Socket disconnects unexpectedly
Solution:
- Check server is running
- Implement reconnection logic
- Handle
disconnectevent 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:
- GitHub Issues: https://github.com/Azgaar/Fantasy-Map-Generator/issues
- Wiki: https://github.com/Azgaar/Fantasy-Map-Generator/wiki
- Discord: [Join community]
License
MIT License - same as Fantasy Map Generator
Happy Mapping! 🗺️