mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
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.
737 lines
15 KiB
Markdown
737 lines
15 KiB
Markdown
# 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](#overview)
|
|
- [Integration Methods](#integration-methods)
|
|
- [1. PostMessage Bridge (iframe)](#1-postmessage-bridge-iframe)
|
|
- [2. REST API Server](#2-rest-api-server)
|
|
- [3. Direct JavaScript API](#3-direct-javascript-api)
|
|
- [API Reference](#api-reference)
|
|
- [Events](#events)
|
|
- [Examples](#examples)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
```html
|
|
<!-- 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()`:
|
|
|
|
```javascript
|
|
// 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:
|
|
```bash
|
|
cd api-server
|
|
npm install
|
|
```
|
|
|
|
2. Start the server:
|
|
```bash
|
|
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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
const api = window.FMG_API;
|
|
```
|
|
|
|
#### Methods
|
|
|
|
##### Map Lifecycle
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// In your fork, remove from external-api.js:
|
|
// PostMessageBridge.enable();
|
|
```
|
|
|
|
### Custom Event Throttling
|
|
|
|
Adjust change detection throttle:
|
|
|
|
```javascript
|
|
// In external-api.js, modify debounce time:
|
|
const observer = new MutationObserver(debounce(() => {
|
|
// ...
|
|
}, 500)); // Change from 500ms to your preference
|
|
```
|
|
|
|
### Enable Debug Logging
|
|
|
|
```javascript
|
|
// 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! 🗺️**
|