7 Commits

3 changed files with 126 additions and 122 deletions

View File

@@ -33,7 +33,7 @@ try {
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];
$dsn = "pgsql:host=$host;dbname=$db;port=$port";
$dsn = "pgsql:host=$host;dbname=$db;port=$port;sslmode=disable";
$pdo = new PDO($dsn, $user, $pass, $opt);

View File

@@ -54,7 +54,7 @@ if (!$municipality) {
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.css">
<!-- Leaflet Polyline Measurement Tool -->
<link rel="stylesheet" href="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.css">
<!-- <link rel="stylesheet" href="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.css"> -->
<!-- Font Awesome 6 for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
@@ -321,7 +321,7 @@ if (!$municipality) {
<script src="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.min.js"></script>
<!-- Leaflet PolylineMeasure -->
<script src="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.js"></script>
<!-- <script src="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.js"></script> -->
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>

View File

@@ -14,31 +14,31 @@
// =====================================================================
// API Endpoint as relative Path
var API_URL = 'api/contributions.php';
const API_URL = 'api/contributions.php';
// Current User Name, set via Login Modal, stored in sessionStorage
var currentUser = sessionStorage.getItem('webgis_user') || '';
let currentUser = sessionStorage.getItem('webgis_user') || '';
// Category Definitions with Labels, Icons, and Colors
var CATEGORIES = {
mobility: { label: 'Mobilität', icon: '🚲', color: '#1565C0', faIcon: 'fa-bicycle' },
building: { label: 'Bauen', icon: '🏗️', color: '#E65100', faIcon: 'fa-helmet-safety' },
energy: { label: 'Energie', icon: '⚡', color: '#F9A825', faIcon: 'fa-bolt' },
environment: { label: 'Umwelt', icon: '🌳', color: '#2E7D32', faIcon: 'fa-tree' },
industry: { label: 'Industrie', icon: '🏭', color: '#6A1B9A', faIcon: 'fa-industry' },
consumption: { label: 'Konsum', icon: '🛒', color: '#AD1457', faIcon: 'fa-cart-shopping' },
other: { label: 'Sonstiges', icon: '📌', color: '#546E7A', faIcon: 'fa-map-pin' }
const CATEGORIES = {
consumption: { label: 'Geschäfte', faIcon: 'fa-cart-shopping', color: '#C00000' },
building: { label: 'Bauen', faIcon: 'fa-building', color: '#E65100' },
energy: { label: 'Energie', faIcon: 'fa-bolt', color: '#FFC000' },
environment: { label: 'Umwelt', faIcon: 'fa-seedling', color: '#92D050' },
mobility: { label: 'Mobilität', faIcon: 'fa-bus', color: '#0070C0' },
industry: { label: 'Industrie', faIcon: 'fa-industry', color: '#7030A0' },
other: { label: 'Sonstiges', faIcon: 'fa-thumbtack', color: '#7F7F7F' }
};
// Application State
var map; // Leaflet Map Instance
var sidebar; // Sidebar Instance
var contributionsLayer; // GeoJSON Layer holding all Contributions
var contributionsData = []; // Raw Contribution Data Array
var activeFilters = Object.keys(CATEGORIES); // Active Category Filters
var drawnGeometry = null; // Temporary Storage for Geometry drawn with Geoman
var drawnGeomType = null; // Temporary Storage for Geometry Type
var userVotes = {}; // Tracks User Votes
let map; // Leaflet Map Instance
let sidebar; // Sidebar Instance
let contributionsLayer; // GeoJSON Layer holding all Contributions
let contributionsData = []; // Raw Contribution Data Array
let activeFilters = Object.keys(CATEGORIES); // Active Category Filters
let drawnGeometry = null; // Temporary Storage for Geometry drawn with Geoman
let drawnGeomType = null; // Temporary Storage for Geometry Type
let userVotes = {}; // Tracks User Votes
// =====================================================================
// Block 2: Map Initialization
@@ -63,17 +63,17 @@ map = L.map('map', {
// =====================================================================
// Basemap Tile Layers
var basemapOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
const basemapOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 20
});
var basemapCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© <a href="https://carto.com/">CARTO</a>',
const basemapCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© <a href="https://carto.com/">Carto</a>',
maxZoom: 20
});
var basemapSatellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
const basemapSatellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© <a href="https://www.esri.com/">Esri</a>',
maxZoom: 20
});
@@ -82,15 +82,15 @@ var basemapSatellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/
basemapCartoDB.addTo(map);
// Layer Control
var basemaps = {
'OpenStreetMap': basemapOSM,
'CartoDB (hell)': basemapCartoDB,
'Satellit (Esri)': basemapSatellite,
const basemaps = {
'<i class="fa-solid fa-map" style="color:#404040;"></i> Hintergrundkarte (farbe)': basemapOSM,
'<i class="fa-solid fa-map" style="color:#404040;"></i> Hintergrundkarte (grau)': basemapCartoDB,
'<i class="fa-solid fa-satellite" style="color:#404040;"></i> Satellitenbild': basemapSatellite,
};
var overlays = {}; // Populated later with Contribution Layers
const overlays = {}; // Populated later with Contribution Layers
var layerControl = L.control.layers(basemaps, overlays, {
const layerControl = L.control.layers(basemaps, overlays, {
position: 'topright',
collapsed: true
}).addTo(map);
@@ -135,39 +135,39 @@ L.Control.geocoder({
}).addTo(map);
// Polyline Measure Tool
L.control.polylineMeasure({
position: 'topright',
unit: 'metres',
showBearings: false,
clearMeasurementsOnStop: false,
showClearControl: true
}).addTo(map);
// L.control.polylineMeasure({
// position: 'topright',
// unit: 'metres',
// showBearings: false,
// clearMeasurementsOnStop: false,
// showClearControl: true
// }).addTo(map);
// Mouse Position Display
var MousePositionControl = L.Control.extend({
options: { position: 'bottomright' },
// const MousePositionControl = L.Control.extend({
// options: { position: 'bottomright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'mouse-position-display');
container.innerHTML = 'Lat: , Lng: ';
// onAdd: function () {
// const container = L.DomUtil.create('div', 'mouse-position-display');
// container.innerHTML = 'Lat: , Lng: ';
map.on('mousemove', function (e) {
container.innerHTML = 'Lat: ' + e.latlng.lat.toFixed(5) + ', Lng: ' + e.latlng.lng.toFixed(5);
});
// map.on('mousemove', function (e) {
// container.innerHTML = 'Lat: ' + e.latlng.lat.toFixed(5) + ', Lng: ' + e.latlng.lng.toFixed(5);
// });
return container;
}
});
// return container;
// }
// });
new MousePositionControl().addTo(map);
// new MousePositionControl().addTo(map);
// GPS Location Button
var GpsControl = L.Control.extend({
const GpsControl = L.Control.extend({
options: { position: 'topright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
var button = L.DomUtil.create('a', 'gps-control-button', container);
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
const button = L.DomUtil.create('a', 'gps-control-button', container);
button.href = '#';
button.title = 'Mein Standort';
button.innerHTML = '<i class="fa-solid fa-location-crosshairs"></i>';
@@ -184,7 +184,7 @@ var GpsControl = L.Control.extend({
new GpsControl().addTo(map);
// GPS Location Found Handler
var gpsMarker = null;
let gpsMarker = null;
map.on('locationfound', function (e) {
if (gpsMarker) {
@@ -240,7 +240,7 @@ map.pm.setLang('de');
// Captures drawn Geometry and opens the Create Modal
map.on('pm:create', function (e) {
var geojson = e.layer.toGeoJSON().geometry;
const geojson = e.layer.toGeoJSON().geometry;
// Determines drawn Geometry Type and normalizes to simple Types
if (e.shape === 'Marker') {
@@ -280,8 +280,8 @@ map.on('pm:create', function (e) {
// Generic API Call Function
function apiCall(data, callback) {
var formData = new FormData();
for (var key in data) {
const formData = new FormData();
for (const key in data) {
formData.append(key, data[key]);
}
@@ -324,8 +324,7 @@ function loadContributions() {
onEachFeature: bindFeaturePopup
}).addTo(map);
layerControl.addOverlay(contributionsLayer, 'Beiträge');
layerControl.addOverlay(contributionsLayer, '<i class="fa-solid fa-map-pin" style="color:#C00000;"></i> Beiträge');
// Update Sidebar List and Statistics
updateContributionsList();
updateStatistics();
@@ -339,7 +338,7 @@ function loadContributions() {
// Style for Point Features (CircleMarkers)
function stylePoint(feature, latlng) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
const cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return L.circleMarker(latlng, {
radius: 8,
@@ -352,7 +351,7 @@ function stylePoint(feature, latlng) {
// Style for Line and Polygon Features
function styleLinePolygon(feature) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
const cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return {
color: cat.color,
@@ -369,19 +368,19 @@ function styleLinePolygon(feature) {
// =====================================================================
function bindFeaturePopup(feature, layer) {
var props = feature.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
const props = feature.properties;
const cat = CATEGORIES[props.category] || CATEGORIES.other;
// Formats Date
var date = new Date(props.created_at);
var dateStr = date.toLocaleDateString('de-DE', {
const date = new Date(props.created_at);
const dateStr = date.toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric'
});
// Builds Popup on Click
var html = '' +
const html = '' +
'<div class="popup-detail">' +
'<span class="popup-detail-category">' + cat.icon + ' ' + cat.label + '</span>' +
'<span class="popup-detail-category">' + categoryIcon(cat) + ' ' + cat.label + '</span>' +
'<div class="popup-detail-title">' + escapeHtml(props.title) + '</div>' +
(props.description ? '<div class="popup-detail-description">' + escapeHtml(props.description) + '</div>' : '') +
'<div class="popup-detail-meta">' +
@@ -406,7 +405,7 @@ function bindFeaturePopup(feature, layer) {
layer.bindPopup(html, { maxWidth: 320, minWidth: 240 });
// Builds Tooltip on Hover
layer.bindTooltip(cat.icon + ' ' + escapeHtml(props.title), {
layer.bindTooltip(categoryIcon(cat) + ' ' + escapeHtml(props.title), {
direction: 'top',
offset: [0, -10]
});
@@ -419,11 +418,11 @@ function bindFeaturePopup(feature, layer) {
// CREATE: Submits new Contributions from Modal
function submitCreate() {
var category = document.getElementById('create-category').value;
var title = document.getElementById('create-title').value.trim();
var description = document.getElementById('create-description').value.trim();
var geom = document.getElementById('create-geom').value;
var geomType = document.getElementById('create-geom-type').value;
const category = document.getElementById('create-category').value;
const title = document.getElementById('create-title').value.trim();
const description = document.getElementById('create-description').value.trim();
const geom = document.getElementById('create-geom').value;
const geomType = document.getElementById('create-geom-type').value;
// Validates
if (!category) {
@@ -479,13 +478,13 @@ function closeCreateModal() {
// UPDATE: Edits existing Contributions
function editContribution(contributionId) {
// Finds Contribution in local Data
var contribution = contributionsData.find(function (f) {
const contribution = contributionsData.find(function (f) {
return f.properties.contribution_id === contributionId;
});
if (!contribution) return;
var props = contribution.properties;
const props = contribution.properties;
Swal.fire({
title: 'Beitrag bearbeiten',
@@ -571,10 +570,10 @@ function voteContribution(contributionId, voteType) {
}
// Updates local Vote State
var likeBtn = document.getElementById('vote-like-' + contributionId);
var dislikeBtn = document.getElementById('vote-dislike-' + contributionId);
var likesSpan = document.getElementById('likes-' + contributionId);
var dislikesSpan = document.getElementById('dislikes-' + contributionId);
const likeBtn = document.getElementById('vote-like-' + contributionId);
const dislikeBtn = document.getElementById('vote-dislike-' + contributionId);
const likesSpan = document.getElementById('likes-' + contributionId);
const dislikesSpan = document.getElementById('dislikes-' + contributionId);
if (response.action === 'created') {
// New Vote — Highlights Button and updates Count
@@ -620,16 +619,16 @@ function voteContribution(contributionId, voteType) {
// =====================================================================
function updateContributionsList() {
var container = document.getElementById('contributions-list');
var searchInput = document.getElementById('list-search-input');
var searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
const container = document.getElementById('contributions-list');
const searchInput = document.getElementById('list-search-input');
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
// Filters by Categories and Search Term
var filtered = contributionsData.filter(function (f) {
var props = f.properties;
var matchesCategory = activeFilters.indexOf(props.category) !== -1;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
var matchesSearch = !searchTerm ||
const filtered = contributionsData.filter(function (f) {
const props = f.properties;
const matchesCategory = activeFilters.indexOf(props.category) !== -1;
const cat = CATEGORIES[props.category] || CATEGORIES.other;
const matchesSearch = !searchTerm ||
props.title.toLowerCase().indexOf(searchTerm) !== -1 ||
(props.description && props.description.toLowerCase().indexOf(searchTerm) !== -1) ||
props.author_name.toLowerCase().indexOf(searchTerm) !== -1 ||
@@ -648,16 +647,16 @@ function updateContributionsList() {
return;
}
var html = '';
let html = '';
filtered.forEach(function (f) {
var props = f.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
var date = new Date(props.created_at).toLocaleDateString('de-DE');
const props = f.properties;
const cat = CATEGORIES[props.category] || CATEGORIES.other;
const date = new Date(props.created_at).toLocaleDateString('de-DE');
html += '' +
'<div class="contribution-card" onclick="flyToContribution(' + props.contribution_id + ')">' +
'<div class="contribution-card-header">' +
'<span class="contribution-card-category">' + cat.icon + ' ' + cat.label + '</span>' +
'<span class="contribution-card-category">' + categoryIcon(cat) + ' ' + cat.label + '</span>' +
'</div>' +
'<div class="contribution-card-title">' + escapeHtml(props.title) + '</div>' +
'<div class="contribution-card-meta">' +
@@ -709,18 +708,17 @@ document.getElementById('list-search-input').addEventListener('input', function
// Builds Category Filter Checkboxes
function buildCategoryFilter() {
var container = document.getElementById('category-filter');
var html = '';
const container = document.getElementById('category-filter');
let html = '';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var checked = activeFilters.indexOf(key) !== -1 ? 'checked' : '';
for (const key in CATEGORIES) {
const cat = CATEGORIES[key];
const checked = activeFilters.indexOf(key) !== -1 ? 'checked' : '';
html += '' +
'<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;">' +
'<input type="checkbox" value="' + key + '" ' + checked + ' onchange="toggleCategoryFilter(this)">' +
'<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:' + cat.color + ';"></span>' +
'<span>' + cat.icon + ' ' + cat.label + '</span>' +
'<span>' + categoryIcon(cat) + ' ' + cat.label + '</span>' +
'</label>';
}
@@ -729,7 +727,7 @@ function buildCategoryFilter() {
// Toggles a Category Filter on or off
function toggleCategoryFilter(checkbox) {
var category = checkbox.value;
const category = checkbox.value;
if (checkbox.checked) {
if (activeFilters.indexOf(category) === -1) {
@@ -739,11 +737,11 @@ function toggleCategoryFilter(checkbox) {
activeFilters = activeFilters.filter(function (c) { return c !== category; });
}
// Refilters Map Layer
// Refilters Map Layer
if (contributionsLayer) {
contributionsLayer.eachLayer(function (layer) {
if (layer.feature) {
var cat = layer.feature.properties.category;
const cat = layer.feature.properties.category;
if (activeFilters.indexOf(cat) !== -1) {
layer.setStyle({ opacity: 1, fillOpacity: layer.feature.geometry.type === 'Point' ? 0.9 : 0.25 });
if (layer.setRadius) layer.setRadius(8);
@@ -765,24 +763,24 @@ function toggleCategoryFilter(checkbox) {
// Updates Statistics in Home Tab
function updateStatistics() {
var container = document.getElementById('stats-container');
var total = contributionsData.length;
const container = document.getElementById('stats-container');
const total = contributionsData.length;
// Counts per Category
var counts = {};
const counts = {};
contributionsData.forEach(function (f) {
var cat = f.properties.category;
const cat = f.properties.category;
counts[cat] = (counts[cat] || 0) + 1;
});
var html = '<p style="font-size:0.9rem;"><strong>' + total + '</strong> Beiträge insgesamt</p>';
let html = '<p style="font-size:0.9rem;"><strong>' + total + '</strong> Beiträge insgesamt</p>';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var count = counts[key] || 0;
for (const key in CATEGORIES) {
const cat = CATEGORIES[key];
const count = counts[key] || 0;
if (count > 0) {
html += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:0.85rem;">' +
'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + cat.color + ';"></span>' +
categoryIcon(cat) + ' ' +
cat.label + ': ' + count +
'</div>';
}
@@ -798,7 +796,7 @@ function updateStatistics() {
// Welcome Modal shows on new Visits
function checkWelcomeModal() {
var hasVisited = localStorage.getItem('webgis_welcomed');
const hasVisited = localStorage.getItem('webgis_welcomed');
if (!hasVisited) {
document.getElementById('welcome-modal').style.display = 'flex';
}
@@ -818,7 +816,7 @@ function showLoginModal() {
}
function submitLogin() {
var name = document.getElementById('user-name-input').value.trim();
const name = document.getElementById('user-name-input').value.trim();
if (!name) {
Swal.fire('Name eingeben', 'Bitte geben Sie Ihren Namen ein.', 'warning');
return;
@@ -882,14 +880,14 @@ function showImprintModal() {
// =====================================================================
function toggleMobileNav() {
var nav = document.querySelector('.header-nav');
const nav = document.querySelector('.header-nav');
nav.classList.toggle('open');
}
// Closes Mobile Nav when clicking outside
document.addEventListener('click', function (e) {
var nav = document.querySelector('.header-nav');
var toggle = document.querySelector('.header-menu-toggle');
const nav = document.querySelector('.header-nav');
const toggle = document.querySelector('.header-menu-toggle');
if (nav.classList.contains('open') && !nav.contains(e.target) && !toggle.contains(e.target)) {
nav.classList.remove('open');
@@ -913,11 +911,16 @@ document.addEventListener('keydown', function (e) {
// Escapes HTML to prevent Cross-Site Scripting (XSS) in Popups and Lists
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
// Returns a colored Font Awesome Icon HTML String for a Category
function categoryIcon(cat) {
return '<i class="fa-solid ' + cat.faIcon + '" style="color:' + cat.color + ';"></i>';
}
// =====================================================================
// Block 16: Application Startup
@@ -925,12 +928,13 @@ function escapeHtml(text) {
// Populates Category Dropdown in Create Modal from Categories Object
function buildCategoryDropdown() {
var select = document.getElementById('create-category');
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var option = document.createElement('option');
const select = document.getElementById('create-category');
for (const key in CATEGORIES) {
const cat = CATEGORIES[key];
const option = document.createElement('option');
option.value = key;
option.textContent = cat.icon + ' ' + cat.label;
option.textContent = cat.label;
option.dataset.icon = cat.faIcon;
select.appendChild(option);
}
}