475 lines
15 KiB
HTML
475 lines
15 KiB
HTML
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="/static/leaflet.css"/>
|
|
|
|
<style>
|
|
#map {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 400px;
|
|
background: #1a1a1a;
|
|
border: 2px solid var(--lcars-hopbush);
|
|
border-radius: 8px;
|
|
box-sizing: border-box;
|
|
}
|
|
main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
.leaflet-container {
|
|
background: #1a1a1a;
|
|
}
|
|
.location-marker {
|
|
background-color: var(--lcars-golden-tanoi);
|
|
border: 2px solid var(--lcars-tanoi);
|
|
border-radius: 50%;
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
.player-marker {
|
|
background-color: var(--lcars-anakiwa);
|
|
border: 2px solid var(--lcars-mariner);
|
|
border-radius: 50%;
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
.map-controls {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Custom Legend Styling */
|
|
.map-legend {
|
|
background: rgba(26, 26, 26, 0.95);
|
|
border: 2px solid var(--lcars-hopbush);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
color: var(--lcars-golden-tanoi);
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
|
|
min-width: 200px;
|
|
}
|
|
.map-legend h4 {
|
|
margin: 0 0 8px 0;
|
|
color: var(--lcars-hopbush);
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
border-bottom: 1px solid var(--lcars-hopbush);
|
|
padding-bottom: 4px;
|
|
}
|
|
.map-legend .legend-item {
|
|
margin: 4px 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
.map-legend .legend-label {
|
|
color: var(--lcars-anakiwa);
|
|
font-weight: bold;
|
|
}
|
|
.map-legend .legend-value {
|
|
color: var(--lcars-golden-tanoi);
|
|
text-align: right;
|
|
}
|
|
|
|
/* Coordinates Display */
|
|
.coordinates-display {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
background: rgba(26, 26, 26, 0.95);
|
|
border: 2px solid var(--lcars-anakiwa);
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
color: var(--lcars-anakiwa);
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.coordinates-display .coord-label {
|
|
font-weight: bold;
|
|
color: var(--lcars-hopbush);
|
|
}
|
|
|
|
/* Create Location Button */
|
|
.create-location-btn {
|
|
background: var(--lcars-hopbush);
|
|
border: 2px solid var(--lcars-hopbush);
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.create-location-btn:hover {
|
|
background: var(--lcars-golden-tanoi);
|
|
border-color: var(--lcars-golden-tanoi);
|
|
color: #000;
|
|
}
|
|
.create-location-btn.active {
|
|
background: var(--lcars-tanoi);
|
|
border-color: var(--lcars-tanoi);
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
</style>
|
|
|
|
<header>
|
|
<div>
|
|
<span>Locations Map</span>
|
|
</div>
|
|
</header>
|
|
<aside>
|
|
{{ control_switch_view }}
|
|
</aside>
|
|
<main>
|
|
<div id="map"></div>
|
|
|
|
<!-- Leaflet JS -->
|
|
<script src="/static/leaflet.js"></script>
|
|
|
|
<script>
|
|
// Wait for Leaflet to load
|
|
(function initMap() {
|
|
if (typeof L === 'undefined') {
|
|
setTimeout(initMap, 100);
|
|
return;
|
|
}
|
|
|
|
// Check if map container exists
|
|
const mapContainer = document.getElementById('map');
|
|
if (!mapContainer) {
|
|
console.error('[MAP] Map container #map not found!');
|
|
return;
|
|
}
|
|
|
|
// 7D2D Projection (from Alloc's mod)
|
|
const SDTD_Projection = {
|
|
project: function (latlng) {
|
|
return new L.Point(
|
|
(latlng.lat) / Math.pow(2, 4),
|
|
(latlng.lng) / Math.pow(2, 4)
|
|
);
|
|
},
|
|
unproject: function (point) {
|
|
return new L.LatLng(
|
|
point.x * Math.pow(2, 4),
|
|
point.y * Math.pow(2, 4)
|
|
);
|
|
}
|
|
};
|
|
|
|
// 7D2D CRS (from Alloc's mod)
|
|
const SDTD_CRS = L.extend({}, L.CRS.Simple, {
|
|
projection: SDTD_Projection,
|
|
transformation: new L.Transformation(1, 0, -1, 0),
|
|
scale: function (zoom) {
|
|
return Math.pow(2, zoom);
|
|
}
|
|
});
|
|
|
|
// Initialize map with 7D2D CRS
|
|
const map = L.map('map', {
|
|
crs: SDTD_CRS,
|
|
center: [0, 0],
|
|
zoom: 3,
|
|
minZoom: -1,
|
|
maxZoom: 7,
|
|
attributionControl: false
|
|
});
|
|
|
|
// Create tile layer (Y-axis flipping handled by backend)
|
|
const tileLayer = L.tileLayer('/map_tiles/{z}/{x}/{y}.png', {
|
|
tileSize: 128,
|
|
minNativeZoom: 0,
|
|
minZoom: -1,
|
|
maxNativeZoom: 4,
|
|
maxZoom: 7
|
|
}).addTo(map);
|
|
|
|
// Storage for markers and shapes
|
|
const locationShapes = {};
|
|
const playerMarkers = {};
|
|
|
|
// Data variables for locations and players (initially empty, loaded via socket.io)
|
|
const locations = {};
|
|
const players = {};
|
|
|
|
// ========================================
|
|
// Location Shape Creation Function
|
|
// ========================================
|
|
{{ webmap_templates.location_shapes|safe }}
|
|
|
|
// ========================================
|
|
// Player Popup Creation
|
|
// ========================================
|
|
{{ webmap_templates.player_popup|safe }}
|
|
|
|
// ========================================
|
|
// Real-time Update Handlers
|
|
// ========================================
|
|
|
|
// Remove old map handler if it exists from previous view load
|
|
if (window.mapDataHandler) {
|
|
window.socket.off('data', window.mapDataHandler);
|
|
}
|
|
|
|
// Create NEW handler function that captures current local scope
|
|
// This must be recreated each time to access current locationShapes/playerMarkers
|
|
window.mapDataHandler = function(data) {
|
|
console.log('[MAP] Received socket.io data:', data.data_type, data);
|
|
|
|
// Map metadata (gameprefs, dataset info)
|
|
if (data.data_type === 'map_metadata' && data.payload) {
|
|
gameprefs = data.payload.gameprefs || {};
|
|
activeDataset = data.payload.active_dataset || 'Unknown';
|
|
updateLegend();
|
|
console.log('[MAP] Updated map metadata:', gameprefs, activeDataset);
|
|
}
|
|
|
|
// Player position updates
|
|
{{ webmap_templates.player_update_handler|safe }}
|
|
|
|
// Location updates and removals
|
|
{{ webmap_templates.location_update_handler|safe }}
|
|
|
|
// Update legend counts after any changes
|
|
if (data.data_type === 'player_position_update' || data.data_type === 'location_update' || data.data_type === 'location_remove') {
|
|
updateLegend();
|
|
}
|
|
};
|
|
|
|
// Register the new handler
|
|
window.socket.on('data', window.mapDataHandler);
|
|
|
|
// Fit map to show all markers and shapes on load
|
|
const allMapObjects = Object.values(locationShapes).concat(Object.values(playerMarkers));
|
|
|
|
if (allMapObjects.length > 0) {
|
|
const group = L.featureGroup(allMapObjects);
|
|
map.fitBounds(group.getBounds().pad(0.1));
|
|
}
|
|
|
|
// Fix map size after a short delay (in case container was hidden initially)
|
|
setTimeout(function() {
|
|
map.invalidateSize();
|
|
}, 250);
|
|
|
|
// ========================================
|
|
// FEATURE 1: Map Legend with Metadata (loaded via Socket.IO)
|
|
// ========================================
|
|
|
|
// Map metadata variables (loaded via socket.io)
|
|
let gameprefs = {};
|
|
let activeDataset = 'Loading...';
|
|
|
|
// Create custom legend control
|
|
const legend = L.control({position: 'topleft'});
|
|
let legendDiv = null;
|
|
|
|
legend.onAdd = function (mapInstance) {
|
|
legendDiv = L.DomUtil.create('div', 'map-legend');
|
|
updateLegend();
|
|
return legendDiv;
|
|
};
|
|
|
|
// Function to update legend content
|
|
function updateLegend() {
|
|
if (!legendDiv) return;
|
|
|
|
const gameName = gameprefs['GameName'] || activeDataset;
|
|
const gameWorld = gameprefs['GameWorld'] || 'N/A';
|
|
const worldGenSeed = gameprefs['WorldGenSeed'] || 'N/A';
|
|
const worldGenSize = gameprefs['WorldGenSize'] || 'N/A';
|
|
const locationCount = Object.keys(locations).length;
|
|
const playerCount = Object.keys(players).length;
|
|
|
|
legendDiv.innerHTML = `
|
|
<h4>🗺️ Map Info</h4>
|
|
<div class="legend-item">
|
|
<span class="legend-label">World:</span>
|
|
<span class="legend-value">${gameName}</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-label">Type:</span>
|
|
<span class="legend-value">${gameWorld}</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-label">Size:</span>
|
|
<span class="legend-value">${worldGenSize}</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-label">Seed:</span>
|
|
<span class="legend-value">${worldGenSeed}</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-label">Locations:</span>
|
|
<span class="legend-value" id="legend-location-count">${locationCount}</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-label">Players:</span>
|
|
<span class="legend-value" id="legend-player-count">${playerCount}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
legend.addTo(map);
|
|
|
|
// ========================================
|
|
// FEATURE 2: Coordinates Under Mouse Cursor
|
|
// ========================================
|
|
|
|
// Create coordinates display element
|
|
const coordsDiv = L.DomUtil.create('div', 'coordinates-display');
|
|
coordsDiv.innerHTML = '<span class="coord-label">X:</span> <span id="coord-x">0</span> | ' +
|
|
'<span class="coord-label">Z:</span> <span id="coord-z">0</span>';
|
|
document.getElementById('map').appendChild(coordsDiv);
|
|
|
|
// Update coordinates on mouse move
|
|
map.on('mousemove', function(e) {
|
|
const coords = e.latlng;
|
|
// In 7D2D, lat corresponds to X and lng to Z
|
|
document.getElementById('coord-x').textContent = Math.round(coords.lat);
|
|
document.getElementById('coord-z').textContent = Math.round(coords.lng);
|
|
});
|
|
|
|
// ========================================
|
|
// FEATURE 3: Create Location UI
|
|
// ========================================
|
|
|
|
// Store tempMarker globally so it can be accessed by confirm/cancel functions
|
|
let tempMarker = null;
|
|
|
|
// Create custom control for location creation
|
|
const createLocationControl = L.control({position: 'topright'});
|
|
|
|
createLocationControl.onAdd = function (mapInstance) {
|
|
const btn = L.DomUtil.create('button', 'create-location-btn');
|
|
btn.innerHTML = '📍 Create Location';
|
|
btn.title = 'Click to enable location creation mode';
|
|
|
|
// Prevent map interactions when clicking the button
|
|
L.DomEvent.disableClickPropagation(btn);
|
|
|
|
let creationMode = false;
|
|
|
|
btn.onclick = function() {
|
|
creationMode = !creationMode;
|
|
|
|
if (creationMode) {
|
|
btn.classList.add('active');
|
|
btn.innerHTML = '✖️ Cancel';
|
|
map.getContainer().style.cursor = 'crosshair';
|
|
|
|
// Add click handler for location creation
|
|
map.once('click', function(e) {
|
|
const coords = e.latlng;
|
|
const x = Math.round(coords.lat);
|
|
const z = Math.round(coords.lng);
|
|
|
|
// Create temporary marker
|
|
tempMarker = L.circleMarker([coords.lat, coords.lng], {
|
|
radius: 8,
|
|
fillColor: '#ff00ff',
|
|
color: '#ff00ff',
|
|
weight: 3,
|
|
opacity: 1,
|
|
fillOpacity: 0.6
|
|
}).addTo(map);
|
|
|
|
tempMarker.bindPopup(
|
|
'<div style="text-align: center;">' +
|
|
'<b>New Location</b><br>' +
|
|
'X: ' + x + ', Z: ' + z + '<br>' +
|
|
'<button onclick="confirmLocation(' + x + ', ' + z + ')" ' +
|
|
'style="margin: 5px; padding: 5px 10px; background: var(--lcars-hopbush); color: white; border: none; border-radius: 4px; cursor: pointer;">' +
|
|
'Create Here</button>' +
|
|
'<button onclick="cancelLocation()" ' +
|
|
'style="margin: 5px; padding: 5px 10px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">' +
|
|
'Cancel</button>' +
|
|
'</div>'
|
|
).openPopup();
|
|
|
|
// Reset button state
|
|
btn.classList.remove('active');
|
|
btn.innerHTML = '📍 Create Location';
|
|
map.getContainer().style.cursor = '';
|
|
creationMode = false;
|
|
});
|
|
} else {
|
|
btn.classList.remove('active');
|
|
btn.innerHTML = '📍 Create Location';
|
|
map.getContainer().style.cursor = '';
|
|
|
|
// Remove temp marker if exists
|
|
if (tempMarker) {
|
|
map.removeLayer(tempMarker);
|
|
tempMarker = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
return btn;
|
|
};
|
|
|
|
createLocationControl.addTo(map);
|
|
|
|
// Global functions for location creation confirmation
|
|
window.confirmLocation = function(x, z) {
|
|
// Navigate to create_new view with pre-filled coordinates using WebSocket
|
|
// This follows the exact same pattern as all other action calls in the system
|
|
window.socket.emit(
|
|
'widget_event', // Socket event name
|
|
['locations', // Module identifier
|
|
['toggle_locations_widget_view', // Action identifier
|
|
{ // Action parameters
|
|
'action': 'show_create_new',
|
|
'prefill_x': x,
|
|
'prefill_z': z,
|
|
'prefill_y': 0 // Default Y coordinate
|
|
}]]
|
|
);
|
|
|
|
console.log('[MAP] Navigating to create location form with coordinates:', x, z);
|
|
|
|
// Remove temp marker
|
|
if (tempMarker) {
|
|
map.removeLayer(tempMarker);
|
|
tempMarker = null;
|
|
}
|
|
};
|
|
|
|
window.cancelLocation = function() {
|
|
// Remove temp marker
|
|
if (tempMarker) {
|
|
map.removeLayer(tempMarker);
|
|
tempMarker = null;
|
|
}
|
|
};
|
|
|
|
// ========================================
|
|
// Location Actions (Edit, Enable, Move, Set Teleport)
|
|
// ========================================
|
|
{{ webmap_templates.location_actions|safe }}
|
|
|
|
// ========================================
|
|
// Player Actions (Kick, Message, Mute, Auth, Teleport)
|
|
// ========================================
|
|
{{ webmap_templates.player_actions|safe }}
|
|
|
|
})();
|
|
</script>
|
|
</main>
|