@@ -24,6 +24,58 @@ function sanitizeHtml(html) {
2424 return doc . body . innerHTML ;
2525}
2626
27+ /**
28+ * Build a GeoJSON point feature for an observation if presence is true and coordinates are valid.
29+ * Returns null for absence entries or invalid coordinates to skip rendering.
30+ * @param {number } lat
31+ * @param {number } lon
32+ * @param {boolean|string } presenceRaw
33+ * @param {string } date
34+ * @param {Object } properties
35+ * @returns {Object|null }
36+ */
37+ function buildObservationFeature ( lat , lon , presenceRaw , date , properties = { } ) {
38+ const isPresent = typeof presenceRaw === 'string'
39+ ? presenceRaw . toLowerCase ( ) === 'true'
40+ : Boolean ( presenceRaw ) ;
41+
42+ if ( ! isPresent || ! Number . isFinite ( lat ) || ! Number . isFinite ( lon ) ) {
43+ return null ;
44+ }
45+
46+ return {
47+ type : 'Feature' ,
48+ geometry : {
49+ type : 'Point' ,
50+ coordinates : [ lon , lat ]
51+ } ,
52+ properties : {
53+ ...properties ,
54+ presence : true ,
55+ date
56+ }
57+ } ;
58+ }
59+
60+ /**
61+ * Attempt to load observation data as GeoJSON when available
62+ * @param {string } url
63+ * @returns {Promise<Object|null> }
64+ */
65+ async function tryLoadObservationGeoJSON ( url ) {
66+ const lowerUrl = url . toLowerCase ( ) ;
67+ try {
68+ const response = await fetch ( url ) ;
69+ const contentType = ( response . headers . get ( 'content-type' ) || '' ) . toLowerCase ( ) ;
70+ if ( response . ok && ( contentType . includes ( 'json' ) || lowerUrl . endsWith ( '.json' ) || lowerUrl . endsWith ( '.geojson' ) ) ) {
71+ return await response . json ( ) ;
72+ }
73+ } catch ( err ) {
74+ console . warn ( 'Could not load observations as GeoJSON:' , err ) ;
75+ }
76+ return null ;
77+ }
78+
2779/**
2880 * Get the currently selected model
2981 * @returns {string }
@@ -184,22 +236,18 @@ function setupNavigation() {
184236 * Set up layer control event listeners
185237 */
186238function setupLayerControls ( ) {
187- const riskLayerRadio = document . getElementById ( 'risk-layer' ) ;
188- const observationsLayerRadio = document . getElementById ( 'observations-layer' ) ;
239+ const riskLayerCheckbox = document . getElementById ( 'risk-layer' ) ;
240+ const observationsLayerCheckbox = document . getElementById ( 'observations-layer' ) ;
189241
190- if ( riskLayerRadio ) {
191- riskLayerRadio . addEventListener ( 'change' , async function ( ) {
192- if ( this . checked ) {
193- await handleLayerSelection ( 'risk' ) ;
194- }
242+ if ( riskLayerCheckbox ) {
243+ riskLayerCheckbox . addEventListener ( 'change' , async function ( ) {
244+ await handleLayerSelection ( 'risk' , this . checked ) ;
195245 } ) ;
196246 }
197247
198- if ( observationsLayerRadio ) {
199- observationsLayerRadio . addEventListener ( 'change' , async function ( ) {
200- if ( this . checked ) {
201- await handleLayerSelection ( 'observations' ) ;
202- }
248+ if ( observationsLayerCheckbox ) {
249+ observationsLayerCheckbox . addEventListener ( 'change' , async function ( ) {
250+ await handleLayerSelection ( 'observations' , this . checked ) ;
203251 } ) ;
204252 }
205253
@@ -242,54 +290,46 @@ function setupLayerControls() {
242290}
243291
244292/**
245- * Ensure only one layer is active and handle map transitions for observations
293+ * Toggle layer visibility and ensure data is available
246294 * @param {string } selectedLayer
295+ * @param {boolean } isVisible
296+ * @returns {Promise<void> }
247297 */
248- async function handleLayerSelection ( selectedLayer ) {
298+ async function handleLayerSelection ( selectedLayer , isVisible ) {
249299 if ( ! mapManager || ! mapManager . currentRegion ) {
250300 return ;
251301 }
252302
253303 const regionKey = mapManager . currentRegion ;
254304 const region = CONFIG . regions [ regionKey ] ;
255- const riskLayerRadio = document . getElementById ( 'risk-layer' ) ;
256- const observationsLayerRadio = document . getElementById ( 'observations-layer' ) ;
257-
258305 if ( selectedLayer === 'observations' ) {
259- if ( riskLayerRadio ) riskLayerRadio . checked = false ;
260- if ( observationsLayerRadio ) observationsLayerRadio . checked = true ;
261-
262- // If a Mapbox map is active, replace it with Leaflet so observations render correctly
263- const wasMapbox = Boolean ( mapManager . mbMap ) ;
264- if ( mapManager . mbMap ) {
265- mapManager . mbMap . remove ( ) ;
266- mapManager . mbMap = null ;
267- }
268-
269- if ( ! mapManager . map || wasMapbox ) {
270- mapManager . initMap ( ) ;
271- if ( region ?. center && region ?. zoom ) {
272- mapManager . map . setView ( region . center , region . zoom ) ;
273- }
306+ if ( ! isVisible ) {
307+ mapManager . toggleLayer ( 'observations' , false ) ;
308+ return ;
274309 }
275-
276- // Pass `true` to disable the selector while observations are shown
277- setBasemapSelectorAvailability ( true , 'Basemap selection is disabled in observations view' ) ;
278- mapManager . toggleLayer ( 'risk' , false ) ;
279310 await loadObservationOverlay ( regionKey ) ;
280311 mapManager . toggleLayer ( 'observations' , true ) ;
281312 return ;
282313 }
283314
284315 if ( selectedLayer === 'risk' ) {
285- if ( riskLayerRadio ) riskLayerRadio . checked = true ;
286- if ( observationsLayerRadio ) observationsLayerRadio . checked = false ;
316+ if ( ! isVisible ) {
317+ mapManager . toggleLayer ( 'risk' , false ) ;
318+ return ;
319+ }
287320
288321 const selectedModel = getSelectedModel ( ) ;
289322 if ( regionKey === 'spain' && region ?. dataSources ?. mosquitoAlertES ?. enabled && selectedModel === 'mosquito-alert-municipalities' ) {
290- const datePicker = document . getElementById ( 'data-date-picker' ) ;
291- const dateToLoad = datePicker && datePicker . value ? datePicker . value : new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
292- await loadSpainMosquitoAlertData ( dateToLoad , selectedModel ) ;
323+ const mbMap = mapManager . mbMap ;
324+ const hasMapboxLayers = Boolean (
325+ mbMap ?. getLayer ( 'muni-high-res' ) ||
326+ mbMap ?. getLayer ( 'muni-low-res' )
327+ ) ;
328+ if ( ! hasMapboxLayers ) {
329+ const datePicker = document . getElementById ( 'data-date-picker' ) ;
330+ const dateToLoad = datePicker && datePicker . value ? datePicker . value : new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
331+ await loadSpainMosquitoAlertData ( dateToLoad , selectedModel ) ;
332+ }
293333 } else if ( regionKey === 'barcelona' && region ?. dataSources ?. mosquitoAlertBCN ?. enabled && ! mapManager . layers . geotiff ) {
294334 const datePicker = document . getElementById ( 'data-date-picker' ) ;
295335 const dateToLoad = datePicker && datePicker . value ? datePicker . value : new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
@@ -298,7 +338,6 @@ async function handleLayerSelection(selectedLayer) {
298338 await mapManager . loadRegion ( regionKey ) ;
299339 }
300340
301- mapManager . toggleLayer ( 'observations' , false ) ;
302341 mapManager . toggleLayer ( 'risk' , true ) ;
303342 }
304343}
@@ -666,33 +705,35 @@ async function loadObservationOverlay(regionKey) {
666705 const visible = checkbox ? checkbox . checked : false ;
667706
668707 try {
669- const csvData = await dataLoader . loadCSV ( observationsUrl ) ;
670- const features = ( csvData || [ ] ) . map ( row => {
671- const lat = parseFloat ( row . lat ?? row . Latitude ?? row . latitude ) ;
672- const lon = parseFloat ( row . lon ?? row . Longitude ?? row . longitude ) ;
673- const presenceRaw = row . presence ?? row . PRESENCE ?? row . Presence ;
674- const date = row . date || row . Date || '' ;
675- const isPresent = typeof presenceRaw === 'string' ?
676- presenceRaw . toLowerCase ( ) === 'true' :
677- Boolean ( presenceRaw ) ;
678-
679- // Only filter out invalid coordinates, not based on presence
680- if ( ! Number . isFinite ( lat ) || ! Number . isFinite ( lon ) ) {
681- return null ;
682- }
708+ let features = [ ] ;
709+ const geojsonData = await tryLoadObservationGeoJSON ( observationsUrl ) ;
710+
711+ if ( geojsonData && Array . isArray ( geojsonData . features ) ) {
712+ features = geojsonData . features . map ( feature => {
713+ const coords = feature ?. geometry ?. coordinates || [ ] ;
714+ const lon = parseFloat ( coords [ 0 ] ) ;
715+ const lat = parseFloat ( coords [ 1 ] ) ;
716+ const presenceRaw = feature ?. properties ?. presence ??
717+ feature ?. properties ?. PRESENCE ??
718+ feature ?. properties ?. Presence ;
719+ const date = feature ?. properties ?. date ||
720+ feature ?. properties ?. Date ||
721+ feature ?. properties ?. DATE ||
722+ '' ;
723+ return buildObservationFeature ( lat , lon , presenceRaw , date , feature . properties || { } ) ;
724+ } ) . filter ( Boolean ) ;
725+ }
683726
684- return {
685- type : 'Feature' ,
686- geometry : {
687- type : 'Point' ,
688- coordinates : [ lon , lat ]
689- } ,
690- properties : {
691- presence : isPresent ,
692- date : date
693- }
694- } ;
695- } ) . filter ( Boolean ) ;
727+ if ( ! features . length ) {
728+ const csvData = await dataLoader . loadCSV ( observationsUrl ) ;
729+ features = ( csvData || [ ] ) . map ( row => {
730+ const lat = parseFloat ( row . lat ?? row . Latitude ?? row . latitude ) ;
731+ const lon = parseFloat ( row . lon ?? row . Longitude ?? row . longitude ) ;
732+ const presenceRaw = row . presence ?? row . PRESENCE ?? row . Presence ;
733+ const date = row . date || row . Date || '' ;
734+ return buildObservationFeature ( lat , lon , presenceRaw , date , { } ) ;
735+ } ) . filter ( Boolean ) ;
736+ }
696737
697738 const geojson = {
698739 type : 'FeatureCollection' ,
@@ -701,7 +742,6 @@ async function loadObservationOverlay(regionKey) {
701742
702743 mapManager . addObservationLayer ( geojson , visible , {
703744 fillColor : '#ffd92f' , // Yellow for presence points
704- fillColorAbsence : '#a6cee3' , // Light blue for absence points
705745 strokeColor : '#000000' ,
706746 strokeWidth : 1.5 ,
707747 radius : regionKey === 'barcelona' ? 7 : 6 ,
0 commit comments