mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
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.
This commit is contained in:
parent
dede314c94
commit
20458e39e2
10 changed files with 4699 additions and 0 deletions
233
api-server/README.md
Normal file
233
api-server/README.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# Fantasy Map Generator - REST API Server
|
||||
|
||||
A Node.js/Express server that provides REST API endpoints and WebSocket support for the Fantasy Map Generator.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ RESTful API with full CRUD operations
|
||||
- ✅ WebSocket support for real-time updates
|
||||
- ✅ Map storage and management
|
||||
- ✅ CSV import for rivers and other data
|
||||
- ✅ Export support (SVG, PNG, JSON)
|
||||
- ✅ CORS enabled for cross-origin requests
|
||||
- ✅ File upload support
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run Server
|
||||
|
||||
```bash
|
||||
# Production
|
||||
npm start
|
||||
|
||||
# Development (with auto-reload)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Server will start on `http://localhost:3000`
|
||||
|
||||
## API Documentation
|
||||
|
||||
Visit `http://localhost:3000` after starting the server to see the full API documentation.
|
||||
|
||||
### Quick Examples
|
||||
|
||||
#### Create a Map
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/maps \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"seed": "my-world"}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"mapId": "map_1234567890_abc123",
|
||||
"message": "Map creation initiated",
|
||||
"pollUrl": "/api/maps/map_1234567890_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Map Data
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/maps/map_1234567890_abc123
|
||||
```
|
||||
|
||||
#### Update Rivers
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/maps/map_1234567890_abc123/rivers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"rivers": [{"i":1,"name":"Mystic River","type":"River"}]}'
|
||||
```
|
||||
|
||||
#### Import Rivers from CSV
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/maps/map_1234567890_abc123/rivers/import \
|
||||
-F "file=@rivers.csv"
|
||||
```
|
||||
|
||||
## WebSocket
|
||||
|
||||
### Connect
|
||||
|
||||
```javascript
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const socket = io('http://localhost:3000');
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected!');
|
||||
});
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
**Server Events (listen):**
|
||||
- `map:creating` - Map creation started
|
||||
- `map:created` - Map creation completed
|
||||
- `map:updated` - Map data updated
|
||||
- `map:deleted` - Map deleted
|
||||
- `rivers:updated` - Rivers updated
|
||||
- `rivers:imported` - Rivers imported from CSV
|
||||
- `burg:added` - New burg added
|
||||
|
||||
**Client Events (emit):**
|
||||
- `map:update` - Update map data
|
||||
- `map:created` - Notify map creation complete
|
||||
- `export:completed` - Export completed
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Port (default: 3000)
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
### Storage
|
||||
|
||||
By default, maps are stored in memory. For production, replace the `Map` with a database:
|
||||
|
||||
```javascript
|
||||
// Replace this in server.js
|
||||
const maps = new Map();
|
||||
|
||||
// With this (example with MongoDB)
|
||||
const maps = await db.collection('maps');
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM node:18
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
Build and run:
|
||||
```bash
|
||||
docker build -t fmg-api .
|
||||
docker run -p 3000:3000 fmg-api
|
||||
```
|
||||
|
||||
### Production Considerations
|
||||
|
||||
1. **Database**: Replace in-memory storage with MongoDB, PostgreSQL, etc.
|
||||
2. **Authentication**: Add JWT or OAuth for protected endpoints
|
||||
3. **Rate Limiting**: Add express-rate-limit
|
||||
4. **Validation**: Add input validation with joi or express-validator
|
||||
5. **Logging**: Add morgan or winston
|
||||
6. **Monitoring**: Add health checks and metrics
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client (Browser/Wiki)
|
||||
↓
|
||||
REST API / WebSocket
|
||||
↓
|
||||
Server (Express + Socket.IO)
|
||||
↓
|
||||
Storage (In-memory Map / Database)
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **express**: Web framework
|
||||
- **socket.io**: WebSocket server
|
||||
- **cors**: Cross-origin resource sharing
|
||||
- **multer**: File upload handling
|
||||
- **body-parser**: Request body parsing
|
||||
|
||||
## Development
|
||||
|
||||
### Add New Endpoint
|
||||
|
||||
```javascript
|
||||
// In server.js
|
||||
app.get('/api/maps/:id/custom', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
// Your logic here
|
||||
res.json({success: true, data: {}});
|
||||
} catch (error) {
|
||||
res.status(500).json({success: false, error: error.message});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Add WebSocket Event
|
||||
|
||||
```javascript
|
||||
// In server.js
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('custom:event', (data) => {
|
||||
// Handle event
|
||||
io.emit('custom:response', data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
Use the demo pages:
|
||||
- REST API: `http://localhost:3000/demos/rest-api-demo.html`
|
||||
- WebSocket: `http://localhost:3000/demos/websocket-demo.html`
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
npm install --save-dev mocha chai supertest
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT - Same as Fantasy Map Generator
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions, see the main repository.
|
||||
260
api-server/client.js
Normal file
260
api-server/client.js
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
/**
|
||||
* Fantasy Map Generator - JavaScript Client Library
|
||||
*
|
||||
* A simple client library for interacting with the FMG REST API
|
||||
*
|
||||
* Usage:
|
||||
* const client = new FMGClient('http://localhost:3000/api');
|
||||
* const map = await client.createMap({ seed: 'my-world' });
|
||||
*/
|
||||
|
||||
class FMGClient {
|
||||
constructor(baseUrl = 'http://localhost:3000/api') {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
* @private
|
||||
*/
|
||||
async _request(endpoint, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const config = {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...options.headers
|
||||
}
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
if (options.body instanceof FormData) {
|
||||
config.body = options.body;
|
||||
} else {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
config.body = JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error(`API request failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check API health
|
||||
* @returns {Promise<Object>} Health status
|
||||
*/
|
||||
async health() {
|
||||
return this._request('/health');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAP OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a new map
|
||||
* @param {Object} options - Map options
|
||||
* @param {string} options.seed - Map seed
|
||||
* @param {number} options.width - Map width
|
||||
* @param {number} options.height - Map height
|
||||
* @returns {Promise<Object>} Created map info
|
||||
*/
|
||||
async createMap(options = {}) {
|
||||
return this._request('/maps', {
|
||||
method: 'POST',
|
||||
body: options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map by ID
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Object>} Map data
|
||||
*/
|
||||
async getMap(mapId) {
|
||||
return this._request(`/maps/${mapId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all maps
|
||||
* @returns {Promise<Object>} Map list
|
||||
*/
|
||||
async listMaps() {
|
||||
return this._request('/maps');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {Object} data - Map data
|
||||
* @returns {Promise<Object>} Updated map
|
||||
*/
|
||||
async updateMap(mapId, data) {
|
||||
return this._request(`/maps/${mapId}`, {
|
||||
method: 'PUT',
|
||||
body: { data }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete map
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Object>} Deletion result
|
||||
*/
|
||||
async deleteMap(mapId) {
|
||||
return this._request(`/maps/${mapId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load map from file
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {File} file - Map file
|
||||
* @returns {Promise<Object>} Load result
|
||||
*/
|
||||
async loadMap(mapId, file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return this._request(`/maps/${mapId}/load`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RIVERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get rivers
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Array>} Rivers array
|
||||
*/
|
||||
async getRivers(mapId) {
|
||||
const result = await this._request(`/maps/${mapId}/rivers`);
|
||||
return result.rivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rivers
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {Array} rivers - Rivers array
|
||||
* @returns {Promise<Object>} Update result
|
||||
*/
|
||||
async updateRivers(mapId, rivers) {
|
||||
return this._request(`/maps/${mapId}/rivers`, {
|
||||
method: 'PUT',
|
||||
body: { rivers }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Import rivers from CSV
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {File} csvFile - CSV file
|
||||
* @returns {Promise<Object>} Import result
|
||||
*/
|
||||
async importRiversCSV(mapId, csvFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', csvFile);
|
||||
|
||||
return this._request(`/maps/${mapId}/rivers/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CULTURES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get cultures
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Array>} Cultures array
|
||||
*/
|
||||
async getCultures(mapId) {
|
||||
const result = await this._request(`/maps/${mapId}/cultures`);
|
||||
return result.cultures;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get states
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Array>} States array
|
||||
*/
|
||||
async getStates(mapId) {
|
||||
const result = await this._request(`/maps/${mapId}/states`);
|
||||
return result.states;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BURGS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get burgs (cities/towns)
|
||||
* @param {string} mapId - Map ID
|
||||
* @returns {Promise<Array>} Burgs array
|
||||
*/
|
||||
async getBurgs(mapId) {
|
||||
const result = await this._request(`/maps/${mapId}/burgs`);
|
||||
return result.burgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new burg
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {Object} burgData - Burg data
|
||||
* @returns {Promise<Object>} Created burg
|
||||
*/
|
||||
async addBurg(mapId, burgData) {
|
||||
return this._request(`/maps/${mapId}/burgs`, {
|
||||
method: 'POST',
|
||||
body: burgData
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Export map in specified format
|
||||
* @param {string} mapId - Map ID
|
||||
* @param {string} format - Export format (svg, png, json, data)
|
||||
* @returns {Promise<Object>} Export result
|
||||
*/
|
||||
async exportMap(mapId, format) {
|
||||
return this._request(`/maps/${mapId}/export/${format}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for Node.js and browser
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = FMGClient;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.FMGClient = FMGClient;
|
||||
}
|
||||
31
api-server/package.json
Normal file
31
api-server/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "fantasy-map-generator-api",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API server for Fantasy Map Generator with WebSocket support",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"fantasy",
|
||||
"map",
|
||||
"generator",
|
||||
"api",
|
||||
"rest",
|
||||
"websocket"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"body-parser": "^1.20.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"socket.io": "^4.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
830
api-server/server.js
Normal file
830
api-server/server.js
Normal file
|
|
@ -0,0 +1,830 @@
|
|||
/**
|
||||
* Fantasy Map Generator - REST API Server
|
||||
* Provides HTTP endpoints for external tools to control the map generator
|
||||
*
|
||||
* Usage:
|
||||
* npm install
|
||||
* node server.js
|
||||
*
|
||||
* Then access via http://localhost:3000
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { createServer } = require('http');
|
||||
const { Server: SocketIOServer } = require('socket.io');
|
||||
|
||||
// Initialize Express
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({limit: '50mb'}));
|
||||
app.use(bodyParser.urlencoded({extended: true, limit: '50mb'}));
|
||||
|
||||
// Serve static files from FMG directory
|
||||
const FMG_ROOT = path.join(__dirname, '..');
|
||||
app.use('/fmg', express.static(FMG_ROOT));
|
||||
|
||||
// File upload configuration
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {fileSize: 50 * 1024 * 1024} // 50MB
|
||||
});
|
||||
|
||||
// In-memory storage for maps (use database in production)
|
||||
const maps = new Map();
|
||||
|
||||
// ============================================================================
|
||||
// API ENDPOINTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* GET /api/health
|
||||
* Health check endpoint
|
||||
*/
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/maps
|
||||
* Create a new map
|
||||
* Body: { seed?: string, width?: number, height?: number, ...options }
|
||||
*/
|
||||
app.post('/api/maps', async (req, res) => {
|
||||
try {
|
||||
const options = req.body;
|
||||
const mapId = generateMapId();
|
||||
|
||||
// Store map creation request
|
||||
maps.set(mapId, {
|
||||
id: mapId,
|
||||
status: 'pending',
|
||||
options,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Notify via WebSocket that map is being created
|
||||
io.emit('map:creating', {mapId, options});
|
||||
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
mapId,
|
||||
message: 'Map creation initiated. Use WebSocket or polling to get updates.',
|
||||
pollUrl: `/api/maps/${mapId}`,
|
||||
websocketEvent: 'map:created'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id
|
||||
* Get map by ID
|
||||
*/
|
||||
app.get('/api/maps/:id', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
map
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps
|
||||
* List all maps
|
||||
*/
|
||||
app.get('/api/maps', async (req, res) => {
|
||||
try {
|
||||
const allMaps = Array.from(maps.values());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: allMaps.length,
|
||||
maps: allMaps
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/maps/:id
|
||||
* Update map data
|
||||
* Body: { data: mapData }
|
||||
*/
|
||||
app.put('/api/maps/:id', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const {data} = req.body;
|
||||
|
||||
if (!maps.has(id)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
const map = maps.get(id);
|
||||
map.data = data;
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(id, map);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('map:updated', {mapId: id, data});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
map
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/maps/:id
|
||||
* Delete a map
|
||||
*/
|
||||
app.delete('/api/maps/:id', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
|
||||
if (!maps.has(id)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
maps.delete(id);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('map:deleted', {mapId: id});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Map deleted'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/maps/:id/load
|
||||
* Load map from file
|
||||
* Body: multipart/form-data with 'file' field
|
||||
*/
|
||||
app.post('/api/maps/:id/load', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file provided'
|
||||
});
|
||||
}
|
||||
|
||||
const mapData = req.file.buffer.toString('utf-8');
|
||||
|
||||
if (!maps.has(id)) {
|
||||
maps.set(id, {
|
||||
id,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const map = maps.get(id);
|
||||
map.data = mapData;
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(id, map);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('map:loaded', {mapId: id});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
map
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id/export/:format
|
||||
* Export map in specified format
|
||||
* Formats: svg, png, json, data
|
||||
*/
|
||||
app.get('/api/maps/:id/export/:format', async (req, res) => {
|
||||
try {
|
||||
const {id, format} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Request export via WebSocket and wait for response
|
||||
io.emit('export:request', {mapId: id, format});
|
||||
|
||||
// In a real implementation, you'd wait for the export to complete
|
||||
// For now, return a pending response
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Export requested. Listen to WebSocket event for completion.',
|
||||
websocketEvent: `export:completed:${id}`
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id/rivers
|
||||
* Get rivers data
|
||||
*/
|
||||
app.get('/api/maps/:id/rivers', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map || !map.data) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found or no data available'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
rivers: map.data.pack?.rivers || []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/maps/:id/rivers
|
||||
* Update rivers data
|
||||
* Body: { rivers: [...] }
|
||||
*/
|
||||
app.put('/api/maps/:id/rivers', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const {rivers} = req.body;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.data) {
|
||||
map.data = {pack: {}};
|
||||
}
|
||||
if (!map.data.pack) {
|
||||
map.data.pack = {};
|
||||
}
|
||||
|
||||
map.data.pack.rivers = rivers;
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(id, map);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('rivers:updated', {mapId: id, rivers});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
rivers
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/maps/:id/rivers/import
|
||||
* Import rivers from CSV
|
||||
* Body: multipart/form-data with 'file' field
|
||||
*/
|
||||
app.post('/api/maps/:id/rivers/import', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file provided'
|
||||
});
|
||||
}
|
||||
|
||||
const csvData = req.file.buffer.toString('utf-8');
|
||||
|
||||
// Parse CSV (basic implementation)
|
||||
const rows = csvData.split('\n');
|
||||
const headers = rows[0].split(',').map(h => h.trim().toLowerCase());
|
||||
const rivers = [];
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
if (!rows[i].trim()) continue;
|
||||
|
||||
const values = rows[i].split(',');
|
||||
const river = {};
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
river[header] = values[index]?.trim();
|
||||
});
|
||||
|
||||
rivers.push(river);
|
||||
}
|
||||
|
||||
// Update map
|
||||
const map = maps.get(id);
|
||||
if (!map) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.data) map.data = {pack: {}};
|
||||
if (!map.data.pack) map.data.pack = {};
|
||||
|
||||
map.data.pack.rivers = rivers;
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(id, map);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('rivers:imported', {mapId: id, rivers});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: rivers.length,
|
||||
rivers
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id/cultures
|
||||
* Get cultures data
|
||||
*/
|
||||
app.get('/api/maps/:id/cultures', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map || !map.data) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found or no data available'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
cultures: map.data.pack?.cultures || []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id/states
|
||||
* Get states data
|
||||
*/
|
||||
app.get('/api/maps/:id/states', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map || !map.data) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found or no data available'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
states: map.data.pack?.states || []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/maps/:id/burgs
|
||||
* Get burgs (cities/towns) data
|
||||
*/
|
||||
app.get('/api/maps/:id/burgs', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map || !map.data) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found or no data available'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
burgs: map.data.pack?.burgs || []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/maps/:id/burgs
|
||||
* Add a new burg
|
||||
* Body: { name, x, y, cell, population, type, ... }
|
||||
*/
|
||||
app.post('/api/maps/:id/burgs', async (req, res) => {
|
||||
try {
|
||||
const {id} = req.params;
|
||||
const burgData = req.body;
|
||||
const map = maps.get(id);
|
||||
|
||||
if (!map) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Map not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.data) map.data = {pack: {}};
|
||||
if (!map.data.pack) map.data.pack = {};
|
||||
if (!map.data.pack.burgs) map.data.pack.burgs = [];
|
||||
|
||||
const newBurg = {
|
||||
i: map.data.pack.burgs.length,
|
||||
...burgData
|
||||
};
|
||||
|
||||
map.data.pack.burgs.push(newBurg);
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(id, map);
|
||||
|
||||
// Notify via WebSocket
|
||||
io.emit('burg:added', {mapId: id, burg: newBurg});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
burg: newBurg
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WEBSOCKET HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected:', socket.id);
|
||||
|
||||
// Client can send updates directly
|
||||
socket.on('map:update', (data) => {
|
||||
const {mapId, updates} = data;
|
||||
const map = maps.get(mapId);
|
||||
|
||||
if (map) {
|
||||
Object.assign(map, updates);
|
||||
map.updatedAt = new Date().toISOString();
|
||||
maps.set(mapId, map);
|
||||
|
||||
// Broadcast to all clients
|
||||
io.emit('map:updated', {mapId, updates});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('map:created', (data) => {
|
||||
const {mapId, mapData} = data;
|
||||
const map = maps.get(mapId);
|
||||
|
||||
if (map) {
|
||||
map.status = 'ready';
|
||||
map.data = mapData;
|
||||
map.readyAt = new Date().toISOString();
|
||||
maps.set(mapId, map);
|
||||
|
||||
// Broadcast to all clients
|
||||
io.emit('map:created', {mapId, mapData});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('export:completed', (data) => {
|
||||
const {mapId, format, exportData} = data;
|
||||
|
||||
// Broadcast to all clients
|
||||
io.emit(`export:completed:${mapId}`, {format, exportData});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected:', socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// STATIC PAGES
|
||||
// ============================================================================
|
||||
|
||||
// Serve demo page
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Fantasy Map Generator - API Server</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
.endpoint {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.get { background: #28a745; }
|
||||
.post { background: #007bff; }
|
||||
.put { background: #ffc107; color: #000; }
|
||||
.delete { background: #dc3545; }
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.status { color: #28a745; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Fantasy Map Generator - API Server</h1>
|
||||
<p class="status">✓ Server is running</p>
|
||||
|
||||
<h2>REST API Endpoints</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/health</code>
|
||||
<p>Health check endpoint</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span>
|
||||
<code>/api/maps</code>
|
||||
<p>Create a new map</p>
|
||||
<pre>{
|
||||
"seed": "optional-seed",
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps</code>
|
||||
<p>List all maps</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps/:id</code>
|
||||
<p>Get specific map by ID</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method put">PUT</span>
|
||||
<code>/api/maps/:id</code>
|
||||
<p>Update map data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method delete">DELETE</span>
|
||||
<code>/api/maps/:id</code>
|
||||
<p>Delete a map</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps/:id/rivers</code>
|
||||
<p>Get rivers data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method put">PUT</span>
|
||||
<code>/api/maps/:id/rivers</code>
|
||||
<p>Update rivers data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span>
|
||||
<code>/api/maps/:id/rivers/import</code>
|
||||
<p>Import rivers from CSV file</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps/:id/cultures</code>
|
||||
<p>Get cultures data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps/:id/states</code>
|
||||
<p>Get states data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<code>/api/maps/:id/burgs</code>
|
||||
<p>Get burgs (cities/towns) data</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span>
|
||||
<code>/api/maps/:id/burgs</code>
|
||||
<p>Add a new burg</p>
|
||||
</div>
|
||||
|
||||
<h2>WebSocket Events</h2>
|
||||
<p>Connect to WebSocket at: <code>ws://localhost:3000</code></p>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Server Events:</strong>
|
||||
<ul>
|
||||
<li><code>map:creating</code> - Map creation started</li>
|
||||
<li><code>map:created</code> - Map creation completed</li>
|
||||
<li><code>map:updated</code> - Map data updated</li>
|
||||
<li><code>map:deleted</code> - Map deleted</li>
|
||||
<li><code>rivers:updated</code> - Rivers data updated</li>
|
||||
<li><code>rivers:imported</code> - Rivers imported from CSV</li>
|
||||
<li><code>burg:added</code> - New burg added</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Integration Examples</h2>
|
||||
<p>Visit these demo pages:</p>
|
||||
<ul>
|
||||
<li><a href="/demos/postmessage-demo.html">PostMessage Demo (iframe integration)</a></li>
|
||||
<li><a href="/demos/rest-api-demo.html">REST API Demo</a></li>
|
||||
<li><a href="/demos/websocket-demo.html">WebSocket Demo</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Quick Start</h2>
|
||||
<pre>// Create a new map
|
||||
curl -X POST http://localhost:3000/api/maps \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"seed": "my-custom-seed"}'
|
||||
|
||||
// Get map data
|
||||
curl http://localhost:3000/api/maps/MAP_ID
|
||||
|
||||
// Update rivers
|
||||
curl -X PUT http://localhost:3000/api/maps/MAP_ID/rivers \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"rivers": [...]}'</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function generateMapId() {
|
||||
return 'map_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// START SERVER
|
||||
// ============================================================================
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Fantasy Map Generator - API Server');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Server running on: http://localhost:${PORT}`);
|
||||
console.log(`WebSocket endpoint: ws://localhost:${PORT}`);
|
||||
console.log('');
|
||||
console.log('REST API: http://localhost:' + PORT + '/api');
|
||||
console.log('Documentation: http://localhost:' + PORT);
|
||||
console.log('='.repeat(60));
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue