I have a Laravel app that is using javascript to display a Google map with several layers. One of those layers is an “Incident” layer. The Incident layer displays a particular icon based on the type of incident that is currently in progress (active) within the City limits. Right now, I have the app displaying the currently “open” incidents, with an update occurring every sixty seconds. The issue is that there is a “blinking” effect either caused by the speed of the query or a coding factor that I do not see. It clear is occuring because of the query and the reloading of the data and replacing the icons. I would appreciate any suggestions on how to clean this up and make it work better.
Here is my map.blade.php file:
@php
$pageTitle = 'Active Map';
@endphp
@extends('layouts.master')
@section('title', $pageTitle)
@push('headerCode')
<style>
#header,
#nav,
.page-sidebar {
}
#main {
margin-left: 0 !important;
padding-top: 0 !important;
}
html, body, #js-page-content, #google-maps {
height: 100% !important;
margin: 0;
padding: 0;
}
</style>
@endpush
@section('headerStyle')
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
<meta charset="utf-8"/>
@endsection
@section('content')
<main id="js-page-content" role="main" class="page-content">
<x-google-map :layers="$mapLayers" mapId="hydrantMap" :height="'100vh'"/>
</main>
@endsection
@push('footerScript')
@endpush
Here is my x-google-map component included in the code above.
<div style="position: relative;">
<div id="{{ $mapId ?? 'google-maps' }}" style="height: {{ $height ?? '80vh' }};"></div>
</div>
<div id="layer-toggle-menu" class="map-control">
<div><strong>Map Layers</strong></div>
@foreach ($layers as $layer)
<label>
<input type="checkbox" class="layer-toggle" data-layer="{{ $layer }}" checked>
{{ ucwords(str_replace('_', ' ', $layer)) }}
</label>
@endforeach
</div>
@push('footerScript')
@php
$mapCenter = config('services.google_maps.center'); // "41.0534,-73.5387"
[$lat, $lng] = explode(',', $mapCenter);
@endphp
<script>
const MAP_CONFIG = {
mapId: '{{ $mapId ?? 'google-maps' }}',
center: {
lat: parseFloat('{{ $lat }}'),
lng: parseFloat('{{ $lng }}')
},
zoom: {{ $zoom ?? 13 }},
layers: @json($layers),
apiKey: '{{ config('services.google_maps.key') }}'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
{{-- City Boundaries --}}
@vite('resources/js/maps/index.js')
@endpush
Here is my index.js file, included above:
// resources/js/maps/index.js
import '../../sass/maps/map.scss'; // this ensures it’s included
import './map-loader.js';
import './layers/city-boundaries.js';
import './layers/fire-districts.js';
import './layers/hydrants.js';
import './layers/address-points.js';
import './layers/fire-stations.js';
import './layers/active-incidents.js';
And here is the active-incidents.js
export async function loadActiveIncidents(map) {
try {
const response = await fetch('/api/map/incidents');
const incidents = await response.json();
// Clear old markers (yes, this causes flicker — we’ll optimize after this works)
activeIncidentMarkers.forEach(marker => marker.setMap(null));
activeIncidentMarkers = [];
const infoWindow = new google.maps.InfoWindow();
const markers = [];
incidents.forEach(incident => {
if (!incident.latitude || !incident.longitude || !incident.id) {
console.warn('Skipping invalid incident:', incident);
return;
}
const position = {
lat: parseFloat(incident.latitude),
lng: parseFloat(incident.longitude)
};
const icon = {
url: getIncidentIcon(incident.incident_type),
scaledSize: new google.maps.Size(40, 40),
anchor: new google.maps.Point(20, 40)
};
const marker = new google.maps.Marker({
position,
map,
title: incident.incident_type,
icon
});
const vehicles = Array.isArray(incident.vehicles)
? incident.vehicles.filter(v => v.time_call_cleared === null)
: [];
const assignedUnits = vehicles.length > 0
? vehicles.map(v => v.vehicle_name).join(', ')
: 'None.';
const infoHtml = `
<a href="/cad/incidents/${incident.id}" style="cursor:pointer;">
<table style="width:300px;">
<tr>
<td><img src="${icon.url}" width="40" height="40"></td>
<td>
<strong>${incident.incident_type}</strong><br>
${incident.location_name ? `${incident.location_name}<br>` : ''}
${incident.location_address}${incident.location_apartment ? `, Apt ${incident.location_apartment}` : ''}<br>
<em>Units Assigned: ${assignedUnits}</em>
</td>
</tr>
</table>
</a>`;
marker.addListener('click', () => {
infoWindow.setContent(infoHtml);
infoWindow.open(map, marker);
});
markers.push(marker);
});
// Store markers
activeIncidentMarkers = markers;
window.loadedLayers.active_incidents = markers;
} catch (error) {
console.error('Failed to load active incidents:', error);
}
}
//Icon resolution logic
function getIncidentIcon(incidentType) {
const type = (incidentType || '').toLowerCase();
const basePath = `${window.location.origin}/media/images/inci_type_icons/`;
const map = {
fire: 'fire_icon.png',
ems: 'sol_icon.png',
hazmat: 'haznmat_icon.png',
rescue: 'technical_rescue.png',
alarm: 'alarm_icon.png'
};
for (const key in map) {
if (type.includes(key)) {
return `${basePath}${map[key]}`;
}
}
return `${basePath}default.png`;
}
And here is my overall map-reload file required to load the map and all its elements.:
// Google Map-Loader.js
import {loadCityBoundaries} from './layers/city-boundaries.js';
import {loadFireDistricts} from './layers/fire-districts.js';
import {loadHydrants} from './layers/hydrants.js';
import {loadAddressPoints} from './layers/address-points.js';
import {loadFireStations} from './layers/fire-stations.js';
import {loadActiveIncidents} from './layers/active-incidents.js';
// Global variables to be shared with other layer files
window.loadedLayers = {
city_boundaries: null,
fire_districts: null,
hydrants: null,
address_points: null,
fire_stations: null,
active_incidents: null
};
document.addEventListener('DOMContentLoaded', function () {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${MAP_CONFIG.apiKey}&callback=initializeMap&libraries=geometry`;
script.onload = () => initializeMap();
document.head.appendChild(script);
});
function initializeMap() {
const map = new google.maps.Map(document.getElementById(MAP_CONFIG.mapId), {
center: MAP_CONFIG.center,
zoom: MAP_CONFIG.zoom,
styles: [
{
featureType: "poi",
elementType: "labels",
stylers: [{visibility: "off"}]
}
],
fullscreenControl: true,
mapTypeControl: true,
streetViewControl: true
});
window.hydrantMap = map;
window.sharedInfoWindow = new google.maps.InfoWindow(); // global shared info window
window.loadedLayers = window.loadedLayers || {};
window.sharedInfoWindow = window.sharedInfoWindow || new google.maps.InfoWindow();
const layers = MAP_CONFIG.layers || [];
// Inject Layer Toggle Menu into Map UI
const layerPanel = document.getElementById("layer-toggle-menu");
if (layerPanel) {
layerPanel.style.display = 'block';
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(layerPanel);
}
// Load only active layers
if (layers.includes('city_boundaries')) loadCityBoundaries(map);
if (layers.includes('fire_districts')) loadFireDistricts(map);
if (layers.includes('hydrants')) loadHydrants(map);
if (layers.includes('address_points')) loadAddressPoints(map);
if (layers.includes('fire_stations')) loadFireStations(map);
if (layers.includes('active_incidents')) loadActiveIncidents(map);
document.querySelectorAll('.layer-toggle').forEach(toggle => {
toggle.addEventListener('change', (e) => {
const layer = e.target.dataset.layer;
const isChecked = e.target.checked;
let layerObj = window.loadedLayers[layer];
if (layerObj) {
// Toggle visibility of already loaded layer
if (Array.isArray(layerObj)) {
layerObj.forEach(marker => marker.setMap(isChecked ? map : null));
} else if (layerObj.setMap) {
layerObj.setMap(isChecked ? map : null);
}
} else if (isChecked) {
// Load the layer if it's not already loaded
switch (layer) {
case 'city_boundaries':
window.loadedLayers[layer] = loadCityBoundaries(map);
break;
case 'fire_districts':
window.loadedLayers[layer] = loadFireDistricts(map);
break;
case 'fire_stations':
window.loadedLayers[layer] = loadFireStations(map);
break;
case 'hydrants':
window.loadedLayers[layer] = loadHydrants(map);
break;
case 'address_points':
window.loadedLayers[layer] = loadAddressPoints(map);
break;
case 'active-incidents':
window.loadedLayers[layer] = loadActiveIncidents(map);
break;
default:
console.warn(`No loader function defined for layer: "${layer}"`);
}
}
});
});
// POI Toggle Button (Place Names)
const togglePOIButton = document.createElement("button");
togglePOIButton.classList.add("custom-map-control-button");
togglePOIButton.innerHTML = `<i class="fa-regular fa-flag"></i>`;
togglePOIButton.title = "Toggle Place Names";
let poiLabelsVisible = false;
togglePOIButton.addEventListener("click", () => {
poiLabelsVisible = !poiLabelsVisible;
const icon = togglePOIButton.querySelector("i");
icon.classList.remove("fa-regular", "fa-solid");
icon.classList.add(poiLabelsVisible ? "fa-solid" : "fa-regular");
map.setOptions({
styles: poiLabelsVisible ? [] : [
{
featureType: "poi",
elementType: "labels",
stylers: [{visibility: "off"}]
}
]
});
});
// Measurement Tool
let measuring = false;
let showMenu = false;
let measureMarkers = [];
let measureLine = null;
let distanceOverlays = [];
const measureButton = document.createElement("button");
measureButton.innerHTML = `<i class="fas fa-ruler"></i>`;
measureButton.title = "Start Measuring";
measureButton.classList.add("custom-map-control-button");
const measureMenu = document.createElement("div");
measureMenu.classList.add("map-control", "measure-submenu");
measureMenu.style.display = "none";
measureMenu.innerHTML = `
<div><strong>Measurement</strong></div>
<label class="measure-menu-item" id="clear-measurement-option">🧹 Clear Measurement</label>
<label class="measure-menu-item" id="leave-measurement-option">🚪 Leave Measurement</label>
`;
const measureWrapper = document.createElement("div");
measureWrapper.style.position = "relative";
measureWrapper.appendChild(measureButton);
measureWrapper.appendChild(measureMenu);
measureButton.addEventListener("click", () => {
if (!measuring && !showMenu) {
measuring = true;
measureButton.classList.add("active");
measureMenu.style.display = "none";
} else if (measuring && !showMenu) {
showMenu = true;
measureMenu.style.display = "block";
}
});
measureMenu.querySelector("#clear-measurement-option").addEventListener("click", () => {
clearMeasurement();
});
measureMenu.querySelector("#leave-measurement-option").addEventListener("click", () => {
measuring = false;
showMenu = false;
measureMenu.style.display = "none";
measureButton.classList.remove("active");
});
map.addListener("click", (e) => {
if (!measuring) return;
const marker = new google.maps.Marker({
position: e.latLng,
map: map,
draggable: true
});
measureMarkers.push(marker);
marker.addListener("dragend", updateMeasurement);
updateMeasurement();
});
function clearMeasurement() {
measureMarkers.forEach(m => m.setMap(null));
measureMarkers = [];
if (measureLine) {
measureLine.setMap(null);
measureLine = null;
}
distanceOverlays.forEach(o => o.setMap(null));
distanceOverlays = [];
measuring = false;
showMenu = false;
measureMenu.style.display = "none";
measureButton.classList.remove("active");
}
function updateMeasurement() {
if (measureMarkers.length < 2) return;
if (measureLine) measureLine.setMap(null);
distanceOverlays.forEach(o => o.setMap(null));
distanceOverlays = [];
const path = measureMarkers.map(m => m.getPosition());
measureLine = new google.maps.Polyline({
path: path,
map: map,
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 2
});
for (let i = 0; i < path.length - 1; i++) {
const start = path[i];
const end = path[i + 1];
const distM = google.maps.geometry.spherical.computeDistanceBetween(start, end);
const distFt = (distM * 3.28084).toFixed(0);
const mid = google.maps.geometry.spherical.interpolate(start, end, 0.5);
distanceOverlays.push(createLabelOverlay(`${distFt} ft`, mid, 'segment-label'));
}
const totalM = google.maps.geometry.spherical.computeLength(path);
const totalFt = (totalM * 3.28084).toFixed(0);
distanceOverlays.push(createLabelOverlay(`<strong>${totalFt} ft total</strong>`, path[path.length - 1], 'total-label'));
}
function createLabelOverlay(text, position, styleClass) {
function Label(position, text, className) {
this.position = position;
this.text = text;
this.className = className;
}
Label.prototype = new google.maps.OverlayView();
Label.prototype.onAdd = function () {
const div = document.createElement("div");
div.className = this.className;
div.innerHTML = this.text;
this.div = div;
const panes = this.getPanes();
panes.floatPane.appendChild(div);
};
Label.prototype.draw = function () {
const projection = this.getProjection();
const point = projection.fromLatLngToDivPixel(this.position);
const div = this.div;
div.style.left = point.x + "px";
div.style.top = point.y + "px";
};
Label.prototype.onRemove = function () {
if (this.div) {
this.div.parentNode.removeChild(this.div);
this.div = null;
}
};
const overlay = new Label(position, text, styleClass);
overlay.setMap(map);
return overlay;
}
// Create Center-on-User button
const locateButton = document.createElement("button");
locateButton.classList.add("custom-map-control-button");
locateButton.innerHTML = `<i class="fas fa-crosshairs"></i>`;
locateButton.title = "Center Map on My Location";
locateButton.addEventListener("click", () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("Got position:", position);
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
map.setCenter(userLocation);
map.setZoom(16);
},
(error) => {
console.error("Geolocation error:", error);
switch (error.code) {
case error.PERMISSION_DENIED:
alert("Permission denied.");
break;
case error.POSITION_UNAVAILABLE:
alert("Position unavailable.");
break;
case error.TIMEOUT:
alert("Location request timed out.");
break;
default:
alert("Unknown error.");
break;
}
},
{timeout: 10000} // Optional: prevent indefinite hanging
);
} else {
alert("Geolocation not supported.");
}
});
// Create Reset View button (centers map to default config center)
const resetViewButton = document.createElement("button");
resetViewButton.classList.add("custom-map-control-button");
resetViewButton.innerHTML = `<i class="fas fa-map-marker-alt"></i>`;
resetViewButton.title = "Reset Map View";
resetViewButton.addEventListener("click", () => {
map.setCenter(MAP_CONFIG.center);
map.setZoom(MAP_CONFIG.zoom);
});
const weatherLayer = new google.maps.ImageMapType({
getTileUrl: function (coord, zoom) {
return `https://tile.openweathermap.org/map/precipitation_new/${zoom}/${coord.x}/${coord.y}.png?appid=9e47fd126a54994ba650ea17db9cf973`;
},
tileSize: new google.maps.Size(256, 256),
name: "Weather",
opacity: 0.6,
isPng: true
});
// Close shared InfoWindow when clicking on the map (outside of a marker)
map.addListener('click', () => {
window.sharedInfoWindow.close();
});
// Check incidents every 10 seconds
if (layers.includes('active_incidents')) {
loadActiveIncidents(map); // Initial load
setInterval(() => {
if (isLayerVisible('active_incidents')) {
loadActiveIncidents(map);
}
}, 60000);
}
function isLayerVisible(layerName) {
const checkbox = document.querySelector(`.layer-toggle[data-layer="${layerName}"]`);
return checkbox && checkbox.checked;
}
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(measureWrapper);
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(resetViewButton);
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(locateButton);
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(togglePOIButton);
}
window.initializeMap = initializeMap;
My web route looks like this:
Route::get('/api/map/incidents', [CadIncidentController::class, 'activeIncidents']);
and the controller method looks like this:
public function activeIncidents()
{
$incidents = CadIncident::with(['vehicles' => function ($query) {
// Optionally filter vehicles here (e.g., only active ones)
$query->select('id', 'cadincident_id', 'vehicle_name', 'time_call_cleared');
}])
->whereNull('time_call_closed')
->get()
->map(function ($incident) {
// Optionally add an icon URL using your helper
$incident->icon = getIncidentIcon($incident->incident_type);
return $incident;
});
return response()->json($incidents);
}