419 lines
No EOL
16 KiB
HTML
419 lines
No EOL
16 KiB
HTML
{% extends "assets/base_assets_index.html" %}
|
|
{% load static i18n %}
|
|
|
|
|
|
{% block locations_filter_form %}
|
|
<form method="get" id="filter-form" class="row g-3 mb-3 me-3" style="width: 41vw;">
|
|
<div class="col-md-4">
|
|
<label for="status" class="form-label">{% translate "Statut" %}</label>
|
|
<select name="status" id="status" class="form-select" multiple>
|
|
{% for key, value in status_choices %}
|
|
<option value="{{ key }}" {% if key in selected_statuses %}selected{% endif %}>
|
|
{{ value }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="voltage" class="form-label">{% translate "Voltage" %}</label>
|
|
<select name="voltage" id="voltage" class="form-select" multiple>
|
|
{% for key, value in voltage_choices %}
|
|
<option value="{{ key }}" {% if key in selected_voltages %}selected{% endif %}>
|
|
{{ value }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="begin_date" class="form-label">{% translate "Date de mise en service (à partir de)" %}</label>
|
|
<input type="date" name="begin_date" id="begin_date" class="form-control" value="{{ begin_date }}">
|
|
<br>
|
|
<label class="form-label">{% translate "Connexion à la centrale" %}</label>
|
|
<div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="radio" name="connected" id="connected_yes" value="yes" {% if connected == 'yes' %}checked{% endif %}>
|
|
<label class="form-check-label" for="connected_yes">{% translate "Oui" %}</label>
|
|
</div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="radio" name="connected" id="connected_no" value="no" {% if connected == 'no' %}checked{% endif %}>
|
|
<label class="form-check-label" for="connected_no">{% translate "Non" %}</label>
|
|
</div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="radio" name="connected" id="connected_all" value="" {% if not connected %}checked{% endif %}>
|
|
<label class="form-check-label" for="connected_all">{% translate "Tous" %}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if is_intern %}
|
|
<div class="col-md-4">
|
|
<label for="controller_maintenance_provider" class="form-label">{% translate "Prestataire maintenance contrôleurs" %}</label>
|
|
<select name="controller_maintenance_provider" id="controller_maintenance_provider" class="form-select">
|
|
<option value="">{% translate "Tous" %}</option>
|
|
{% for contract_id, contract_name in controller_maintenance_contracts %}
|
|
<option value="{{ contract_id }}" {% if controller_maintenance_provider == contract_id|stringformat:"s" %}selected{% endif %}>
|
|
{{ contract_name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
<div class="col-12">
|
|
<button type="button" id="apply-filters" class="btn btn-primary">{% translate "Filtrer" %}</button>
|
|
<button type="button" id="reset-filters" class="btn btn-secondary">{% translate "Réinitialiser" %}</button>
|
|
</div>
|
|
</form>
|
|
{% endblock locations_filter_form %}
|
|
|
|
|
|
|
|
{% block location_row_actions %}
|
|
{% if can_create_location %}
|
|
<button type="button" class="btn btn-sm btn-outline-primary" data-location-create
|
|
title="{% translate 'Créer une nouvelle intersection' %}">
|
|
<i class="bi bi-plus-circle"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% endblock location_row_actions %}
|
|
|
|
{% block assets_filter_form %}
|
|
<form method="get" id="assets-filter-form" class="row g-3 mb-3 me-3" style="width: 600px;">
|
|
<div class="col-6">
|
|
<label for="category" class="form-label">{% translate "Catégorie" %}</label>
|
|
<select name="category" id="category" class="form-select" multiple>
|
|
{% for key, value in category_choices %}
|
|
<option value="{{ key }}" {% if key in selected_categories %}selected{% endif %}>
|
|
{{ value }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-6">
|
|
<label for="model" class="form-label">{% translate "Modèle" %}</label>
|
|
<select name="model" id="model" class="form-select" multiple>
|
|
{% for key, value in model_choices %}
|
|
<option value="{{ key }}" {% if key in selected_models %}selected{% endif %}>
|
|
{{ value }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-12 d-flex justify-content-between mt-4" style="width: 200px;">
|
|
<button type="button" id="apply-assets-filters" class="btn btn-sm btn-primary">{% translate "Filtrer" %}</button>
|
|
<button type="button" id="reset-assets-filters" class="btn btn-sm btn-secondary">{% translate "Réinitialiser" %}</button>
|
|
</div>
|
|
</form>
|
|
{% endblock assets_filter_form %}
|
|
|
|
|
|
{% block const_script %}
|
|
<script>
|
|
const locationsgeojsonUrl = "{% url 'assets:trafficlights_intersections_geojson' %}";
|
|
const assetsgeojsonUrl = "{% url 'assets:trafficlights_assets_geojson' %}";
|
|
|
|
const locationsurl = "{% url 'assets:trafficlights_intersections' %}";
|
|
const assetsurl = "{% url 'assets:trafficlights_index' %}";
|
|
|
|
|
|
var locations_property_fields = ['name', 'voltage_type'];
|
|
var locations_columns = [
|
|
{ field: 'name', title: '{% translate "Nom" %}' },
|
|
{ field: 'voltage_type', title: '{% translate "Type" %}', align: 'center' }
|
|
];
|
|
|
|
var assets_property_fields = ['name', 'category', 'model'];
|
|
var assets_columns = [
|
|
{ field: 'name', title: '{% translate "Nom" %}' },
|
|
{ field: 'category', title: '{% translate "Catégorie" %}' },
|
|
{ field: 'model', title: '{% translate "Modèle" %}' }
|
|
]
|
|
|
|
|
|
const locations_layer_def = {
|
|
id: 'locations',
|
|
type: 'circle',
|
|
source: 'locations',
|
|
paint: {
|
|
'circle-radius': [
|
|
'interpolate',
|
|
['linear'],
|
|
['zoom'],
|
|
8, 1,
|
|
13, 4,
|
|
16, 7,
|
|
19, 10
|
|
],
|
|
'circle-stroke-color': [
|
|
'case',
|
|
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
|
'#00000000'
|
|
],
|
|
'circle-stroke-width': [
|
|
'case',
|
|
['boolean', ['feature-state', 'highlighted'], false], 3,
|
|
0
|
|
],
|
|
'circle-color': [
|
|
'match', ['get', 'status'],
|
|
'active', '#0094ff',
|
|
'in_preparation', '#f59f00',
|
|
'under_repair', '#f76707',
|
|
'voluntarily_out_of_service', '#e03131',
|
|
'inactive', '#e03131',
|
|
'archived', '#6c757d',
|
|
'#6c757d'
|
|
],
|
|
'circle-opacity': 0.7
|
|
},
|
|
minzoom: 7
|
|
};
|
|
|
|
const assets_layer_def = {
|
|
id: 'assets',
|
|
type: 'symbol',
|
|
source: 'assets',
|
|
layout: {
|
|
'icon-image': [
|
|
'match',
|
|
['get', 'asset_type'],
|
|
'trafficlightpole', 'traffic-light-icon',
|
|
'trafficlightradarpole', 'simple-pole-icon',
|
|
'trafficlightradar', 'radar-preventif-icon',
|
|
'trafficlightelectricalcabinet', 'electrical-panel-icon',
|
|
'trafficlightcontroller', 'electrical-panel-icon',
|
|
'trafficlightdetector', 'electrical-panel-icon',
|
|
'electrical-panel-icon' // default for non-pole trafficlights assets
|
|
],
|
|
'icon-size': [
|
|
'interpolate', ['linear'], ['zoom'],
|
|
8, 0.1,
|
|
14, 0.5,
|
|
22, 1
|
|
],
|
|
'icon-offset': [
|
|
'case',
|
|
['all', ['==', ['get', 'asset_type'], 'trafficlightradar'], ['coalesce', ['get', 'has_support'], false]], ['literal', [0, -25]],
|
|
['literal', [0, 0]]
|
|
],
|
|
'icon-allow-overlap': true,
|
|
'icon-ignore-placement': true,
|
|
'text-field': ['get', 'name'],
|
|
'text-font': ['Noto Sans Regular'],
|
|
'text-size': [
|
|
'interpolate',
|
|
['linear'],
|
|
['zoom'],
|
|
14, 8,
|
|
18, 11,
|
|
22, 14
|
|
],
|
|
'text-anchor': 'top',
|
|
'text-offset': [0, 1.5],
|
|
'text-allow-overlap': false,
|
|
'text-optional': true
|
|
},
|
|
paint: {
|
|
'icon-opacity': [
|
|
'case',
|
|
['boolean', ['feature-state', 'highlighted'], false], 1,
|
|
0.85
|
|
],
|
|
'text-color': '#333333',
|
|
'text-halo-color': 'rgba(255, 255, 255, 0.85)',
|
|
'text-halo-width': 3,
|
|
'text-halo-blur': 1
|
|
},
|
|
minzoom: 8
|
|
};
|
|
|
|
// Fallback circle layer for assets (shown before icon loads or if icon fails)
|
|
const assets_circle_layer_def = {
|
|
id: 'assets-circles',
|
|
type: 'circle',
|
|
source: 'assets',
|
|
paint: {
|
|
'circle-radius': [
|
|
'interpolate',
|
|
['linear'],
|
|
['zoom'],
|
|
8, 1,
|
|
22, 10
|
|
],
|
|
'circle-stroke-color': [
|
|
'case',
|
|
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
|
'#00000000'
|
|
],
|
|
'circle-stroke-width': [
|
|
'case',
|
|
['boolean', ['feature-state', 'highlighted'], false], 3,
|
|
0
|
|
],
|
|
'circle-color': '#0022ff',
|
|
'circle-opacity': 0.7
|
|
},
|
|
minzoom: 8
|
|
};
|
|
|
|
const assets_layers_defs = [assets_layer_def];
|
|
|
|
// Normalise an image URL onto a fixed-size canvas so icon-size is
|
|
// a true multiplier regardless of the source image's intrinsic dimensions.
|
|
function loadNormalisedIcon(url, size = 64) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
const ctx = canvas.getContext('2d');
|
|
const scale = Math.min(size / img.width, size / img.height);
|
|
const w = img.width * scale;
|
|
const h = img.height * scale;
|
|
ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
|
|
resolve(ctx.getImageData(0, 0, size, size));
|
|
};
|
|
img.onerror = () => reject(new Error('Failed to load icon: ' + url));
|
|
img.src = url;
|
|
});
|
|
}
|
|
|
|
// Load icons into the map
|
|
function loadMapIcons() {
|
|
// Load traffic light icon
|
|
if (!map.hasImage('traffic-light-icon')) {
|
|
loadNormalisedIcon("{% static 'assets/traffic_light_icon.svg' %}", 64).then(canvas => {
|
|
if (!map.hasImage('traffic-light-icon')) {
|
|
map.addImage('traffic-light-icon', canvas);
|
|
}
|
|
}).catch(e => console.warn(e));
|
|
}
|
|
|
|
// Load electrical panel icon
|
|
if (!map.hasImage('electrical-panel-icon')) {
|
|
loadNormalisedIcon("{% static 'assets/electrical-panel.png' %}", 32).then(canvas => {
|
|
if (!map.hasImage('electrical-panel-icon')) {
|
|
map.addImage('electrical-panel-icon', canvas);
|
|
}
|
|
}).catch(e => console.warn(e));
|
|
}
|
|
|
|
// Load prevention radar icon
|
|
if (!map.hasImage('radar-preventif-icon')) {
|
|
loadNormalisedIcon("{% static 'assets/radar_preventif_icon.svg' %}?v=20260710", 64).then(canvas => {
|
|
if (!map.hasImage('radar-preventif-icon')) {
|
|
map.addImage('radar-preventif-icon', canvas);
|
|
}
|
|
}).catch(e => console.warn(e));
|
|
}
|
|
|
|
// Load simple pole icon
|
|
if (!map.hasImage('simple-pole-icon')) {
|
|
loadNormalisedIcon("{% static 'assets/simple_pole_icon.svg' %}", 64).then(canvas => {
|
|
if (!map.hasImage('simple-pole-icon')) {
|
|
map.addImage('simple-pole-icon', canvas);
|
|
}
|
|
}).catch(e => console.warn(e));
|
|
}
|
|
|
|
// Register cross SDF icons for radar locations
|
|
const sz = 36;
|
|
const pad = 6;
|
|
if (!map.hasImage('cross-sdf')) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = sz; canvas.height = sz;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.clearRect(0, 0, sz, sz);
|
|
ctx.strokeStyle = '#000000';
|
|
ctx.lineWidth = 6;
|
|
ctx.lineCap = 'round';
|
|
ctx.beginPath();
|
|
// Horizontal
|
|
ctx.moveTo(pad, sz / 2);
|
|
ctx.lineTo(sz - pad, sz / 2);
|
|
// Vertical
|
|
ctx.moveTo(sz / 2, pad);
|
|
ctx.lineTo(sz / 2, sz - pad);
|
|
ctx.stroke();
|
|
map.addImage('cross-sdf', { width: sz, height: sz, data: ctx.getImageData(0, 0, sz, sz).data }, { sdf: true });
|
|
}
|
|
if (!map.hasImage('cross-sdf-border')) {
|
|
const bc = document.createElement('canvas');
|
|
bc.width = sz; bc.height = sz;
|
|
const bctx = bc.getContext('2d');
|
|
bctx.clearRect(0, 0, sz, sz);
|
|
bctx.strokeStyle = '#000000';
|
|
bctx.lineWidth = 12;
|
|
bctx.lineCap = 'round';
|
|
bctx.beginPath();
|
|
// Horizontal
|
|
bctx.moveTo(pad, sz / 2);
|
|
bctx.lineTo(sz - pad, sz / 2);
|
|
// Vertical
|
|
bctx.moveTo(sz / 2, pad);
|
|
bctx.lineTo(sz / 2, sz - pad);
|
|
bctx.stroke();
|
|
map.addImage('cross-sdf-border', { width: sz, height: sz, data: bctx.getImageData(0, 0, sz, sz).data }, { sdf: true });
|
|
}
|
|
}
|
|
|
|
|
|
const modelsByCategory = {{ category_model_map|safe }};
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const categorySelect = document.getElementById("category");
|
|
const modelSelect = document.getElementById("model");
|
|
|
|
function updateModelOptions() {
|
|
const selectedCategories = Array.from(categorySelect.selectedOptions).map(opt => opt.value);
|
|
const previouslySelectedModels = Array.from(modelSelect.selectedOptions).map(opt => opt.value);
|
|
|
|
if (selectedCategories.length === 0) {
|
|
return;
|
|
}
|
|
|
|
modelSelect.innerHTML = "";
|
|
|
|
const allModels = [];
|
|
const addedKeys = new Set();
|
|
|
|
selectedCategories.forEach(cat => {
|
|
const models = modelsByCategory[cat] || [];
|
|
models.forEach(model => {
|
|
if (!addedKeys.has(model.key)) {
|
|
allModels.push(model);
|
|
addedKeys.add(model.key);
|
|
}
|
|
});
|
|
});
|
|
|
|
allModels.sort((a, b) => a.label.localeCompare(b.label));
|
|
|
|
allModels.forEach(model => {
|
|
const option = document.createElement("option");
|
|
option.value = model.key;
|
|
option.text = model.label;
|
|
if (previouslySelectedModels.includes(model.key)) {
|
|
option.selected = true;
|
|
}
|
|
modelSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
categorySelect.addEventListener("change", updateModelOptions);
|
|
|
|
updateModelOptions();
|
|
});
|
|
</script>
|
|
{% endblock const_script %}
|
|
|
|
{% block extra_script %}
|
|
<script>
|
|
// Load icons when map is ready
|
|
maploader.addEventListener("mapLoaded", function(event) {
|
|
loadMapIcons();
|
|
});
|
|
</script>
|
|
|
|
{% if can_create_location %}
|
|
{% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Intersection" %}
|
|
{% endif %}
|
|
{% endblock extra_script %} |