1740 lines
65 KiB
HTML
1740 lines
65 KiB
HTML
{% extends "mobile/mobile_base.html" %}
|
|
{% load static i18n %}
|
|
|
|
{% block extra_head %}
|
|
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet"/>
|
|
<link href="{% static 'common/custom-draw-toolbar.css' %}" rel="stylesheet"/>
|
|
<link href="{% static 'assets/info-panel.css' %}" rel="stylesheet"/>
|
|
<style>
|
|
body {
|
|
overflow: hidden;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
-webkit-touch-callout: none;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
button, input[type="button"], input[type="submit"], a {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
#map {
|
|
touch-action: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
#map-container {
|
|
position: fixed;
|
|
top: 56px;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
#map {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
#attributes-panel {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: white;
|
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
|
z-index: 1000;
|
|
max-height: 50vh;
|
|
overflow-y: auto;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
#attributes-panel.collapsed {
|
|
transform: translateY(calc(100% - 50px));
|
|
}
|
|
|
|
#panel-toggle {
|
|
position: sticky;
|
|
top: 0;
|
|
background: white;
|
|
border-bottom: 1px solid #ddd;
|
|
padding: 0.5rem 1rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
z-index: 10;
|
|
}
|
|
|
|
#panel-content {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.maplibregl-ctrl-group button {
|
|
width: 42px;
|
|
height: 42px;
|
|
display: flex !important;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.maplibregl-ctrl-group button span {
|
|
display: block;
|
|
width: 32px;
|
|
height: 32px;
|
|
background-size: 32px 32px;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.marker-current-location {
|
|
background-color: #4285F4;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
border: 3px solid white;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
/* Model badges styling */
|
|
.model-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.4rem 0.2rem;
|
|
margin: 0.25rem;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
border: 2px solid transparent;
|
|
transition: all 0.2s ease;
|
|
font-size: 0.75rem;
|
|
background-color: #f8f9fa;
|
|
width: 120px;
|
|
}
|
|
|
|
.model-badge:hover {
|
|
background-color: #e9ecef;
|
|
}
|
|
|
|
.model-badge.active {
|
|
border-color: #0d6efd;
|
|
background-color: #e7f3ff;
|
|
}
|
|
|
|
.model-badge.disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.model-badge .color-swatch {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
margin-right: 0.5rem;
|
|
border: 1px solid rgba(0,0,0,0.2);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.model-badge .model-name {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 120px;
|
|
}
|
|
|
|
/* Selection info styling */
|
|
.selection-info {
|
|
background: #e7f3ff;
|
|
border-left: 4px solid #0d6efd;
|
|
padding: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.modified-info {
|
|
background: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
padding: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Status badges */
|
|
.status-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.status-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* Tablet landscape mode */
|
|
@media (min-width: 768px) and (orientation: landscape) {
|
|
#map-container {
|
|
right: 33.33%;
|
|
}
|
|
|
|
#attributes-panel {
|
|
left: 66.67%;
|
|
bottom: 0;
|
|
top: 56px;
|
|
max-height: none;
|
|
transform: none !important;
|
|
}
|
|
|
|
#panel-toggle {
|
|
display: none;
|
|
}
|
|
|
|
#attributes-panel.collapsed {
|
|
transform: none;
|
|
}
|
|
|
|
.draw-floating-actions {
|
|
right: calc(33.33% + 20px);
|
|
}
|
|
|
|
.draw-help-message {
|
|
max-width: calc(66.67% - 40px);
|
|
}
|
|
}
|
|
|
|
/* Phone mode: keep space for collapsed attributes panel */
|
|
@media (max-width: 767px) {
|
|
#map-container {
|
|
bottom: 50px;
|
|
}
|
|
}
|
|
|
|
/* Floating exit button on map */
|
|
.draw-floating-actions-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
width: 100%;
|
|
}
|
|
|
|
.draw-floating-btn-exit {
|
|
background-color: #dc3545;
|
|
color: #fff;
|
|
}
|
|
|
|
.draw-floating-btn-exit:hover {
|
|
background-color: #bb2d3b;
|
|
}
|
|
</style>
|
|
{% endblock extra_head %}
|
|
|
|
{% block title %}
|
|
{% translate "Espaces verts - V2" %}
|
|
{% endblock title %}
|
|
|
|
{% block content %}
|
|
<!-- CSRF Token for API requests -->
|
|
{% csrf_token %}
|
|
|
|
<div id="map-container">
|
|
<div id="map"></div>
|
|
</div>
|
|
|
|
<div id="attributes-panel" class="collapsed">
|
|
<div id="panel-toggle">
|
|
<strong>
|
|
<span id="panel-title">{% translate "Édition des espaces verts" %}</span>
|
|
<span id="selection-count" class="badge bg-primary ms-2"></span>
|
|
</strong>
|
|
<i class="bi bi-chevron-up" id="toggle-icon"></i>
|
|
</div>
|
|
<div id="panel-content">
|
|
<!-- Comment content container (hidden by default) -->
|
|
<div id="comment-panel-content" style="display: none;">
|
|
<!-- Le contenu des commentaires sera injecté ici -->
|
|
</div>
|
|
|
|
<!-- Default panel content -->
|
|
<div id="default-panel-content">
|
|
<!-- Modified geometries info -->
|
|
<div class="modified-info" id="modified-info" style="display: none;">
|
|
<strong><i class="bi bi-pencil-square"></i> <span id="modified-assets-count">0</span> {% translate "géométrie(s) modifiée(s)" %}</strong>
|
|
</div>
|
|
|
|
<!-- Selection info for normal mode -->
|
|
<div class="selection-info" id="selection-info" style="display: none;">
|
|
<strong><i class="bi bi-check2-square"></i> <span id="selected-count">0</span> {% translate "élément(s) sélectionné(s)" %}</strong>
|
|
<p class="mb-0 small text-muted">{% translate "Sélectionnez un modèle ci-dessous pour l'appliquer aux éléments sélectionnés." %}</p>
|
|
</div>
|
|
|
|
<!-- Attribute Edit Panel - for draw mode editing -->
|
|
<div id="attribute-panel" style="display: none;">
|
|
<h6><i class="bi bi-pencil-square"></i> {% translate "Édition des attributs" %}</h6>
|
|
<div class="mb-2">
|
|
<small class="text-muted">ID: <span id="edit-feature-id">-</span></small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model selection with badges -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold"><i class="bi bi-collection"></i> {% translate "Modèle" %}</label>
|
|
<div id="model-badges" class="d-flex flex-wrap">
|
|
{% for model in models %}
|
|
<div class="model-badge disabled" data-value="{{ model.id }}" data-color="{{ model.color|default:'#cccccc' }}" title="{{ model.get_name }}">
|
|
<span class="color-swatch" style="background-color: {{ model.color|default:'#cccccc' }};"></span>
|
|
<span class="model-name">{{ model.get_name }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<input type="hidden" id="model-select" name="model_id">
|
|
</div>
|
|
|
|
<!-- Bulb types selection (green surfaces only) -->
|
|
<div class="mb-3">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<label class="form-label fw-bold mb-0"><i class="bi bi-flower1"></i> {% translate "Types de bulbes" %}</label>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" id="btn-toggle-bulbs">
|
|
<i class="bi bi-chevron-down" id="bulbs-toggle-icon"></i>
|
|
</button>
|
|
</div>
|
|
<div id="bulb-types-panel" style="display: none;">
|
|
<div id="bulb-badges" class="d-flex flex-wrap">
|
|
{% for value, label in bulb_type_choices %}
|
|
<div class="model-badge bulb-badge disabled" data-value="{{ value }}" title="{{ label }}">
|
|
<span class="model-name">{{ label }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<input type="hidden" id="bulb-types-input" name="bulb_types" value="[]">
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="btn-toolbar mb-3 gap-2">
|
|
<button type="button" class="btn btn-sm btn-primary" id="btn-save" disabled>
|
|
<i class="bi bi-save"></i> {% translate "Enregistrer" %}
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-clear-selection">
|
|
<i class="bi bi-x-circle"></i> {% translate "Désélectionner" %}
|
|
</button>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<!-- Status info -->
|
|
<div id="status-info">
|
|
<div class="status-row">
|
|
<div class="status-item">
|
|
<span class="text-muted">{% translate "Mode dessin" %}:</span>
|
|
<span id="draw-mode-status" class="badge bg-secondary">{% translate "Inactif" %}</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="text-muted">{% translate "Modifications" %}:</span>
|
|
<span id="modified-count" class="badge bg-warning text-dark">0</span>
|
|
</div>
|
|
</div>
|
|
<div class="status-row">
|
|
<div class="status-item">
|
|
<span class="text-muted">{% translate "Assets chargés" %}:</span>
|
|
<span id="loaded-count" class="badge bg-info">0</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="text-muted">{% translate "En édition" %}:</span>
|
|
<span id="draw-count" class="badge bg-primary">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="edit-message" class="alert mt-3" style="display: none;"></div>
|
|
</div><!-- End of default-panel-content -->
|
|
</div>
|
|
</div>
|
|
{% endblock content %}
|
|
|
|
{% block footer %}
|
|
{% endblock footer %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
|
<script src="{% static 'common/custom-draw.js' %}"></script>
|
|
<script src="{% static 'common/custom-draw-toolbar.js' %}"></script>
|
|
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
|
<script src="{% static 'assets/green-surfaces-comments.js' %}"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Green Surfaces Edit V2
|
|
*
|
|
* Combines CustomDrawToolbar from standalone template with:
|
|
* - Multi-selection capability for bulk model updates
|
|
* - Ergonomic model selection with Bootstrap badges
|
|
* - Geolocated comments with photo capture
|
|
*
|
|
* Features:
|
|
* - Normal mode: Click assets to select/deselect for bulk operations
|
|
* - Draw mode (via CustomDrawToolbar): Edit geometries directly
|
|
* - Model badges: Quick model assignment with visual color swatches
|
|
* - Comments: Add geolocated comments with photos (independent from assets)
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Configuration
|
|
const MIN_ZOOM_FOR_ASSETS = 15;
|
|
const BUFFER_SIZE = 0.001; // degrees (~100m)
|
|
const ORTHO_WMTS_URL = 'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}';
|
|
|
|
// State
|
|
let map = null;
|
|
let draw = null;
|
|
let drawToolbar = null;
|
|
let currentLocation = null;
|
|
let selectedAssets = new Set(); // For normal mode multi-selection
|
|
let selectedBulbTypes = new Set(); // Currently selected bulb types
|
|
let currentEditFeatureId = null; // For draw mode editing
|
|
|
|
// =============================
|
|
// Helper Functions
|
|
// =============================
|
|
|
|
function showMessage(message, type) {
|
|
const messageDiv = document.getElementById('edit-message');
|
|
if (messageDiv) {
|
|
messageDiv.className = `alert alert-${type}`;
|
|
messageDiv.textContent = message;
|
|
messageDiv.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
messageDiv.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
// Also show toast
|
|
const toast = document.createElement('div');
|
|
toast.className = `alert alert-${type} position-fixed top-0 start-50 translate-middle-x mt-3`;
|
|
toast.style.zIndex = '9999';
|
|
toast.style.minWidth = '300px';
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 4000);
|
|
}
|
|
|
|
function toggleLoading(show) {
|
|
let loader = document.getElementById('loading-overlay');
|
|
if (!loader && show) {
|
|
loader = document.createElement('div');
|
|
loader.id = 'loading-overlay';
|
|
loader.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center';
|
|
loader.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
|
loader.style.zIndex = '9999';
|
|
loader.innerHTML = '<div class="spinner-border text-light" role="status"><span class="visually-hidden">Loading...</span></div>';
|
|
document.body.appendChild(loader);
|
|
}
|
|
if (loader) {
|
|
loader.style.display = show ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
function getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
|
|
// =============================
|
|
// Model Badge Functions
|
|
// =============================
|
|
|
|
function setModelValue(value) {
|
|
const input = document.getElementById('model-select');
|
|
input.value = value || '';
|
|
|
|
// Update badge UI - exclude bulb badges
|
|
document.querySelectorAll('.model-badge:not(.bulb-badge)').forEach(badge => {
|
|
if (badge.dataset.value === String(value)) {
|
|
badge.classList.add('active');
|
|
} else {
|
|
badge.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
function enableModelBadges(enable) {
|
|
document.querySelectorAll('.model-badge:not(.bulb-badge)').forEach(badge => {
|
|
if (enable) {
|
|
badge.classList.remove('disabled');
|
|
} else {
|
|
badge.classList.add('disabled');
|
|
badge.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// =============================
|
|
// Bulb Type Functions
|
|
// =============================
|
|
|
|
function getBulbTypesArray() {
|
|
return Array.from(selectedBulbTypes);
|
|
}
|
|
|
|
function setBulbTypes(values) {
|
|
selectedBulbTypes.clear();
|
|
(values || []).forEach(v => selectedBulbTypes.add(v));
|
|
document.querySelectorAll('.bulb-badge').forEach(badge => {
|
|
badge.classList.toggle('active', selectedBulbTypes.has(badge.dataset.value));
|
|
});
|
|
const input = document.getElementById('bulb-types-input');
|
|
if (input) input.value = JSON.stringify(Array.from(selectedBulbTypes));
|
|
// Auto-open panel when there are selected values, collapse when empty
|
|
const panel = document.getElementById('bulb-types-panel');
|
|
const icon = document.getElementById('bulbs-toggle-icon');
|
|
if (panel) {
|
|
if (selectedBulbTypes.size > 0) {
|
|
panel.style.display = '';
|
|
if (icon) icon.className = 'bi bi-chevron-up';
|
|
} else {
|
|
panel.style.display = 'none';
|
|
if (icon) icon.className = 'bi bi-chevron-down';
|
|
}
|
|
}
|
|
}
|
|
|
|
function enableBulbBadges(enable) {
|
|
document.querySelectorAll('.bulb-badge').forEach(badge => {
|
|
if (enable) {
|
|
badge.classList.remove('disabled');
|
|
} else {
|
|
badge.classList.add('disabled');
|
|
badge.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// =============================
|
|
// Selection Functions (Normal Mode)
|
|
// =============================
|
|
|
|
function updateSelectionUI() {
|
|
const count = selectedAssets.size;
|
|
|
|
document.getElementById('selected-count').textContent = count;
|
|
document.getElementById('selection-count').textContent = count || '';
|
|
|
|
if (count > 0) {
|
|
document.getElementById('selection-info').style.display = 'block';
|
|
enableModelBadges(true);
|
|
enableBulbBadges(true);
|
|
document.getElementById('btn-save').disabled = false;
|
|
|
|
// Check for common model among selected assets
|
|
let commonModel = undefined;
|
|
let firstPass = true;
|
|
let diverseModel = false;
|
|
|
|
for (const assetId of selectedAssets) {
|
|
// Try to get asset with different ID formats (string/number)
|
|
let asset = drawToolbar?.loadedAssets.get(assetId);
|
|
if (!asset) asset = drawToolbar?.loadedAssets.get(String(assetId));
|
|
if (!asset) asset = drawToolbar?.loadedAssets.get(Number(assetId));
|
|
|
|
const props = asset?.properties || {};
|
|
const modId = props.model_id ? String(props.model_id) : "";
|
|
|
|
if (firstPass) {
|
|
commonModel = modId;
|
|
firstPass = false;
|
|
} else {
|
|
if (commonModel !== modId) diverseModel = true;
|
|
}
|
|
|
|
if (diverseModel) break;
|
|
}
|
|
|
|
// Set model value only if all selected assets have the same model
|
|
// Otherwise, no option is preselected
|
|
if (!diverseModel && commonModel) {
|
|
setModelValue(commonModel);
|
|
} else {
|
|
// Clear selection - diverse models or no model
|
|
setModelValue('');
|
|
}
|
|
|
|
// Check for common bulb types among selected assets
|
|
let commonBulbTypesKey = null;
|
|
let firstPassBulb = true;
|
|
let diverseBulbs = false;
|
|
|
|
for (const assetId of selectedAssets) {
|
|
let asset = drawToolbar?.loadedAssets.get(assetId);
|
|
if (!asset) asset = drawToolbar?.loadedAssets.get(String(assetId));
|
|
if (!asset) asset = drawToolbar?.loadedAssets.get(Number(assetId));
|
|
|
|
const props = asset?.properties || {};
|
|
let bulbs = props.bulb_types || [];
|
|
if (typeof bulbs === 'string') {
|
|
try { bulbs = JSON.parse(bulbs); } catch(e) { bulbs = []; }
|
|
}
|
|
const bulbsKey = JSON.stringify([...bulbs].sort());
|
|
|
|
if (firstPassBulb) {
|
|
commonBulbTypesKey = bulbsKey;
|
|
firstPassBulb = false;
|
|
} else if (commonBulbTypesKey !== bulbsKey) {
|
|
diverseBulbs = true;
|
|
}
|
|
if (diverseBulbs) break;
|
|
}
|
|
|
|
setBulbTypes(!diverseBulbs && commonBulbTypesKey !== null ? JSON.parse(commonBulbTypesKey) : []);
|
|
} else {
|
|
document.getElementById('selection-info').style.display = 'none';
|
|
enableModelBadges(false);
|
|
setModelValue('');
|
|
setBulbTypes([]);
|
|
enableBulbBadges(false);
|
|
document.getElementById('btn-save').disabled = true;
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedAssets.forEach(assetId => {
|
|
map.setFeatureState(
|
|
{ source: 'assets', id: assetId },
|
|
{ selected: false }
|
|
);
|
|
});
|
|
selectedAssets.clear();
|
|
updateSelectionUI();
|
|
}
|
|
|
|
function handleAssetClick(e) {
|
|
// Ignore clicks when comment repositioning is active
|
|
if (window.commentsComponent?.isRepositioning) {
|
|
return;
|
|
}
|
|
|
|
// Close comment panel if open (asset click means user wants asset interaction)
|
|
if (window.infoPanel?.isOpen()) {
|
|
window.infoPanel.close();
|
|
}
|
|
|
|
if (!e.features || e.features.length === 0) return;
|
|
|
|
const feature = e.features[0];
|
|
// Get asset ID from properties (more reliable) or fallback to feature.id
|
|
const assetId = feature.properties?.id || feature.properties?.asset_id || feature.id;
|
|
|
|
// Check if we're in draw mode
|
|
const isDrawMode = drawToolbar?.isActive();
|
|
|
|
if (isDrawMode) {
|
|
// In draw mode, let CustomDrawToolbar handle clicks
|
|
// The toolbar will add features to draw control
|
|
return;
|
|
}
|
|
|
|
// Normal mode: toggle selection
|
|
if (selectedAssets.has(assetId)) {
|
|
selectedAssets.delete(assetId);
|
|
map.setFeatureState(
|
|
{ source: 'assets', id: assetId },
|
|
{ selected: false }
|
|
);
|
|
} else {
|
|
selectedAssets.add(assetId);
|
|
map.setFeatureState(
|
|
{ source: 'assets', id: assetId },
|
|
{ selected: true }
|
|
);
|
|
}
|
|
|
|
updateSelectionUI();
|
|
}
|
|
|
|
// =============================
|
|
// Attribute Panel (Draw Mode)
|
|
// =============================
|
|
|
|
function showAttributePanel(feature) {
|
|
if (!feature) return;
|
|
|
|
const panel = document.getElementById('attribute-panel');
|
|
const featureIdSpan = document.getElementById('edit-feature-id');
|
|
|
|
currentEditFeatureId = feature.id;
|
|
featureIdSpan.textContent = feature.id;
|
|
|
|
// Get model from properties
|
|
const modelId = feature.properties?.model_id || '';
|
|
setModelValue(modelId);
|
|
enableModelBadges(true);
|
|
|
|
// Get bulb types from properties
|
|
// bulb_types may arrive as a JSON string when read from MapLibre feature properties
|
|
let bulbTypes = feature.properties?.bulb_types || [];
|
|
if (typeof bulbTypes === 'string') {
|
|
try { bulbTypes = JSON.parse(bulbTypes); } catch(e) { bulbTypes = []; }
|
|
}
|
|
setBulbTypes(bulbTypes);
|
|
enableBulbBadges(true);
|
|
|
|
document.getElementById('btn-save').disabled = false;
|
|
|
|
panel.style.display = 'block';
|
|
}
|
|
|
|
function hideAttributePanel() {
|
|
const panel = document.getElementById('attribute-panel');
|
|
panel.style.display = 'none';
|
|
currentEditFeatureId = null;
|
|
setBulbTypes([]);
|
|
enableBulbBadges(false);
|
|
}
|
|
|
|
function showMultiSelectInfo(features) {
|
|
const panel = document.getElementById('attribute-panel');
|
|
const featureIdSpan = document.getElementById('edit-feature-id');
|
|
|
|
currentEditFeatureId = null;
|
|
featureIdSpan.textContent = `{% translate "Sélection multiple" %} (${features.length})`;
|
|
|
|
enableModelBadges(true);
|
|
enableBulbBadges(true);
|
|
document.getElementById('btn-save').disabled = false;
|
|
|
|
panel.style.display = 'block';
|
|
}
|
|
|
|
// =============================
|
|
// Save Functions
|
|
// =============================
|
|
|
|
const handleSaveGeometries = function() {
|
|
if (!drawToolbar || drawToolbar.modifiedGeometries.size === 0) {
|
|
showMessage('{% translate "Aucune modification à enregistrer" %}', 'info');
|
|
return;
|
|
}
|
|
|
|
// Prepare data for saving
|
|
const modifications = [];
|
|
const deletions = [];
|
|
|
|
Array.from(drawToolbar.modifiedGeometries.values()).forEach(mod => {
|
|
if (mod.currentGeometry === null) {
|
|
deletions.push({
|
|
asset_id: mod.assetId,
|
|
asset_type: mod.assetType
|
|
});
|
|
} else {
|
|
const modification = {
|
|
asset_id: mod.assetId,
|
|
asset_type: mod.assetType,
|
|
geometry: mod.currentGeometry
|
|
};
|
|
|
|
if (mod.modelId) {
|
|
modification.model_id = mod.modelId;
|
|
}
|
|
|
|
if (mod.properties) {
|
|
modification.properties = mod.properties;
|
|
}
|
|
|
|
if (mod.bulbTypes !== undefined) {
|
|
modification.bulb_types = mod.bulbTypes;
|
|
}
|
|
|
|
modifications.push(modification);
|
|
}
|
|
});
|
|
|
|
toggleLoading(true);
|
|
|
|
fetch('{% url "assets:nature_save_geometries" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
modifications: modifications,
|
|
deletions: deletions
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
toggleLoading(false);
|
|
if (data.success) {
|
|
let message = '{% translate "Géométries enregistrées avec succès" %}';
|
|
if (data.updated_count > 0) {
|
|
message += ` - ${data.updated_count} {% translate "modifié(s)" %}`;
|
|
}
|
|
if (data.deleted_count > 0) {
|
|
message += ` - ${data.deleted_count} {% translate "supprimé(s)" %}`;
|
|
}
|
|
showMessage(message, 'success');
|
|
|
|
// Handle new asset IDs
|
|
if (data.id_mapping) {
|
|
Object.entries(data.id_mapping).forEach(([oldId, newId]) => {
|
|
if (drawToolbar.loadedAssets.has(oldId)) {
|
|
const asset = drawToolbar.loadedAssets.get(oldId);
|
|
asset.id = newId;
|
|
if (asset.properties) asset.properties.id = newId;
|
|
drawToolbar.loadedAssets.delete(oldId);
|
|
drawToolbar.loadedAssets.set(newId, asset);
|
|
}
|
|
|
|
const drawFeature = draw.get(oldId);
|
|
if (drawFeature) {
|
|
draw.add({
|
|
...drawFeature,
|
|
id: newId
|
|
});
|
|
draw.delete(oldId);
|
|
}
|
|
});
|
|
}
|
|
|
|
drawToolbar.modifiedGeometries.clear();
|
|
drawToolbar._updateModifiedCount();
|
|
drawToolbar.geometryHistory = [];
|
|
|
|
loadAssetsIfNeeded();
|
|
updateStatusDisplay();
|
|
} else {
|
|
showMessage(data.error || '{% translate "Erreur lors de la sauvegarde" %}', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
toggleLoading(false);
|
|
showMessage('{% translate "Erreur lors de la sauvegarde des géométries" %}', 'danger');
|
|
});
|
|
};
|
|
|
|
function handleBulkSave() {
|
|
const modelId = document.getElementById('model-select').value;
|
|
|
|
// Check if we're in draw mode with a current feature
|
|
if (drawToolbar?.isActive() && currentEditFeatureId) {
|
|
// Update the model for the feature being edited
|
|
if (modelId && drawToolbar) {
|
|
const mod = drawToolbar.modifiedGeometries.get(currentEditFeatureId);
|
|
if (mod) {
|
|
mod.modelId = modelId;
|
|
mod.bulbTypes = getBulbTypesArray();
|
|
}
|
|
|
|
// Also update loaded asset properties
|
|
const asset = drawToolbar.loadedAssets.get(currentEditFeatureId);
|
|
if (asset) {
|
|
if (!asset.properties) asset.properties = {};
|
|
asset.properties.model_id = modelId;
|
|
asset.properties.bulb_types = getBulbTypesArray();
|
|
}
|
|
}
|
|
|
|
// Save geometries
|
|
handleSaveGeometries();
|
|
return;
|
|
}
|
|
|
|
// Normal mode: bulk update selected assets
|
|
if (selectedAssets.size === 0) {
|
|
showMessage('{% translate "Aucun élément sélectionné" %}', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (!modelId && selectedBulbTypes.size === 0) {
|
|
showMessage('{% translate "Veuillez sélectionner un modèle ou des types de bulbes" %}', 'warning');
|
|
return;
|
|
}
|
|
|
|
const assetIds = Array.from(selectedAssets);
|
|
|
|
toggleLoading(true);
|
|
|
|
fetch('{% url "assets:nature_bulk_update_assets" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
asset_ids: assetIds,
|
|
category_id: null,
|
|
model_id: modelId || null,
|
|
bulb_types: getBulbTypesArray()
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
toggleLoading(false);
|
|
if (data.success) {
|
|
showMessage('{% translate "Modifications enregistrées avec succès" %}', 'success');
|
|
clearSelection();
|
|
|
|
// Force reload assets from server to get updated colors
|
|
// Clear loadedAssets to force a fresh load
|
|
if (drawToolbar) {
|
|
drawToolbar.loadedAssets.clear();
|
|
}
|
|
forceReloadAssets();
|
|
} else {
|
|
showMessage(data.error || '{% translate "Erreur lors de la sauvegarde" %}', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
toggleLoading(false);
|
|
showMessage('{% translate "Erreur lors de la sauvegarde" %}', 'danger');
|
|
});
|
|
}
|
|
|
|
// =============================
|
|
// Map Initialization
|
|
// =============================
|
|
|
|
function initMap() {
|
|
map = new maplibregl.Map({
|
|
container: 'map',
|
|
style: window.MAP_STYLE_STANDARD,
|
|
center: [4.35, 50.85], // Brussels
|
|
zoom: 13,
|
|
attributionControl: true
|
|
});
|
|
|
|
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
|
|
|
const geolocate = new maplibregl.GeolocateControl({
|
|
positionOptions: {
|
|
enableHighAccuracy: true
|
|
},
|
|
trackUserLocation: true,
|
|
showAccuracyCircle: true
|
|
});
|
|
map.addControl(geolocate, 'top-left');
|
|
|
|
geolocate.on('geolocate', function(e) {
|
|
currentLocation = {
|
|
lat: e.coords.latitude,
|
|
lng: e.coords.longitude
|
|
};
|
|
});
|
|
|
|
const drawStyles = buildDrawStyles();
|
|
|
|
draw = new CustomDraw({
|
|
displayControlsDefault: false,
|
|
controls: {},
|
|
defaultMode: 'edit',
|
|
snap: true,
|
|
snapPx: 15,
|
|
snapToMidPoints: true,
|
|
snapVertexPriorityDistance: 0.0005,
|
|
guides: true,
|
|
snapOptions: {
|
|
snapPx: 15,
|
|
snapToMidPoints: true,
|
|
snapVertexPriorityDistance: 0.0005,
|
|
snapSelfEnabled: true
|
|
},
|
|
styles: drawStyles,
|
|
userProperties: true
|
|
});
|
|
|
|
map.on('load', function() {
|
|
map.addControl(draw, 'top-left');
|
|
|
|
drawToolbar = new CustomDrawToolbar({
|
|
draw: draw,
|
|
layerConfig: {
|
|
sourceId: 'assets',
|
|
layerIds: ['green-surfaces', 'green-surfaces-outline'],
|
|
assetType: 'naturegreensurface'
|
|
},
|
|
api: {
|
|
loadUrl: '/api/nature/assets/',
|
|
saveUrl: '/api/nature/assets/bulk-save/',
|
|
deleteUrl: '/api/nature/assets/bulk-delete/',
|
|
getCsrfToken: () => document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
|
},
|
|
labels: {
|
|
drawMode: '{% translate "Mode dessin" %}',
|
|
addPolygon: '{% translate "Ajouter un polygone" %}',
|
|
deleteSelected: '{% translate "Supprimer les géométries sélectionnées" %}',
|
|
snap: '{% translate "Aimantage (Snap)" %}',
|
|
snapEnabled: '{% translate "Aimantage activé" %}',
|
|
snapDisabled: '{% translate "Aimantage désactivé" %}',
|
|
split: '{% translate "Découper un polygone" %}',
|
|
splitCancelled: '{% translate "Découpage annulé" %}',
|
|
splitSuccess: '{% translate "Polygone découpé avec succès" %}',
|
|
merge: '{% translate "Fusionner les polygones sélectionnés" %}',
|
|
mergeSuccess: '{% translate "Polygones fusionnés avec succès" %}',
|
|
saveGeometries: '{% translate "Enregistrer les modifications" %}',
|
|
undo: '{% translate "Annuler" %}',
|
|
noFeaturesToDelete: '{% translate "Aucune géométrie à supprimer" %}',
|
|
geometriesDeleted: '{% translate "Géométrie(s) supprimée(s)" %}',
|
|
drawModeEnabled: "{% translate "Mode dessin activé - Cliquez sur une géométrie pour l'éditer" %}",
|
|
drawModeDisabled: '{% translate "Mode dessin désactivé" %}',
|
|
addPolygonHelp: '{% translate "Cliquez pour ajouter des points. Double-cliquez ou cliquez sur le premier point pour terminer." %}',
|
|
splitHelp: '{% translate "Tracez une ligne pour découper le polygone. Double-cliquez pour terminer." %}'
|
|
},
|
|
callbacks: {
|
|
onSave: handleSaveGeometries,
|
|
onDrawModeChange: function(isActive) {
|
|
if (isActive) {
|
|
map.doubleClickZoom.disable();
|
|
// Clear normal selection when entering draw mode
|
|
clearSelection();
|
|
document.getElementById('selection-info').style.display = 'none';
|
|
} else {
|
|
map.doubleClickZoom.enable();
|
|
hideAttributePanel();
|
|
enableModelBadges(false);
|
|
|
|
// When exiting draw mode, remove all features from draw control
|
|
const drawFeatures = draw.getAll().features;
|
|
drawFeatures.forEach(feature => {
|
|
const featureId = feature.id;
|
|
draw.delete(featureId);
|
|
// Show the feature back on the map
|
|
if (drawToolbar && drawToolbar.hiddenFeatures) {
|
|
drawToolbar.hiddenFeatures.delete(featureId);
|
|
}
|
|
});
|
|
|
|
// Reset modified geometries counters
|
|
if (drawToolbar) {
|
|
drawToolbar.modifiedGeometries.clear();
|
|
drawToolbar.geometryHistory = [];
|
|
drawToolbar._updateModifiedCount();
|
|
drawToolbar._updateFloatingButtons();
|
|
}
|
|
|
|
// Cancel unsaved modifications by reloading assets from server
|
|
// Clear loadedAssets to force a fresh load
|
|
if (drawToolbar) {
|
|
drawToolbar.loadedAssets.clear();
|
|
}
|
|
forceReloadAssets();
|
|
}
|
|
updateStatusDisplay();
|
|
},
|
|
onFeatureEdit: function(feature) {
|
|
showAttributePanel(feature);
|
|
},
|
|
onSelectionChange: function(selectedFeatures, selectedIds) {
|
|
if (selectedFeatures.length === 1) {
|
|
showAttributePanel(selectedFeatures[0]);
|
|
} else if (selectedFeatures.length === 0) {
|
|
hideAttributePanel();
|
|
enableModelBadges(false);
|
|
} else {
|
|
showMultiSelectInfo(selectedFeatures);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
map.addControl(drawToolbar, 'top-left');
|
|
setupFloatingExitButton();
|
|
|
|
const layerSwitcher = new LayerSwitcherControl({
|
|
layers: [
|
|
{
|
|
id: 'ortho',
|
|
name: 'Orthophotos',
|
|
icon: 'satellite',
|
|
group: 'ortho',
|
|
layer: {
|
|
type: 'raster',
|
|
paint: {
|
|
'raster-opacity': 1
|
|
}
|
|
},
|
|
sublayers: [
|
|
{
|
|
id: 'urbis',
|
|
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
|
|
default: true,
|
|
source: {
|
|
type: 'raster',
|
|
tiles: [
|
|
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
|
|
],
|
|
tileSize: 256,
|
|
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS'
|
|
}
|
|
},
|
|
{
|
|
id: 'vlaanderen',
|
|
name: 'Vlaanderen',
|
|
source: {
|
|
type: 'raster',
|
|
tiles: [ORTHO_WMTS_URL],
|
|
tileSize: 256,
|
|
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'assets-visibility',
|
|
name: '{% translate "Espaces verts" %}',
|
|
icon: 'bi:flower1',
|
|
group: 'data',
|
|
exclusive: false,
|
|
existingLayerIds: ['green-surfaces', 'green-surfaces-outline'],
|
|
visible: true
|
|
},
|
|
{
|
|
id: 'trees-visibility',
|
|
name: '{% translate "Arbres" %}',
|
|
icon: 'bi:tree',
|
|
group: 'data',
|
|
exclusive: false,
|
|
existingLayerIds: ['trees', 'trees-center'],
|
|
visible: true
|
|
}
|
|
]
|
|
});
|
|
map.addControl(layerSwitcher, 'top-left');
|
|
|
|
map.addSource('assets', {
|
|
type: 'geojson',
|
|
data: {
|
|
type: 'FeatureCollection',
|
|
features: []
|
|
},
|
|
promoteId: 'id' // Use the 'id' property from features as the feature ID
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'green-surfaces',
|
|
type: 'fill',
|
|
source: 'assets',
|
|
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
|
paint: {
|
|
'fill-color': ['get', 'color'],
|
|
'fill-opacity': [
|
|
'case',
|
|
['boolean', ['feature-state', 'selected'], false],
|
|
0.7,
|
|
0.5
|
|
]
|
|
}
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'green-surfaces-outline',
|
|
type: 'line',
|
|
source: 'assets',
|
|
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
|
paint: {
|
|
'line-color': [
|
|
'case',
|
|
['boolean', ['feature-state', 'selected'], false],
|
|
'#ff0000',
|
|
'#000000'
|
|
],
|
|
'line-width': [
|
|
'case',
|
|
['boolean', ['feature-state', 'selected'], false],
|
|
3,
|
|
1
|
|
]
|
|
}
|
|
});
|
|
|
|
// Trees layer: green circles with black border
|
|
map.addLayer({
|
|
id: 'trees',
|
|
type: 'circle',
|
|
source: 'assets',
|
|
filter: ['==', ['get', 'asset_type'], 'naturetree'],
|
|
paint: {
|
|
'circle-radius': [
|
|
'interpolate', ['linear'], ['zoom'],
|
|
15, 1.5,
|
|
17, 2.5,
|
|
19, 4
|
|
],
|
|
'circle-color': '#4CAF50',
|
|
'circle-stroke-width': [
|
|
'interpolate', ['linear'], ['zoom'],
|
|
15, 0.25,
|
|
17, 0.5,
|
|
19, 0.75
|
|
],
|
|
'circle-stroke-color': '#222222'
|
|
}
|
|
});
|
|
|
|
// Trees center dot
|
|
map.addLayer({
|
|
id: 'trees-center',
|
|
type: 'circle',
|
|
source: 'assets',
|
|
filter: ['==', ['get', 'asset_type'], 'naturetree'],
|
|
paint: {
|
|
'circle-radius': [
|
|
'interpolate', ['linear'], ['zoom'],
|
|
15, 0.5,
|
|
17, 0.75,
|
|
19, 1.25
|
|
],
|
|
'circle-color': '#111111',
|
|
'circle-stroke-width': 0
|
|
}
|
|
});
|
|
|
|
loadAssetsIfNeeded();
|
|
geolocate.trigger();
|
|
|
|
// Initialize comments component
|
|
const commentsComponent = new GreenSurfacesComments(map, {
|
|
apiBaseUrl: '{% url "assets:green_surfaces_comments_geojson" %}'.replace('/geojson/', '')
|
|
});
|
|
|
|
// Make it globally accessible for debugging
|
|
window.commentsComponent = commentsComponent;
|
|
|
|
setInterval(updateStatusDisplay, 1000);
|
|
});
|
|
|
|
// Handle clicks on assets for selection (normal mode)
|
|
map.on('click', 'green-surfaces', handleAssetClick);
|
|
|
|
// Change cursor on hover
|
|
map.on('mouseenter', 'green-surfaces', function() {
|
|
if (!drawToolbar?.isActive()) {
|
|
map.getCanvas().style.cursor = 'pointer';
|
|
}
|
|
});
|
|
map.on('mouseleave', 'green-surfaces', function() {
|
|
if (!drawToolbar?.isActive()) {
|
|
map.getCanvas().style.cursor = '';
|
|
}
|
|
});
|
|
|
|
map.on('moveend', function() {
|
|
loadAssetsIfNeeded();
|
|
});
|
|
|
|
// Setup panel toggle
|
|
setupPanelToggle();
|
|
|
|
// Setup model badge clicks
|
|
setupModelBadges();
|
|
|
|
// Setup action buttons
|
|
setupActionButtons();
|
|
}
|
|
|
|
function buildDrawStyles() {
|
|
return [
|
|
{
|
|
'id': 'polygon-fill-inactive',
|
|
'type': 'fill',
|
|
'filter': ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
|
|
'paint': {
|
|
'fill-color': ['coalesce', ['get', 'user_color'], '#3bb2d0'],
|
|
'fill-outline-color': ['coalesce', ['get', 'user_color'], '#3bb2d0'],
|
|
'fill-opacity': 0.5
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-fill-active',
|
|
'type': 'fill',
|
|
'filter': ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
|
|
'paint': {
|
|
'fill-color': '#0066ff',
|
|
'fill-outline-color': '#0066ff',
|
|
'fill-opacity': 0.5
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-stroke-inactive',
|
|
'type': 'line',
|
|
'filter': ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
|
|
'layout': {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
'paint': {
|
|
'line-color': ['coalesce', ['get', 'user_color'], '#3bb2d0'],
|
|
'line-width': 2
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-stroke-active',
|
|
'type': 'line',
|
|
'filter': ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
|
|
'layout': {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
'paint': {
|
|
'line-color': '#0066ff',
|
|
'line-dasharray': ['literal', [0.2, 2]],
|
|
'line-width': 2
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-and-line-vertex-halo-active',
|
|
'type': 'circle',
|
|
'filter': ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
|
'paint': {
|
|
'circle-radius': 5,
|
|
'circle-color': '#FFF'
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-and-line-vertex-active',
|
|
'type': 'circle',
|
|
'filter': ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
|
'paint': {
|
|
'circle-radius': 3,
|
|
'circle-color': '#0066ff'
|
|
}
|
|
},
|
|
{
|
|
'id': 'polygon-midpoint',
|
|
'type': 'circle',
|
|
'filter': ['all', ['==', 'meta', 'midpoint'], ['==', '$type', 'Point']],
|
|
'paint': {
|
|
'circle-radius': 3,
|
|
'circle-color': '#3bb2d0'
|
|
}
|
|
},
|
|
{
|
|
'id': 'gl-draw-line-inactive',
|
|
'type': 'line',
|
|
'filter': ['all', ['==', 'active', 'false'], ['==', '$type', 'LineString'], ['!=', 'mode', 'static']],
|
|
'layout': {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
'paint': {
|
|
'line-color': '#3bb2d0',
|
|
'line-width': 2
|
|
}
|
|
},
|
|
{
|
|
'id': 'gl-draw-line-active',
|
|
'type': 'line',
|
|
'filter': ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']],
|
|
'layout': {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
'paint': {
|
|
'line-color': '#ff6600',
|
|
'line-dasharray': ['literal', [0.2, 2]],
|
|
'line-width': 3
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Force reload assets from server (clears cache and reloads)
|
|
* Used after save operations to get updated colors
|
|
*/
|
|
function forceReloadAssets() {
|
|
if (!map) return;
|
|
|
|
const bounds = map.getBounds();
|
|
const sw = bounds.getSouthWest();
|
|
const ne = bounds.getNorthEast();
|
|
|
|
const bufferedBounds = {
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [[
|
|
[sw.lng - BUFFER_SIZE, sw.lat - BUFFER_SIZE],
|
|
[ne.lng + BUFFER_SIZE, sw.lat - BUFFER_SIZE],
|
|
[ne.lng + BUFFER_SIZE, ne.lat + BUFFER_SIZE],
|
|
[sw.lng - BUFFER_SIZE, ne.lat + BUFFER_SIZE],
|
|
[sw.lng - BUFFER_SIZE, sw.lat - BUFFER_SIZE]
|
|
]]
|
|
}
|
|
};
|
|
|
|
fetch('{% url "assets:nature_assets_geojson" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
|
},
|
|
body: JSON.stringify({
|
|
filter_geom: bufferedBounds
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const features = (data.features || []).map(feature => {
|
|
const properties = {
|
|
...feature.properties,
|
|
id: feature.id,
|
|
asset_id: feature.id
|
|
};
|
|
return {
|
|
...feature,
|
|
properties
|
|
};
|
|
});
|
|
|
|
// Clear and reload all assets
|
|
if (drawToolbar) {
|
|
drawToolbar.loadedAssets.clear();
|
|
features.forEach(feature => {
|
|
drawToolbar.loadedAssets.set(feature.id, {
|
|
id: feature.id,
|
|
type: 'Feature',
|
|
geometry: feature.geometry,
|
|
properties: feature.properties || {}
|
|
});
|
|
});
|
|
drawToolbar._updateMapSource();
|
|
} else {
|
|
if (map.getSource('assets')) {
|
|
map.getSource('assets').setData({
|
|
type: 'FeatureCollection',
|
|
features: features
|
|
});
|
|
}
|
|
}
|
|
|
|
updateStatusDisplay();
|
|
})
|
|
.catch(error => {
|
|
showMessage('{% translate "Erreur lors du rechargement des données" %}', 'danger');
|
|
});
|
|
}
|
|
|
|
function loadAssetsIfNeeded() {
|
|
if (!map || map.getZoom() < MIN_ZOOM_FOR_ASSETS) {
|
|
return;
|
|
}
|
|
|
|
const bounds = map.getBounds();
|
|
const sw = bounds.getSouthWest();
|
|
const ne = bounds.getNorthEast();
|
|
|
|
const bufferedBounds = {
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [[
|
|
[sw.lng - BUFFER_SIZE, sw.lat - BUFFER_SIZE],
|
|
[ne.lng + BUFFER_SIZE, sw.lat - BUFFER_SIZE],
|
|
[ne.lng + BUFFER_SIZE, ne.lat + BUFFER_SIZE],
|
|
[sw.lng - BUFFER_SIZE, ne.lat + BUFFER_SIZE],
|
|
[sw.lng - BUFFER_SIZE, sw.lat - BUFFER_SIZE]
|
|
]]
|
|
}
|
|
};
|
|
|
|
fetch('{% url "assets:nature_assets_geojson" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
|
},
|
|
body: JSON.stringify({
|
|
filter_geom: bufferedBounds
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const features = (data.features || []).map(feature => {
|
|
const properties = {
|
|
...feature.properties,
|
|
id: feature.id,
|
|
asset_id: feature.id
|
|
};
|
|
return {
|
|
...feature,
|
|
properties
|
|
};
|
|
});
|
|
|
|
if (drawToolbar && features) {
|
|
features.forEach(feature => {
|
|
if (!drawToolbar.loadedAssets.has(feature.id)) {
|
|
drawToolbar.loadedAssets.set(feature.id, {
|
|
id: feature.id,
|
|
type: 'Feature',
|
|
geometry: feature.geometry,
|
|
properties: feature.properties || {}
|
|
});
|
|
}
|
|
});
|
|
|
|
drawToolbar._updateMapSource();
|
|
} else {
|
|
if (map.getSource('assets')) {
|
|
map.getSource('assets').setData({
|
|
type: 'FeatureCollection',
|
|
features: features
|
|
});
|
|
}
|
|
}
|
|
|
|
updateStatusDisplay();
|
|
})
|
|
.catch(error => {
|
|
showMessage('{% translate "Erreur lors du chargement des données" %}', 'danger');
|
|
});
|
|
}
|
|
|
|
function updateStatusDisplay() {
|
|
const drawModeStatus = document.getElementById('draw-mode-status');
|
|
if (drawToolbar) {
|
|
const isActive = drawToolbar.isActive();
|
|
drawModeStatus.textContent = isActive ? '{% translate "Actif" %}' : '{% translate "Inactif" %}';
|
|
drawModeStatus.className = 'badge ' + (isActive ? 'bg-success' : 'bg-secondary');
|
|
}
|
|
|
|
const modifiedCount = document.getElementById('modified-count');
|
|
const modifiedInfo = document.getElementById('modified-info');
|
|
const modifiedAssetsCount = document.getElementById('modified-assets-count');
|
|
if (drawToolbar) {
|
|
const count = drawToolbar.modifiedGeometries.size;
|
|
modifiedCount.textContent = count;
|
|
modifiedCount.className = 'badge ' + (count > 0 ? 'bg-warning text-dark' : 'bg-secondary');
|
|
modifiedAssetsCount.textContent = count;
|
|
modifiedInfo.style.display = count > 0 ? 'block' : 'none';
|
|
}
|
|
|
|
const loadedCount = document.getElementById('loaded-count');
|
|
if (drawToolbar) {
|
|
loadedCount.textContent = drawToolbar.loadedAssets.size;
|
|
}
|
|
|
|
const drawCount = document.getElementById('draw-count');
|
|
if (draw) {
|
|
drawCount.textContent = draw.getAll().features.length;
|
|
}
|
|
}
|
|
|
|
function setupPanelToggle() {
|
|
const panelToggle = document.getElementById('panel-toggle');
|
|
panelToggle.addEventListener('click', function() {
|
|
const panel = document.getElementById('attributes-panel');
|
|
const icon = document.getElementById('toggle-icon');
|
|
|
|
panel.classList.toggle('collapsed');
|
|
|
|
if (panel.classList.contains('collapsed')) {
|
|
icon.className = 'bi bi-chevron-up';
|
|
} else {
|
|
icon.className = 'bi bi-chevron-down';
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupModelBadges() {
|
|
document.querySelectorAll('.model-badge:not(.bulb-badge)').forEach(badge => {
|
|
badge.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (this.classList.contains('disabled')) return;
|
|
|
|
const value = this.dataset.value;
|
|
setModelValue(value);
|
|
|
|
// If in draw mode, update the model for all selected features
|
|
if (drawToolbar?.isActive()) {
|
|
// Get the color for the selected model
|
|
const modelColor = this.dataset.color || '#3bb2d0';
|
|
|
|
// Get all features currently in draw control
|
|
const allDrawFeatures = drawToolbar.draw.getAll().features;
|
|
|
|
// Update model for all features in draw control
|
|
allDrawFeatures.forEach(feature => {
|
|
const featureId = feature.id;
|
|
|
|
// Get or create modification entry
|
|
let mod = drawToolbar.modifiedGeometries.get(featureId);
|
|
if (!mod) {
|
|
// Feature exists but hasn't been modified yet - create entry
|
|
const asset = drawToolbar.loadedAssets.get(featureId);
|
|
mod = {
|
|
assetId: featureId,
|
|
assetType: drawToolbar.layerConfig.assetType,
|
|
originalGeometry: asset?.geometry || feature.geometry,
|
|
currentGeometry: feature.geometry,
|
|
isNew: feature.properties?.isNew || false
|
|
};
|
|
drawToolbar.modifiedGeometries.set(featureId, mod);
|
|
}
|
|
|
|
// Update model
|
|
mod.modelId = value;
|
|
if (!mod.properties) mod.properties = {};
|
|
mod.properties.model_id = value;
|
|
mod.properties.color = modelColor;
|
|
|
|
// Also update loadedAssets
|
|
const asset = drawToolbar.loadedAssets.get(featureId);
|
|
if (asset) {
|
|
if (!asset.properties) asset.properties = {};
|
|
asset.properties.model_id = value;
|
|
asset.properties.color = modelColor;
|
|
}
|
|
|
|
// Update draw feature properties with color
|
|
feature.properties = feature.properties || {};
|
|
feature.properties.model_id = value;
|
|
feature.properties.color = modelColor;
|
|
|
|
// Update the feature in draw control to refresh the display
|
|
draw.add({
|
|
id: featureId,
|
|
type: 'Feature',
|
|
geometry: feature.geometry,
|
|
properties: feature.properties
|
|
});
|
|
});
|
|
|
|
// Update the modified count display
|
|
if (drawToolbar._updateModifiedCount) {
|
|
drawToolbar._updateModifiedCount();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupBulbBadges() {
|
|
// Toggle panel visibility
|
|
document.getElementById('btn-toggle-bulbs')?.addEventListener('click', function() {
|
|
const panel = document.getElementById('bulb-types-panel');
|
|
const icon = document.getElementById('bulbs-toggle-icon');
|
|
if (panel.style.display === 'none') {
|
|
panel.style.display = '';
|
|
icon.className = 'bi bi-chevron-up';
|
|
} else {
|
|
panel.style.display = 'none';
|
|
icon.className = 'bi bi-chevron-down';
|
|
}
|
|
});
|
|
|
|
// Badge click: toggle selection (multi-select)
|
|
document.querySelectorAll('.bulb-badge').forEach(badge => {
|
|
badge.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (this.classList.contains('disabled')) return;
|
|
|
|
const value = this.dataset.value;
|
|
if (selectedBulbTypes.has(value)) {
|
|
selectedBulbTypes.delete(value);
|
|
} else {
|
|
selectedBulbTypes.add(value);
|
|
}
|
|
this.classList.toggle('active', selectedBulbTypes.has(value));
|
|
|
|
const input = document.getElementById('bulb-types-input');
|
|
if (input) input.value = JSON.stringify(Array.from(selectedBulbTypes));
|
|
|
|
// In draw mode, propagate to all features currently in draw control
|
|
if (drawToolbar?.isActive()) {
|
|
const bulbTypesArray = Array.from(selectedBulbTypes);
|
|
const allDrawFeatures = drawToolbar.draw.getAll().features;
|
|
allDrawFeatures.forEach(feature => {
|
|
const featureId = feature.id;
|
|
let mod = drawToolbar.modifiedGeometries.get(featureId);
|
|
if (!mod) {
|
|
const asset = drawToolbar.loadedAssets.get(featureId);
|
|
mod = {
|
|
assetId: featureId,
|
|
assetType: drawToolbar.layerConfig.assetType,
|
|
originalGeometry: asset?.geometry || feature.geometry,
|
|
currentGeometry: feature.geometry,
|
|
isNew: feature.properties?.isNew || false
|
|
};
|
|
drawToolbar.modifiedGeometries.set(featureId, mod);
|
|
}
|
|
mod.bulbTypes = bulbTypesArray;
|
|
const asset = drawToolbar.loadedAssets.get(featureId);
|
|
if (asset) {
|
|
if (!asset.properties) asset.properties = {};
|
|
asset.properties.bulb_types = bulbTypesArray;
|
|
}
|
|
});
|
|
if (drawToolbar._updateModifiedCount) drawToolbar._updateModifiedCount();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupActionButtons() {
|
|
document.getElementById('btn-save').addEventListener('click', handleBulkSave);
|
|
document.getElementById('btn-clear-selection').addEventListener('click', function() {
|
|
clearSelection();
|
|
hideAttributePanel();
|
|
});
|
|
setupBulbBadges();
|
|
}
|
|
|
|
function setupFloatingExitButton() {
|
|
if (!drawToolbar || !drawToolbar.floatingActionsContainer || !drawToolbar.saveBtn) return;
|
|
|
|
const container = drawToolbar.floatingActionsContainer;
|
|
let row = container.querySelector('.draw-floating-actions-row');
|
|
|
|
if (!row) {
|
|
row = document.createElement('div');
|
|
row.className = 'draw-floating-actions-row';
|
|
container.insertBefore(row, drawToolbar.saveBtn);
|
|
row.appendChild(drawToolbar.saveBtn);
|
|
}
|
|
|
|
if (!row.querySelector('.draw-floating-btn-exit')) {
|
|
const exitBtn = document.createElement('button');
|
|
exitBtn.type = 'button';
|
|
exitBtn.className = 'draw-floating-btn draw-floating-btn-exit';
|
|
exitBtn.title = '{% translate "Quitter" %}';
|
|
exitBtn.innerHTML = `<span class="label">{% translate "Quitter" %}</span>`;
|
|
exitBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (drawToolbar.isActive()) {
|
|
drawToolbar.toggleDrawMode();
|
|
}
|
|
});
|
|
|
|
row.insertBefore(exitBtn, drawToolbar.saveBtn);
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', initMap);
|
|
|
|
})();
|
|
</script>
|
|
{% endblock extra_scripts %}
|