-
-
-
- Twoja ocena została zapisana!
-
-
+
+
+
Twoja ocena została zapisana!
+
+
-
{% endblock %}
{% block extra_js %}
+
+
{% endblock %}
\ No newline at end of file
diff --git a/cityfeel/static/js/map.js b/cityfeel/static/js/map.js
index b9589af..bec0d4f 100644
--- a/cityfeel/static/js/map.js
+++ b/cityfeel/static/js/map.js
@@ -2,299 +2,338 @@
* CityFeel Emotion Map
* Wyświetlanie lokalizacji z emocjami z clustering i dynamic loading
*/
-(function() {
- 'use strict';
-
- // === KONFIGURACJA ===
- const CONFIG = {
- DEBOUNCE_DELAY: 300, // ms - opóźnienie dla moveend event
-
- // Specjalne kolory punktów
- COLORS: {
- EMPTY: '#7f8c8d', // Szary - brak ocen i komentarzy
- COMMENT: '#9b59b6' // Fioletowy - tylko komentarze (brak ocen)
- },
-
- // Kolory według avg_emotional_value (1-5)
- EMOTION_COLORS: {
- 1.0: '#e74c3c', // Bardzo negatywne (czerwony)
- 2.0: '#e67e22', // Negatywne (pomarańczowy)
- 3.0: '#f39c12', // Neutralne (żółty)
- 4.0: '#2ecc71', // Pozytywne (zielony)
- 5.0: '#27ae60', // Bardzo pozytywne (ciemnozielony)
- }
- };
-
- // === STAN ===
- let map = null;
- let markerClusterGroup = null;
- let debounceTimer = null;
- let currentBounds = null;
-
- // Stan filtrów
- let activeFilters = [];
-
- // Stan dla dodawania emocji
- let addEmotionModal = null;
- let isUserAuthenticated = false;
- let selectedCoordinates = null;
- let proximityRadius = 50; // domyślnie 50m, nadpisane z data-attribute
- let emotionPointsUrl = '/api/emotion-points/'; // fallback, nadpisane z data-attribute
-
- // === INICJALIZACJA ===
- function init() {
- // Mapa jest już zainicjalizowana w template jako window.map
- map = window.map;
-
- // Sprawdź czy user jest zalogowany i pobierz konfigurację z data-attributes
- const mapElement = document.getElementById('map');
- isUserAuthenticated = mapElement.dataset.userAuthenticated === 'true';
- proximityRadius = parseInt(mapElement.dataset.proximityRadius, 10) || 50;
- emotionPointsUrl = mapElement.dataset.emotionPointsUrl || '/api/emotion-points/';
-
- // Inicjalizacja funkcjonalności dodawania emocji (tylko dla zalogowanych)
- if (isUserAuthenticated) {
- initAddEmotionFeature();
- }
-
- initFilters();
-
- // Cluster group
- markerClusterGroup = L.markerClusterGroup({
- maxClusterRadius: 50,
- spiderfyOnMaxZoom: true,
- showCoverageOnHover: false,
- zoomToBoundsOnClick: true,
- iconCreateFunction: createClusterIcon
- });
-
- map.addLayer(markerClusterGroup);
-
- // Event listeners
- map.on('moveend', debounce(loadVisibleLocations, CONFIG.DEBOUNCE_DELAY));
-
- // Pierwsze załadowanie
- loadVisibleLocations();
- }
-
- // === OBSŁUGA FILTRÓW ===
-function initFilters() {
- const filtersContainer = document.getElementById('map-filters');
- if (!filtersContainer) return;
-
- const buttons = filtersContainer.querySelectorAll('.filter-btn');
-
- buttons.forEach(btn => {
- btn.addEventListener('click', function(e) {
- // Zapobiegaj propagacji, żeby nie klikać mapy pod spodem
- e.stopPropagation();
-
- const btn = e.currentTarget;
- const value = parseInt(btn.dataset.value, 10);
-
- // 1. Zaktualizuj stan (tablica activeFilters)
- if (activeFilters.includes(value)) {
- // Usuń z filtrów
- activeFilters = activeFilters.filter(v => v !== value);
- toggleFilterButtonStyle(btn, false);
+(function () {
+ 'use strict';
+
+ // === KONFIGURACJA ===
+ const CONFIG = {
+ DEBOUNCE_DELAY: 300, // ms - opóźnienie dla moveend event
+
+ // Specjalne kolory punktów
+ COLORS: {
+ EMPTY: '#7f8c8d', // Szary - brak ocen i komentarzy
+ COMMENT: '#9b59b6' // Fioletowy - tylko komentarze (brak ocen)
+ },
+
+ // Kolory według avg_emotional_value (1-5)
+ EMOTION_COLORS: {
+ 1.0: '#e74c3c', // Bardzo negatywne (czerwony)
+ 2.0: '#e67e22', // Negatywne (pomarańczowy)
+ 3.0: '#f39c12', // Neutralne (żółty)
+ 4.0: '#2ecc71', // Pozytywne (zielony)
+ 5.0: '#27ae60', // Bardzo pozytywne (ciemnozielony)
+ },
+
+ // NOWE: Konfiguracja Heatmapy
+ HEATMAP: {
+ DEFAULT_RADIUS: 30,
+ DEFAULT_BLUR: 20,
+ MAX_ZOOM: 16,
+ // Gradient: Blue (chłodno) -> Yellow -> Red (gorąco/najlepiej)
+ GRADIENT: {
+ 0.4: 'blue',
+ 0.6: 'lime', // Przejściowy zielony dla płynności
+ 0.8: 'yellow',
+ 1.0: 'red' // Czerwony na szczycie skali (max)
+ }
+ }
+ };
+
+ // === STAN ===
+ let map = null;
+ let markerClusterGroup = null;
+ let debounceTimer = null;
+ let currentBounds = null;
+
+ // NOWE: Stan Heatmapy
+ let heatLayer = null;
+ let isHeatmapActive = false;
+ let currentLocationsData = []; // Cache danych do przełączania widoku i sprawdzania bliskości
+
+ // Stan filtrów
+ let activeFilters = [];
+
+ // Stan dla dodawania emocji
+ let addEmotionModal = null;
+ let isUserAuthenticated = false;
+ let selectedCoordinates = null;
+ let proximityRadius = 50; // domyślnie 50m, nadpisane z data-attribute
+ let emotionPointsUrl = '/api/emotion-points/'; // fallback, nadpisane z data-attribute
+
+ // === INICJALIZACJA ===
+ function init() {
+ // Mapa jest już zainicjalizowana w template jako window.map
+ map = window.map;
+
+ // Sprawdź czy user jest zalogowany i pobierz konfigurację z data-attributes
+ const mapElement = document.getElementById('map');
+ isUserAuthenticated = mapElement.dataset.userAuthenticated === 'true';
+ proximityRadius = parseInt(mapElement.dataset.proximityRadius, 10) || 50;
+ emotionPointsUrl = mapElement.dataset.emotionPointsUrl || '/api/emotion-points/';
+
+ // Inicjalizacja funkcjonalności dodawania emocji (tylko dla zalogowanych)
+ if (isUserAuthenticated) {
+ initAddEmotionFeature();
+ }
+
+ initFilters();
+ initHeatmapControls(); // NOWE: UI Heatmapy
+
+ // Cluster group
+ markerClusterGroup = L.markerClusterGroup({
+ maxClusterRadius: 50,
+ spiderfyOnMaxZoom: true,
+ showCoverageOnHover: false,
+ zoomToBoundsOnClick: true,
+ iconCreateFunction: createClusterIcon
+ });
+
+ // NOWE: Warstwa Heatmapy
+ if (typeof L.heatLayer === 'function') {
+ heatLayer = L.heatLayer([], {
+ radius: CONFIG.HEATMAP.DEFAULT_RADIUS,
+ blur: CONFIG.HEATMAP.DEFAULT_BLUR,
+ maxZoom: CONFIG.HEATMAP.MAX_ZOOM,
+ gradient: CONFIG.HEATMAP.GRADIENT
+ });
+ }
+
+ map.addLayer(markerClusterGroup);
+
+ // Event listeners
+ map.on('moveend', debounce(loadVisibleLocations, CONFIG.DEBOUNCE_DELAY));
+
+ // Pierwsze załadowanie
+ loadVisibleLocations();
+ }
+
+ // === OBSŁUGA FILTRÓW ===
+ function initFilters() {
+ const filtersContainer = document.getElementById('map-filters');
+ if (!filtersContainer) return;
+
+ const buttons = filtersContainer.querySelectorAll('.filter-btn');
+
+ buttons.forEach(btn => {
+ btn.addEventListener('click', function (e) {
+ // Zapobiegaj propagacji, żeby nie klikać mapy pod spodem
+ e.stopPropagation();
+
+ const btn = e.currentTarget;
+ const value = parseInt(btn.dataset.value, 10);
+
+ // 1. Zaktualizuj stan (tablica activeFilters)
+ if (activeFilters.includes(value)) {
+ // Usuń z filtrów
+ activeFilters = activeFilters.filter(v => v !== value);
+ toggleFilterButtonStyle(btn, false);
+ } else {
+ // Dodaj do filtrów
+ activeFilters.push(value);
+ toggleFilterButtonStyle(btn, true);
+ }
+
+ // 2. Przeładuj mapę z nowymi filtrami
+ // force=true wymusza odświeżenie nawet jak nie ruszyliśmy mapą
+ loadVisibleLocations(true);
+ });
+ });
+ }
+
+ // Funkcja zmieniająca wygląd przycisku (Outline <-> Solid)
+ function toggleFilterButtonStyle(btn, isActive) {
+ // Pobieramy klasy Bootstrapa na podstawie koloru (np. btn-outline-danger -> btn-danger)
+ const classList = Array.from(btn.classList);
+ const outlineClass = classList.find(c => c.startsWith('btn-outline-'));
+
+ if (outlineClass) {
+ const solidClass = outlineClass.replace('btn-outline-', 'btn-');
+
+ if (isActive) {
+ btn.classList.remove(outlineClass);
+ btn.classList.add(solidClass, 'text-white', 'shadow'); // Dodajemy cień i biały tekst
+ } else {
+ }
} else {
- // Dodaj do filtrów
- activeFilters.push(value);
- toggleFilterButtonStyle(btn, true);
+ // Jeśli przycisk jest już "solid", to znaczy że go odznaczamy
+ const solidClass = classList.find(c => c.startsWith('btn-') && !c.startsWith('btn-outline-') && c !== 'btn-sm');
+ if (solidClass) {
+ const outlineClass = solidClass.replace('btn-', 'btn-outline-');
+ if (!isActive) {
+ btn.classList.remove(solidClass, 'text-white', 'shadow');
+ btn.classList.add(outlineClass);
+ }
+ }
}
+ }
- // 2. Przeładuj mapę z nowymi filtrami
- // force=true wymusza odświeżenie nawet jak nie ruszyliśmy mapą
- loadVisibleLocations(true);
- });
- });
- }
-
- // Funkcja zmieniająca wygląd przycisku (Outline <-> Solid)
- function toggleFilterButtonStyle(btn, isActive) {
- // Pobieramy klasy Bootstrapa na podstawie koloru (np. btn-outline-danger -> btn-danger)
- const classList = Array.from(btn.classList);
- const outlineClass = classList.find(c => c.startsWith('btn-outline-'));
-
- if (outlineClass) {
- const solidClass = outlineClass.replace('btn-outline-', 'btn-');
-
- if (isActive) {
- btn.classList.remove(outlineClass);
- btn.classList.add(solidClass, 'text-white', 'shadow'); // Dodajemy cień i biały tekst
- } else {
- }
- } else {
- // Jeśli przycisk jest już "solid", to znaczy że go odznaczamy
- const solidClass = classList.find(c => c.startsWith('btn-') && !c.startsWith('btn-outline-') && c !== 'btn-sm');
- if (solidClass) {
- const outlineClass = solidClass.replace('btn-', 'btn-outline-');
- if (!isActive) {
- btn.classList.remove(solidClass, 'text-white', 'shadow');
- btn.classList.add(outlineClass);
- }
+
+ // === CLUSTER ICON ===
+ function createClusterIcon(cluster) {
+ const childMarkers = cluster.getAllChildMarkers();
+ const avgValue = calculateAverageEmotionValue(childMarkers);
+ const count = childMarkers.length;
+ const color = getColorByValue(avgValue);
+
+ return L.divIcon({
+ html: `
${count}
`,
+ className: 'marker-cluster-custom',
+ iconSize: L.point(40, 40)
+ });
+ }
+
+ function calculateAverageEmotionValue(markers) {
+ if (!markers.length) return 3.0;
+
+ const sum = markers.reduce((acc, marker) => {
+ return acc + (marker.options.emotionValue || 3.0);
+ }, 0);
+
+ return sum / markers.length;
+ }
+
+ // === KOLORY ===
+ function getColorByValue(value) {
+ if (!value) return CONFIG.EMOTION_COLORS[3.0]; // Default fallback
+
+ if (value < 1.5) return CONFIG.EMOTION_COLORS[1.0];
+ if (value < 2.5) return CONFIG.EMOTION_COLORS[2.0];
+ if (value < 3.5) return CONFIG.EMOTION_COLORS[3.0];
+ if (value < 4.5) return CONFIG.EMOTION_COLORS[4.0];
+ return CONFIG.EMOTION_COLORS[5.0];
+ }
+
+ // === LOADING LOCATIONS ===
+ function loadVisibleLocations(force = false) {
+ const bounds = map.getBounds();
+
+ // Normalizuj współrzędne do zakresu -180..180
+ // (potrzebne gdy mapa okrąża Ziemię i lng przekracza ±180)
+ const sw = bounds.getSouthWest().wrap();
+ const ne = bounds.getNorthEast().wrap();
+
+ const bbox = [
+ sw.lng,
+ sw.lat,
+ ne.lng,
+ ne.lat
+ ].join(',');
+
+ // Sprawdź czy bounds się zmieniły (chyba że force=true)
+ if (!force && currentBounds === bbox) return;
+ currentBounds = bbox;
+
+ // Pobierz API URL z data attribute
+ const apiUrl = document.getElementById('map').dataset.apiUrl || '/api/locations/';
+ let url = `${apiUrl}?bbox=${bbox}`;
+
+ // Jeśli są aktywne filtry, dodaj je do URL
+ if (activeFilters.length > 0) {
+ url += `&emotional_value=${activeFilters.join(',')}`;
}
+
+ fetchLocations(url);
}
- }
-
-
- // === CLUSTER ICON ===
- function createClusterIcon(cluster) {
- const childMarkers = cluster.getAllChildMarkers();
- const avgValue = calculateAverageEmotionValue(childMarkers);
- const count = childMarkers.length;
- const color = getColorByValue(avgValue);
-
- return L.divIcon({
- html: `
${count}
`,
- className: 'marker-cluster-custom',
- iconSize: L.point(40, 40)
- });
- }
-
- function calculateAverageEmotionValue(markers) {
- if (!markers.length) return 3.0;
-
- const sum = markers.reduce((acc, marker) => {
- return acc + (marker.options.emotionValue || 3.0);
- }, 0);
-
- return sum / markers.length;
- }
-
- // === KOLORY ===
- function getColorByValue(value) {
- if (!value) return CONFIG.EMOTION_COLORS[3.0]; // Default fallback
-
- if (value < 1.5) return CONFIG.EMOTION_COLORS[1.0];
- if (value < 2.5) return CONFIG.EMOTION_COLORS[2.0];
- if (value < 3.5) return CONFIG.EMOTION_COLORS[3.0];
- if (value < 4.5) return CONFIG.EMOTION_COLORS[4.0];
- return CONFIG.EMOTION_COLORS[5.0];
- }
-
- // === LOADING LOCATIONS ===
- function loadVisibleLocations(force = false) {
- const bounds = map.getBounds();
-
- // Normalizuj współrzędne do zakresu -180..180
- // (potrzebne gdy mapa okrąża Ziemię i lng przekracza ±180)
- const sw = bounds.getSouthWest().wrap();
- const ne = bounds.getNorthEast().wrap();
-
- const bbox = [
- sw.lng,
- sw.lat,
- ne.lng,
- ne.lat
- ].join(',');
-
- // Sprawdź czy bounds się zmieniły (chyba że force=true)
- if (!force && currentBounds === bbox) return;
- currentBounds = bbox;
-
- // Pobierz API URL z data attribute
- const apiUrl = document.getElementById('map').dataset.apiUrl || '/api/locations/';
- let url = `${apiUrl}?bbox=${bbox}`;
-
- // Jeśli są aktywne filtry, dodaj je do URL
- if (activeFilters.length > 0) {
- url += `&emotional_value=${activeFilters.join(',')}`;
- }
-
- fetchLocations(url);
- }
-
- function fetchLocations(url) {
- document.body.style.cursor = 'wait';
-
- fetch(url, {
- credentials: 'same-origin',
- headers: {
- 'Accept': 'application/json'
- }
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
+
+ function fetchLocations(url) {
+ document.body.style.cursor = 'wait';
+
+ fetch(url, {
+ credentials: 'same-origin',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ // Obsługa odpowiedzi bez paginacji (tablica) i z paginacją (obiekt z results)
+ const locations = Array.isArray(data) ? data : (data.results || []);
+
+ // NOWE: Zapisz do cache
+ currentLocationsData = locations;
+
+ // NOWE: Decyzja co wyświetlić
+ if (isHeatmapActive) {
+ updateHeatmapData(locations);
+ } else {
+ displayLocations(locations);
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching locations:', error);
+ })
+ .finally(() => {
+ document.body.style.cursor = 'default';
+ });
+ }
+
+ // === WYŚWIETLANIE MARKERÓW ===
+ function displayLocations(locations) {
+ markerClusterGroup.clearLayers();
+
+ locations.forEach(location => {
+ const marker = createMarker(location);
+ markerClusterGroup.addLayer(marker);
+ });
+ }
+
+ function createMarker(location) {
+ const { coordinates, avg_emotional_value, name, id, emotion_points_count, comments_count } = location;
+ const { latitude, longitude } = coordinates;
+
+ // LOGIKA KOLORÓW PUNKTÓW
+ let color;
+ // 1. Punkty z ocenami -> Skala emocji
+ if (emotion_points_count > 0) {
+ color = getColorByValue(avg_emotional_value);
}
- return response.json();
- })
- .then(data => {
- // Obsługa odpowiedzi bez paginacji (tablica) i z paginacją (obiekt z results)
- const locations = Array.isArray(data) ? data : (data.results || []);
- displayLocations(locations);
- })
- .catch(error => {
- console.error('Error fetching locations:', error);
- })
- .finally(() => {
- document.body.style.cursor = 'default';
- });
- }
-
- // === WYŚWIETLANIE MARKERÓW ===
- function displayLocations(locations) {
- markerClusterGroup.clearLayers();
-
- locations.forEach(location => {
- const marker = createMarker(location);
- markerClusterGroup.addLayer(marker);
- });
- }
-
- function createMarker(location) {
- const { coordinates, avg_emotional_value, name, id, emotion_points_count, comments_count } = location;
- const { latitude, longitude } = coordinates;
-
- // LOGIKA KOLORÓW PUNKTÓW
- let color;
- // 1. Punkty z ocenami -> Skala emocji
- if (emotion_points_count > 0) {
- color = getColorByValue(avg_emotional_value);
- }
- // 2. Punkty TYLKO z komentarzami -> Fioletowy
- else if (comments_count > 0) {
- color = CONFIG.COLORS.COMMENT;
- }
- // 3. Puste punkty -> Szary
- else {
- color = CONFIG.COLORS.EMPTY;
- }
-
- const marker = L.circleMarker([latitude, longitude], {
- radius: 10,
- fillColor: color,
- color: '#ffffff',
- weight: 2,
- opacity: 1,
- fillOpacity: 0.8,
- emotionValue: avg_emotional_value || 3.0
- });
-
- marker.bindPopup(createPopupContent(location));
-
- return marker;
- }
-
-// === POPUP ===
- function createPopupContent(location) {
- const { name, avg_emotional_value, emotion_points_count, comments_count, id, latest_comment } = location;
-
- const stars = getStarsHTML(avg_emotional_value);
- const detailsUrl = `/map/location/${id}/`;
-
- const ratingText = `Oparte na ${emotion_points_count || 0} ${pluralize(emotion_points_count)}`;
-
- let commentsText = '';
- const count = comments_count || 0;
- if (count === 1) commentsText = '1 komentarz';
- else if (count > 1 && count < 5) commentsText = `${count} komentarze`;
- else commentsText = `${count} komentarzy`;
-
- let commentHtml = '';
- if (latest_comment) {
- commentHtml = `
+ // 2. Punkty TYLKO z komentarzami -> Fioletowy
+ else if (comments_count > 0) {
+ color = CONFIG.COLORS.COMMENT;
+ }
+ // 3. Puste punkty -> Szary
+ else {
+ color = CONFIG.COLORS.EMPTY;
+ }
+
+ const marker = L.circleMarker([latitude, longitude], {
+ radius: 10,
+ fillColor: color,
+ color: '#ffffff',
+ weight: 2,
+ opacity: 1,
+ fillOpacity: 0.8,
+ emotionValue: avg_emotional_value || 3.0
+ });
+
+ marker.bindPopup(createPopupContent(location));
+
+ return marker;
+ }
+
+ // === POPUP ===
+ function createPopupContent(location) {
+ const { name, avg_emotional_value, emotion_points_count, comments_count, id, latest_comment } = location;
+
+ const stars = getStarsHTML(avg_emotional_value);
+ const detailsUrl = `/map/location/${id}/`;
+
+ const ratingText = `Oparte na ${emotion_points_count || 0} ${pluralize(emotion_points_count)}`;
+
+ let commentsText = '';
+ const count = comments_count || 0;
+ if (count === 1) commentsText = '1 komentarz';
+ else if (count > 1 && count < 5) commentsText = `${count} komentarze`;
+ else commentsText = `${count} komentarzy`;
+
+ let commentHtml = '';
+ if (latest_comment) {
+ commentHtml = `
${escapeHtml(latest_comment.username)}
@@ -305,15 +344,15 @@ function initFilters() {
`;
- } else {
- commentHtml = `
+ } else {
+ commentHtml = `
Brak opinii. Bądź pierwszy!
`;
- }
+ }
- return `
+ return `
`;
- }
-
- function getStarsHTML(value) {
- if (!value) return '';
-
- const fullStars = Math.floor(value);
- const hasHalfStar = (value % 1) >= 0.5;
- let html = '';
-
- for (let i = 0; i < 5; i++) {
- if (i < fullStars) {
- html += '★';
- } else if (i === fullStars && hasHalfStar) {
- html += '☆';
- } else {
- html += '☆';
- }
- }
-
- return `
${html}`;
- }
-
- // === UTILITIES ===
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
-
- function pluralize(count) {
- if (count === 1) return 'ocenie';
- if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) {
- return 'ocenach';
- }
- return 'ocenach';
- }
-
- function showError(message) {
- alert(message);
- }
-
- function debounce(func, delay) {
- return function(...args) {
- clearTimeout(debounceTimer);
- debounceTimer = setTimeout(() => func.apply(this, args), delay);
- };
- }
-
- // === ADD EMOTION FEATURE ===
-
- /**
- * Inicjalizacja funkcjonalności dodawania emocji.
- * Wywoływana tylko dla zalogowanych użytkowników.
- */
- function initAddEmotionFeature() {
- // Inicjalizacja Bootstrap Modal
- const modalElement = document.getElementById('addEmotionModal');
- if (!modalElement) {
- console.warn('Modal #addEmotionModal nie został znaleziony w HTML');
- return;
- }
-
- addEmotionModal = new bootstrap.Modal(modalElement);
-
- // Event listener dla kliknięcia na mapę
- map.on('click', handleMapClick);
-
- // Dodaj wizualną wskazówkę (cursor: crosshair)
- document.getElementById('map').classList.add('clickable-map');
-
- // Inicjalizacja interaktywnych gwiazdek
- initStarRating();
-
- // Event listener dla przycisku Submit
- document.getElementById('submitEmotion').addEventListener('click', handleEmotionSubmit);
-
- // Event listener dla zamknięcia modala - reset formularza
- modalElement.addEventListener('hidden.bs.modal', resetEmotionForm);
- }
-
- /**
- * Obsługa kliknięcia na mapę.
- * Otwiera modal z formularzem dodawania emocji.
- */
- function handleMapClick(e) {
- const { lat, lng } = e.latlng;
-
- // Zapisz współrzędne
- selectedCoordinates = { latitude: lat, longitude: lng };
-
- // Wyświetl współrzędne w modalu
- document.getElementById('coordinatesDisplay').textContent =
- `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
-
- // Sprawdź proximity matching
- checkProximityAndShowInfo(lat, lng);
-
- // Otwórz modal
- addEmotionModal.show();
- }
-
- /**
- * Sprawdza czy w pobliżu klikniętego punktu (w promieniu proximityRadius)
- * istnieje już jakaś lokalizacja i wyświetla info o tym w modalu.
- */
- function checkProximityAndShowInfo(lat, lng) {
- const proximityInfo = document.getElementById('proximityInfo');
- const proximityText = document.getElementById('proximityText');
- const locationNameContainer = document.getElementById('locationNameContainer');
-
- // Pobierz wszystkie markery z cluster group
- const allMarkers = markerClusterGroup.getLayers();
-
- // Znajdź najbliższy marker w promieniu
- let closestMarker = null;
- let minDistance = Infinity;
-
- allMarkers.forEach(marker => {
- const markerLatLng = marker.getLatLng();
- const distance = map.distance([lat, lng], markerLatLng);
-
- if (distance < proximityRadius && distance < minDistance) {
- minDistance = distance;
- closestMarker = marker;
- }
- });
-
- // Pokaż info jeśli znaleziono bliską lokalizację
- if (closestMarker) {
- const popupContent = closestMarker.getPopup().getContent();
-
- // Wyciągnij nazwę lokalizacji z popup HTML (parsing)
- const tempDiv = document.createElement('div');
- tempDiv.innerHTML = popupContent;
- const locationName = tempDiv.querySelector('h6').textContent;
-
- proximityText.textContent =
- `Twoja ocena zostanie przypisana do: "${locationName}" (${Math.round(minDistance)}m od kliknięcia)`;
- proximityInfo.classList.remove('d-none');
- proximityInfo.classList.add('alert-info');
-
- // Ukryj pole nazwy - przypisujemy do istniejącej lokalizacji
- locationNameContainer.classList.add('d-none');
- } else {
- proximityText.textContent = 'Utworzysz nową lokalizację w tym miejscu';
- proximityInfo.classList.remove('d-none');
- proximityInfo.classList.add('alert-warning');
-
- // Pokaż pole nazwy - użytkownik tworzy nową lokalizację
- locationNameContainer.classList.remove('d-none');
- }
- }
-
- /**
- * Inicjalizacja interaktywnych gwiazdek (click, hover preview).
- */
- function initStarRating() {
- const starRating = document.getElementById('starRating');
- const stars = starRating.querySelectorAll('label');
- const radios = starRating.querySelectorAll('input[type="radio"]');
-
- // Hover effect - preview
- stars.forEach((star, index) => {
- star.addEventListener('mouseenter', function() {
- // Podświetl gwiazdki od prawej do tej nad którą jest hover
- for (let i = stars.length - 1; i >= index; i--) {
- stars[i].style.color = '#f39c12';
+ }
+
+ function getStarsHTML(value) {
+ if (!value) return '';
+
+ const fullStars = Math.floor(value);
+ const hasHalfStar = (value % 1) >= 0.5;
+ let html = '';
+
+ for (let i = 0; i < 5; i++) {
+ if (i < fullStars) {
+ html += '★';
+ } else if (i === fullStars && hasHalfStar) {
+ html += '☆';
+ } else {
+ html += '☆';
+ }
+ }
+
+ return `
${html}`;
+ }
+
+ // === UTILITIES ===
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ function pluralize(count) {
+ if (count === 1) return 'ocenie';
+ if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) {
+ return 'ocenach';
}
- // Przyciemnij pozostałe
- for (let i = index - 1; i >= 0; i--) {
- stars[i].style.color = '#ddd';
+ return 'ocenach';
+ }
+
+ function showError(message) {
+ alert(message);
+ }
+
+ function debounce(func, delay) {
+ return function (...args) {
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => func.apply(this, args), delay);
+ };
+ }
+
+ // === ADD EMOTION FEATURE ===
+
+ /**
+ * Inicjalizacja funkcjonalności dodawania emocji.
+ * Wywoływana tylko dla zalogowanych użytkowników.
+ */
+ function initAddEmotionFeature() {
+ // Inicjalizacja Bootstrap Modal
+ const modalElement = document.getElementById('addEmotionModal');
+ if (!modalElement) {
+ console.warn('Modal #addEmotionModal nie został znaleziony w HTML');
+ return;
+ }
+
+ addEmotionModal = new bootstrap.Modal(modalElement);
+
+ // Event listener dla kliknięcia na mapę
+ map.on('click', handleMapClick);
+
+ // Dodaj wizualną wskazówkę (cursor: crosshair)
+ document.getElementById('map').classList.add('clickable-map');
+
+ // Inicjalizacja interaktywnych gwiazdek
+ initStarRating();
+
+ // Event listener dla przycisku Submit
+ document.getElementById('submitEmotion').addEventListener('click', handleEmotionSubmit);
+
+ // Event listener dla zamknięcia modala - reset formularza
+ modalElement.addEventListener('hidden.bs.modal', resetEmotionForm);
+ }
+
+ /**
+ * Obsługa kliknięcia na mapę.
+ * Otwiera modal z formularzem dodawania emocji.
+ */
+ function handleMapClick(e) {
+ const { lat, lng } = e.latlng;
+
+ // Zapisz współrzędne
+ selectedCoordinates = { latitude: lat, longitude: lng };
+
+ // Wyświetl współrzędne w modalu
+ document.getElementById('coordinatesDisplay').textContent =
+ `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
+
+ // Sprawdź proximity matching
+ checkProximityAndShowInfo(lat, lng);
+
+ // Otwórz modal
+ addEmotionModal.show();
+ }
+
+ /**
+ * Sprawdza czy w pobliżu klikniętego punktu (w promieniu proximityRadius)
+ * istnieje już jakaś lokalizacja i wyświetla info o tym w modalu.
+ */
+ function checkProximityAndShowInfo(lat, lng) {
+ const proximityInfo = document.getElementById('proximityInfo');
+ const proximityText = document.getElementById('proximityText');
+ const locationNameContainer = document.getElementById('locationNameContainer');
+
+ // POPRAWKA: Sprawdzamy dane w pamięci (currentLocationsData) a nie markery
+ // Dzięki temu działa to także, gdy Heatmapa jest włączona i markery są ukryte.
+ let closestLocation = null;
+ let minDistance = Infinity;
+
+ currentLocationsData.forEach(loc => {
+ const dist = map.distance([lat, lng], [loc.coordinates.latitude, loc.coordinates.longitude]);
+ if (dist < proximityRadius && dist < minDistance) {
+ minDistance = dist;
+ closestLocation = loc;
+ }
+ });
+
+ // Pokaż info jeśli znaleziono bliską lokalizację
+ if (closestLocation) {
+ proximityText.textContent =
+ `Twoja ocena zostanie przypisana do: "${closestLocation.name}" (${Math.round(minDistance)}m od kliknięcia)`;
+ proximityInfo.classList.remove('d-none');
+ proximityInfo.classList.add('alert-info');
+
+ // Ukryj pole nazwy - przypisujemy do istniejącej lokalizacji
+ locationNameContainer.classList.add('d-none');
+ } else {
+ proximityText.textContent = 'Utworzysz nową lokalizację w tym miejscu';
+ proximityInfo.classList.remove('d-none');
+ proximityInfo.classList.add('alert-warning');
+
+ // Pokaż pole nazwy - użytkownik tworzy nową lokalizację
+ locationNameContainer.classList.remove('d-none');
+ }
+ }
+
+ /**
+ * Inicjalizacja interaktywnych gwiazdek (click, hover preview).
+ */
+ function initStarRating() {
+ const starRating = document.getElementById('starRating');
+ const stars = starRating.querySelectorAll('label');
+ const radios = starRating.querySelectorAll('input[type="radio"]');
+
+ // Hover effect - preview
+ stars.forEach((star, index) => {
+ star.addEventListener('mouseenter', function () {
+ // Podświetl gwiazdki od prawej do tej nad którą jest hover
+ for (let i = stars.length - 1; i >= index; i--) {
+ stars[i].style.color = '#f39c12';
+ }
+ // Przyciemnij pozostałe
+ for (let i = index - 1; i >= 0; i--) {
+ stars[i].style.color = '#ddd';
+ }
+ });
+ });
+
+ // Usunięcie hover - powrót do checked state
+ starRating.addEventListener('mouseleave', function () {
+ const checkedRadio = starRating.querySelector('input[type="radio"]:checked');
+ if (checkedRadio) {
+ updateStarsDisplay(checkedRadio);
+ } else {
+ // Wszystkie gwiazdki szare
+ stars.forEach(star => star.style.color = '#ddd');
+ }
+ });
+
+ // Click - zaznacz i zapisz
+ radios.forEach(radio => {
+ radio.addEventListener('change', function () {
+ updateStarsDisplay(this);
+ });
+ });
+ }
+
+ /**
+ * Aktualizuje wyświetlanie gwiazdek na podstawie zaznaczonego radio.
+ */
+ function updateStarsDisplay(checkedRadio) {
+ const starRating = document.getElementById('starRating');
+ const stars = starRating.querySelectorAll('label');
+ const value = parseInt(checkedRadio.value, 10);
+
+ // Wypełnij gwiazdki od prawej (5) do zaznaczonej wartości
+ stars.forEach((star, index) => {
+ const starValue = 5 - index; // gwiazdki są w odwrotnej kolejności w HTML
+ if (starValue <= value) {
+ star.style.color = '#f39c12'; // żółty
+ } else {
+ star.style.color = '#ddd'; // szary
+ }
+ });
+ }
+
+ /**
+ * Obsługa wysłania formularza - dodanie emocji.
+ */
+ async function handleEmotionSubmit() {
+ // Pobierz wartości z formularza
+ const emotionalValue = document.querySelector('input[name="emotional_value"]:checked');
+ const privacyStatus = document.getElementById('privacyStatus').value;
+ const locationNameInput = document.getElementById('locationName');
+ const locationNameContainer = document.getElementById('locationNameContainer');
+ const commentInput = document.getElementById('emotionComment');
+
+ // Walidacja
+ if (!emotionalValue) {
+ showEmotionError('Musisz wybrać ocenę (kliknij na gwiazdki).');
+ return;
+ }
+
+ // Ukryj błędy
+ hideEmotionErrors();
+
+ // Pokaż spinner
+ const submitBtn = document.getElementById('submitEmotion');
+ const submitText = document.getElementById('submitText');
+ const submitSpinner = document.getElementById('submitSpinner');
+
+ submitBtn.disabled = true;
+ submitText.classList.add('d-none');
+ submitSpinner.classList.remove('d-none');
+
+ // Przygotuj dane lokalizacji
+ const locationData = {
+ coordinates: {
+ latitude: selectedCoordinates.latitude,
+ longitude: selectedCoordinates.longitude
+ }
+ };
+
+ if (!locationNameContainer.classList.contains('d-none')) {
+ const locationName = locationNameInput.value.trim();
+ if (locationName) {
+ locationData.name = locationName;
+ }
+ }
+
+ // Przygotuj payload
+ const payload = {
+ location: locationData,
+ emotional_value: parseInt(emotionalValue.value, 10),
+ privacy_status: privacyStatus,
+ comment: commentInput ? commentInput.value.trim() : ''
+ };
+
+ try {
+ const csrfToken = getCsrfToken();
+
+ const response = await fetch(emotionPointsUrl, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ // Sprawdź czy odpowiedź to JSON
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ const errorData = await response.json();
+ handleApiErrors(errorData);
+ } else {
+ console.error('SERVER ERROR (HTML):', await response.text());
+ throw new Error(`Błąd serwera (${response.status}). Zobacz konsolę (F12) po szczegóły.`);
+ }
+ return;
+ }
+
+ const data = await response.json();
+ handleEmotionSuccess(data);
+
+ } catch (error) {
+ console.error('Network error:', error);
+ showEmotionError(error.message || 'Błąd połączenia. Sprawdź połączenie internetowe i spróbuj ponownie.');
+ } finally {
+ submitBtn.disabled = false;
+ submitText.classList.remove('d-none');
+ submitSpinner.classList.add('d-none');
}
- });
- });
-
- // Usunięcie hover - powrót do checked state
- starRating.addEventListener('mouseleave', function() {
- const checkedRadio = starRating.querySelector('input[type="radio"]:checked');
- if (checkedRadio) {
- updateStarsDisplay(checkedRadio);
- } else {
- // Wszystkie gwiazdki szare
+ }
+
+ function handleEmotionSuccess(data) {
+ addEmotionModal.hide();
+
+ const toastElement = document.getElementById('successToast');
+ const toastBody = document.getElementById('successMessage');
+ const locationName = data.location?.name || 'lokalizacja';
+ toastBody.textContent = `Twoja ocena została zapisana dla: ${locationName}`;
+
+ const toast = new bootstrap.Toast(toastElement, {
+ autohide: true,
+ delay: 4000
+ });
+ toast.show();
+
+ loadVisibleLocations(true);
+ }
+
+ function handleApiErrors(errorData) {
+ let errorMessage = 'Wystąpił błąd podczas dodawania oceny.';
+ if (errorData.emotional_value) {
+ errorMessage = Array.isArray(errorData.emotional_value)
+ ? errorData.emotional_value[0]
+ : errorData.emotional_value;
+ } else if (errorData.location) {
+ // ... (skrócone dla czytelności, logika taka sama jak w Twoim pliku)
+ } else if (errorData.detail) {
+ errorMessage = errorData.detail;
+ }
+ showEmotionError(errorMessage);
+ }
+
+ function showEmotionError(message) {
+ const errorsDiv = document.getElementById('emotionErrors');
+ errorsDiv.textContent = message;
+ errorsDiv.classList.remove('d-none');
+ }
+
+ function hideEmotionErrors() {
+ const errorsDiv = document.getElementById('emotionErrors');
+ errorsDiv.classList.add('d-none');
+ }
+
+ function resetEmotionForm() {
+ const radios = document.querySelectorAll('input[name="emotional_value"]');
+ radios.forEach(radio => radio.checked = false);
+
+ const stars = document.querySelectorAll('#starRating label');
stars.forEach(star => star.style.color = '#ddd');
- }
- });
-
- // Click - zaznacz i zapisz
- radios.forEach(radio => {
- radio.addEventListener('change', function() {
- updateStarsDisplay(this);
- });
- });
- }
-
- /**
- * Aktualizuje wyświetlanie gwiazdek na podstawie zaznaczonego radio.
- */
- function updateStarsDisplay(checkedRadio) {
- const starRating = document.getElementById('starRating');
- const stars = starRating.querySelectorAll('label');
- const value = parseInt(checkedRadio.value, 10);
-
- // Wypełnij gwiazdki od prawej (5) do zaznaczonej wartości
- stars.forEach((star, index) => {
- const starValue = 5 - index; // gwiazdki są w odwrotnej kolejności w HTML
- if (starValue <= value) {
- star.style.color = '#f39c12'; // żółty
- } else {
- star.style.color = '#ddd'; // szary
- }
- });
- }
-
- /**
- * Obsługa wysłania formularza - dodanie emocji.
- */
- async function handleEmotionSubmit() {
- // Pobierz wartości z formularza
- const emotionalValue = document.querySelector('input[name="emotional_value"]:checked');
- const privacyStatus = document.getElementById('privacyStatus').value;
- const locationNameInput = document.getElementById('locationName');
- const locationNameContainer = document.getElementById('locationNameContainer');
- const commentInput = document.getElementById('emotionComment');
-
- // Walidacja
- if (!emotionalValue) {
- showEmotionError('Musisz wybrać ocenę (kliknij na gwiazdki).');
- return;
- }
-
- // Ukryj błędy
- hideEmotionErrors();
-
- // Pokaż spinner
- const submitBtn = document.getElementById('submitEmotion');
- const submitText = document.getElementById('submitText');
- const submitSpinner = document.getElementById('submitSpinner');
-
- submitBtn.disabled = true;
- submitText.classList.add('d-none');
- submitSpinner.classList.remove('d-none');
-
- // Przygotuj dane lokalizacji
- const locationData = {
- coordinates: {
- latitude: selectedCoordinates.latitude,
- longitude: selectedCoordinates.longitude
- }
- };
- if (!locationNameContainer.classList.contains('d-none')) {
- const locationName = locationNameInput.value.trim();
- if (locationName) {
- locationData.name = locationName;
- }
+ document.getElementById('privacyStatus').value = 'public';
+
+ const locationNameInput = document.getElementById('locationName');
+ const locationNameContainer = document.getElementById('locationNameContainer');
+ locationNameInput.value = '';
+ locationNameContainer.classList.add('d-none');
+
+ const commentInput = document.getElementById('emotionComment');
+ if (commentInput) commentInput.value = '';
+
+ hideEmotionErrors();
+ const proximityInfo = document.getElementById('proximityInfo');
+ proximityInfo.classList.add('d-none');
+ proximityInfo.classList.remove('alert-info', 'alert-warning');
+
+ selectedCoordinates = null;
+ document.getElementById('coordinatesDisplay').textContent = '-';
}
- // Przygotuj payload
- const payload = {
- location: locationData,
- emotional_value: parseInt(emotionalValue.value, 10),
- privacy_status: privacyStatus,
- comment: commentInput ? commentInput.value.trim() : ''
- };
+ function getCsrfToken() {
+ const name = 'csrftoken';
+ 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;
+ }
- try {
- const csrfToken = getCsrfToken();
+ // === NOWE FUNKCJE HEATMAPY (Dodane na końcu) ===
+
+ function initHeatmapControls() {
+ const toggle = document.getElementById('heatmapToggle');
+ const radiusInput = document.getElementById('heatmapRadius');
+ const settingsDiv = document.getElementById('heatmapSettings');
+
+ if (toggle) {
+ toggle.addEventListener('change', function (e) {
+ isHeatmapActive = e.target.checked;
+ toggleHeatmapView();
+
+ // Pokaż/ukryj suwak
+ if (isHeatmapActive) {
+ settingsDiv.classList.remove('d-none');
+ } else {
+ settingsDiv.classList.add('d-none');
+ }
+ });
+ }
- const response = await fetch(emotionPointsUrl, {
- method: 'POST',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'X-CSRFToken': csrfToken
- },
- body: JSON.stringify(payload)
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- handleApiErrors(errorData);
- return;
- }
-
- const data = await response.json();
- handleEmotionSuccess(data);
-
- } catch (error) {
- console.error('Network error:', error);
- showEmotionError('Błąd połączenia. Sprawdź połączenie internetowe i spróbuj ponownie.');
- } finally {
- submitBtn.disabled = false;
- submitText.classList.remove('d-none');
- submitSpinner.classList.add('d-none');
- }
- }
-
- function handleEmotionSuccess(data) {
- addEmotionModal.hide();
-
- const toastElement = document.getElementById('successToast');
- const toastBody = document.getElementById('successMessage');
- const locationName = data.location?.name || 'lokalizacja';
- toastBody.textContent = `Twoja ocena została zapisana dla: ${locationName}`;
-
- const toast = new bootstrap.Toast(toastElement, {
- autohide: true,
- delay: 4000
- });
- toast.show();
-
- loadVisibleLocations(true);
- }
-
- function handleApiErrors(errorData) {
- let errorMessage = 'Wystąpił błąd podczas dodawania oceny.';
- if (errorData.emotional_value) {
- errorMessage = Array.isArray(errorData.emotional_value)
- ? errorData.emotional_value[0]
- : errorData.emotional_value;
- } else if (errorData.location) {
- // ... (skrócone dla czytelności, logika taka sama jak w Twoim pliku)
- } else if (errorData.detail) {
- errorMessage = errorData.detail;
- }
- showEmotionError(errorMessage);
- }
-
- function showEmotionError(message) {
- const errorsDiv = document.getElementById('emotionErrors');
- errorsDiv.textContent = message;
- errorsDiv.classList.remove('d-none');
- }
-
- function hideEmotionErrors() {
- const errorsDiv = document.getElementById('emotionErrors');
- errorsDiv.classList.add('d-none');
- }
-
- function resetEmotionForm() {
- const radios = document.querySelectorAll('input[name="emotional_value"]');
- radios.forEach(radio => radio.checked = false);
-
- const stars = document.querySelectorAll('#starRating label');
- stars.forEach(star => star.style.color = '#ddd');
-
- document.getElementById('privacyStatus').value = 'public';
-
- const locationNameInput = document.getElementById('locationName');
- const locationNameContainer = document.getElementById('locationNameContainer');
- locationNameInput.value = '';
- locationNameContainer.classList.add('d-none');
-
- const commentInput = document.getElementById('emotionComment');
- if (commentInput) commentInput.value = '';
-
- hideEmotionErrors();
- const proximityInfo = document.getElementById('proximityInfo');
- proximityInfo.classList.add('d-none');
- proximityInfo.classList.remove('alert-info', 'alert-warning');
-
- selectedCoordinates = null;
- document.getElementById('coordinatesDisplay').textContent = '-';
- }
-
- function getCsrfToken() {
- const name = 'csrftoken';
- 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;
+ if (radiusInput) {
+ radiusInput.addEventListener('input', function (e) {
+ const radius = parseInt(e.target.value, 10);
+ if (heatLayer) {
+ heatLayer.setOptions({ radius: radius });
+ }
+ });
}
- }
}
- return cookieValue;
- }
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
+ function toggleHeatmapView() {
+ if (!heatLayer) return;
+
+ if (isHeatmapActive) {
+ // Włączamy Heatmapę -> Ukrywamy Markery
+ if (map.hasLayer(markerClusterGroup)) {
+ map.removeLayer(markerClusterGroup);
+ }
+ if (!map.hasLayer(heatLayer)) {
+ map.addLayer(heatLayer);
+ }
+ // Upewnij się, że heatmapa ma dane
+ updateHeatmapData(currentLocationsData);
+ } else {
+ // Włączamy Markery -> Ukrywamy Heatmapę
+ if (map.hasLayer(heatLayer)) {
+ map.removeLayer(heatLayer);
+ }
+ if (!map.hasLayer(markerClusterGroup)) {
+ map.addLayer(markerClusterGroup);
+ }
+ }
+ }
+
+ function updateHeatmapData(locations) {
+ if (!heatLayer) return;
+
+ // Mapujemy lokalizacje na format [lat, lng, intensity]
+ const heatPoints = locations.map(loc => {
+ // Intensity (0.0 - 1.0)
+ // Jeśli jest ocena (1-5), normalizujemy ją: 5.0 -> 1.0 (Najmocniej świeci)
+ // Jeśli brak oceny (sam komentarz), dajemy małą wagę (np. 0.3)
+ let intensity = 0.3;
+ if (loc.avg_emotional_value) {
+ intensity = loc.avg_emotional_value / 5.0;
+ }
+
+ return [
+ loc.coordinates.latitude,
+ loc.coordinates.longitude,
+ intensity
+ ];
+ });
+
+ heatLayer.setLatLngs(heatPoints);
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
})();
\ No newline at end of file