PageSourceSearch

https://www.vips-landbruk.no/static/js/frontpageMap.js

js vips-landbruk.no collected 2026-10-02 11:47:02 UTC 20,505 bytes, 668 lines download raw bytes

1/*
2 * Copyright (c) 2013-2023 NIBIO.
3 *
4 * This file is part of VIPSWeb 
5 * (see https://gitlab.nibio.no/VIPS/VIPSWeb).
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22/**
23 * @copyright 2018 <a href="http://www.nibio.no/">NIBIO</a>
24 * @author Tor-Einar Skog <[email protected]>
25 */
26
27/* ----------------------- Global elements ------------------- */
28var map;
29var FORECAST_LAYER = "forecastLayer";
30var OBSERVATION_LAYER = "observationLayer";
31//Using Bootstrap's popover plugin. See http://getbootstrap.com/javascript/#popovers
32var poiDetails = $("#popover");
33//Using Bootstrap's tooltip plugin. See http://getbootstrap.com/javascript/#tooltips
34var poiTitle = $('#tooltip');
35poiTitle.tooltip({
36  animation: false,
37  trigger: 'manual',
38  html: false
39});
40//Layer for popup
41var popOverlay = new ol.Overlay({
42  element: document.getElementById("popover")
43});
44//State variable
45var currentClickedFeature = false;
46var maxMapZoomForObservations = 7;
47var maxMapZoomForForecasts = 10;
48
49/* ------------- Forecasts related ------------------------------------- */
50var forecastLayer;
51/* ------------------------------------------------------------------------ */
52
53/* ------------- Observations related ------------------------------------- */
54//var allObservations = []; // Populated asynchronously
55var drawnFeatures = []; // Populated asynchronously
56var observationLayer;
57var currentDateInMillis;
58/* ------------------------------------------------------------------------ */
59
60/* - Relax restrictions on HTML in popup (https://getbootstrap.com/docs/3.4/javascript/#js-sanitizer) - */
61var myWhiteList = $.fn.tooltip.Constructor.DEFAULTS.whiteList;
62myWhiteList.table=[];
63myWhiteList.tbody=[];
64myWhiteList.tr=["onclick"];
65myWhiteList.td=["colspan"];
66myWhiteList.span=["onclick"]
67
68/* ------------------------------------------------------------------------ */
69
70/**
71 * Initializes the front page map, including layers for forecasts and observations
72 * @param {ol.Coordinate} lonLat - coordinates for the map's center (WGS84)
73 * @param {int} zoom - the zoom level (1-15, 1 is world wide view, 15 is greatest zoom)
74 * @param {string} mapAttribution - The text in the corner of the map giving credits where it's due
75 */
76function initFrontpageMap(lonLat, zoomLevel, mapAttribution)
77{
78	
79	if(settings.vipslogicServerName === undefined)
80	{
81		alert(gettext("Source hostname not defined."));
82		return;
83	}
84	
85	// ---------- Background layer is OpenStreetMap --------------
86	var backgroundLayer = new ol.layer.Tile({
87	   	 	source: new ol.source.OSM({
88				attributions: [mapAttribution]
89				})
90	    });
91	// Bing Maps example
92	/*var backgroundLayer = new ol.layer.Tile({
93	      source: new ol.source.BingMaps({
94	          key: 'AgBNAZzyzjb-tmdpNGxAQGtvVQryg5Vm3ZekaYVk2iwWNmu41CVxdPYjTXrKQ9ac',
95	          // imagerySet: "Aerial"
96	          //imagerySet: 'Road'
97	          imagerySet: "AerialWithLabels"
98	        })
99	      })
100	*/
101	
102	// ----------- The forecast layer is a KML layer, pure and simple --------
103	forecastLayer = new ol.layer.Vector({
104		source: null
105	});
106	
107	// ------------ The observations layer  (GeoJSON) ---------------------
108	currentDateInMillis = moment().add(settings.systemTimeOffsetMonths,"months").format("X") * 1000; // Reference for coloring the observations
109	var observationFeatures = new ol.Collection(); // Starting on empty   
110	initAgeStyles(); 
111	observationLayer = new ol.layer.Vector({
112	    source: new ol.source.Vector({
113	      features: observationFeatures
114	    }),
115	    style: getCorrectStyle // Defined further down
116	  });
117	
118	initObservationLegend();
119
120	// Creating the map
121	map = new ol.Map({
122			target: 'map',
123			layers: [backgroundLayer, forecastLayer, observationLayer],
124			overlays: [popOverlay],
125			renderer: 'canvas',
126			view: new ol.View({
127				center: ol.proj.fromLonLat(lonLat, 'EPSG:3857'),
128				zoom:zoomLevel,
129				maxZoom: getSelectedMapLayer() == FORECAST_LAYER ? maxMapZoomForForecasts : maxMapZoomForObservations
130			})
131	});
132
133	// Getting data and rendering the selected layer
134	selectMapLayer(getSelectedMapLayer());
135	/*if(getSelectedMapLayer() == FORECAST_LAYER)
136	{
137		updateForecastLayers();
138	}
139	else
140	{
141		initObservationLayer();
142	}*/
143	
144	// Attempting to adjust map to small screen
145	if(map.getSize()[0] < 500)
146	{
147		var zoom = map.getView().getZoom();
148		map.getView().setZoom(zoom -1 );
149	}
150
151	// ---------- MAP EVENTS ------------
152	
153	// On mouseover, display name of POI in tooltip
154    // TODO: Does this work? Doesn't seem so
155	map.on('mousemove', (evt) => {
156		  var pixel = map.getEventPixel(evt.originalEvent);
157		  displayFeatureName(pixel);
158		});
159	
160	// On click, display forecasts in popup
161	map.on('singleclick', (evt) => {
162		var pixel = map.getEventPixel(evt.originalEvent);
163		var coordinate = map.getEventCoordinate(evt.originalEvent);
164		  displayFeatureDetails(pixel, coordinate);
165		});
166	
167	map.on('moveend', (evt) => {
168		updateForecastSummaries();
169		// Store map settings
170		var centerCoordinate = ol.proj.transform(map.getView().getCenter(), map.getView().getProjection().getCode(),'EPSG:4326');
171		var settings = {
172			"longitude": centerCoordinate[0],
173			"latitude": centerCoordinate[1],
174			"zoomLevel": getCurrentMapZoomLevel()
175		};
176		storeLocalSettings(settings);
177		});
178	
179	//For testing: adding mouseposition geographic control
180	/*
181	var mousePositionControl = new ol.control.MousePosition({
182		  coordinateFormat: ol.coordinate.createStringXY(4),
183		  projection: 'EPSG:4326',
184		  undefinedHTML: '&nbsp;'
185		});
186
187	map.addControl(mousePositionControl);
188	*/
189
190}
191
192/**
193 * Error handling when collecting forecast information from server fails
194 * TODO: Not in use!
195 */
196var handleAjaxError = function(jqXHR,textStatus,errorThrown){
197    var message = jqXHR.responseJSON !== undefined ? jqXHR.responseJSON.message : jqXHR.responseText;
198    alert ("Error: " + message);
199};
200
201/**
202 * Initiated on mouseover (? doesn't seem like it does)
203 * Displays station name in a tooltip box
204 */
205var displayFeatureName = function(pixel) {
206	poiTitle.css({
207		left: pixel[0] + 'px',
208		top: (pixel[1] - 15) + 'px'
209	});
210	var feature = map.forEachFeatureAtPixel(pixel, function(feature,layer){
211		return feature; 
212	});
213    var title = poiTitle.data("bs.tooltip").options.title;
214    if (feature && feature != currentClickedFeature) {
215    	  poiTitle.tooltip('hide');
216    	  poiTitle.data("bs.tooltip").options.title = feature.get("name");
217    	  poiTitle.tooltip('show');
218    } 
219    else 
220    {
221	    if(! feature)
222	    {
223	    	poiTitle.tooltip('hide');
224		}
225    }
226    
227};
228
229/**
230 * Displays a feature(s) detail popup IF there is at least one feature where the user clicked
231 * @param pixel the point on the map that was clicked
232 * @param coordinate
233 */
234var displayFeatureDetails = function(pixel, coordinate) {
235	// Collect the features
236	var features = [];
237	map.forEachFeatureAtPixel(pixel, function(feature,layer){
238		features.push(feature)
239	});
240
241	if(features.length > 0) 
242	{
243		if(getSelectedMapLayer() == FORECAST_LAYER)
244		{
245			displayForecastDetails(features, coordinate);
246		}
247		else 
248		{
249			displayObservationDetails(features, coordinate);
250		}
251	} 
252	else 
253	{
254		currentClickedFeature = null;
255		poiDetails.popover('destroy');
256	}
257};
258
259/**
260 * Displays details about a forecast
261 */
262var displayForecastDetails = function(features, coordinate){
263	// Assuming that there is only one feature being clicked
264	  var feature = features[0];
265	  
266	  // Position the popup, and hiding it
267	  // Resetting information from (possible) former popups
268	  var geometry = feature.getGeometry();
269	  popOverlay.setPosition(coordinate);
270	  poiTitle.tooltip('hide');
271	  poiDetails.popover('destroy');
272	  currentClickedFeature = feature;
273	  var summaries = getForecastSummariesForPoi(feature.getId());
274	  var summariesHTML = "";
275	  if(summaries.length > 0)
276	  {
277  	  summariesHTML = "<table class=\"table-responsive\" id=\"forecastSummariesPopoverTable\">" +
278  		  getforecastSummariesTableHTML(summaries,true)
279  		  + "</table>";
280	  }
281	  else
282	  {
283		  summariesHTML = "<p>" + gettext("No forecasts found for selected crops") + "</p>";
284	  }
285	  var externalResourceHTML = "";
286	  var externalResources = getExternalResourcesForPoi(feature.getId());
287	  // These external resources have been integrated into the icons and summaries and forecast details page, so we skip them here to avoid confusion
288	  for(var i in externalResources){
289		  if(settings.externalResourcesToBeIgnoredInFrontend.includes(externalResources[i].pointOfInterestExternalResourcePK.externalResourceId))
290		  {
291			  continue;
292		  }
293		  var resourceLink = externalResources[i].externalResourceUrl;
294		  var resourceName = externalResources[i].externalResource.name;
295		  externalResourceHTML += "<li><a href=\"" + resourceLink + "\" target=\"new\">" + resourceName + "</a></li>";
296	  }
297	  
298	  if(externalResourceHTML != "")
299	  {
300		  externalResourceHTML = "<h3>" + gettext("External resources") + "</h2><ul class=\"popoverExternalResource\">"
301			  + externalResourceHTML
302			  + "</ul>";
303	  }
304	  //console.log(feature);
305	  poiDetails.popover({
306		animation: true,
307		trigger: 'manual',
308		html: true,
309		placement: "auto top",
310		title: feature.get("stationName") + (feature.get("infoUri") != undefined ? 
311				" <a href='" + feature.get("infoUri") + "' target='new'><span class='wi wi-day-cloudy'></span></a>"
312				:""),
313		content: summariesHTML + externalResourceHTML
314	  });
315	
316	  poiDetails.popover('show');
317};
318
319/**
320 * Displays details about one or more observations
321 */
322var displayObservationDetails = function(features, coordinate) {
323	  var titleHTML = gettext("Observation(s) found at location");
324	  var observationsHTML = "<ul style='list-style-type: none;margin: 0; padding: 0;'>";
325	  
326	  var observations = [];
327	  for(var i in features)
328	  {
329		  observations.push(getObservation(parseInt(features[i].get("observationId"))));
330	  }
331	  // Sort observations in descending order
332	  observations.sort(function(a,b){
333		  return a.timeOfObservation >= b.timeOfObservation ? -1 : 1;
334	  });
335	  for(var i in observations){
336		var observation = observations[i];
337		var obsDate = moment(observation.timeOfObservation).format("YYYY-MM-DD");
338		observationsHTML += "<li><i style='color: " + getObservationAgeColor(observation.timeOfObservation) + ";' class='fa fa-square' aria-hidden='true'></i> [" + obsDate + "] <a href='/observations/" + observation.observationId + "' target='new'>" + observation.organismName + " " + gettext ("in") + " " + observation.cropOrganismName.toLowerCase()  + "</a></li>";
339	  }
340	  observationsHTML += "</ul>";
341	  //console.info(features[0].get("observationId"));
342	  //console.info(coordinate);
343	  // Removing whatever was visible previously
344	  poiTitle.tooltip('hide');
345	  poiDetails.popover('destroy');
346	  // Placing and displaying the overlay
347	  popOverlay.setPosition(coordinate);
348	  poiDetails.popover({
349     		animation: true,
350     		trigger: 'manual',
351     		html: true,
352     		placement: "auto top",
353     		title: titleHTML,
354     		content: observationsHTML
355     	  });
356     	
357      poiDetails.popover('show');
358};
359
360/**
361 * Given the layer Id, displays the correct map layer
362 * @param layerId the Id (string) of the layer to be selected
363 */
364var selectMapLayer = function(layerId)
365{
366	// Remove feature popup
367	currentClickedFeature = null;
368	poiDetails.popover('destroy');
369	
370	// Adjust zoom if necessary
371	map.getView().setMaxZoom(layerId == FORECAST_LAYER ? maxMapZoomForForecasts : maxMapZoomForObservations);
372	
373	// Set layer and legend visibility
374	forecastLayer.setVisible(layerId == FORECAST_LAYER); // Constant at top of this file
375	document.getElementById("warningLegend").style.display = layerId == FORECAST_LAYER ? "block" : "none";
376	observationLayer.setVisible(layerId == OBSERVATION_LAYER); // Constant at top of this file
377	document.getElementById("observationLegend").style.display = layerId == OBSERVATION_LAYER ? "block" : "none";
378	
379	// Store selection for later sessions
380	storeSelectedMapLayer(layerId);
381	
382	// Init features for selected layer (if it hasn't already been done)
383	if(layerId == FORECAST_LAYER && (forecastLayer == null || forecastLayer == undefined || forecastLayer.getSource() == null))
384	{
385		updateForecastLayers();
386	}
387	
388}
389
390/**
391 * 
392 * @returns the current zoom level of the map
393 */
394function getCurrentMapZoomLevel()
395{
396	return map.getView().getZoom();
397}
398
399
400
401/**
402 * Loads new KML info from backend, renders forecast layer
403 */
404function updateForecastLayers()
405{
406	map.removeLayer(forecastLayer);
407	var cropCategoryIdStr = buildPathParamString("cropCategoryId", getSelectedCropCategoryIds());
408	// If empty, make sure we send a number anyway
409	if(cropCategoryIdStr === "")
410	{
411		cropCategoryIdStr = "&cropCategoryId=-1";
412	}
413	
414	forecastLayer = new ol.layer.Vector({
415		source: new ol.source.Vector({
416			//url: settings.vipslogicProtocol + "://" + settings.vipslogicServerName + "/rest/forecastresults/aggregate/" + settings.vipsOrganizationId + "?" + cropCategoryIdStr,
417			url: "/vipslogicproxy/rest/forecastresults/aggregate/" + 
418				(
419						settings.includeOrganizationIds != null && settings.includeOrganizationIds.length > 0 ? 
420						"orgspan?organizationId=" + settings.includeOrganizationIds.join("&organizationId=") + cropCategoryIdStr
421						: settings.vipsOrganizationId + "?" + cropCategoryIdStr
422				) 
423				+ 
424				(
425						settings.userUuid != null ? "&userUUID=" + settings.userUuid : ""
426				)
427				,
428			format: new ol.format.KML(),
429			projection: ol.proj.get('EPSG:3857')
430		})
431	});
432	forecastLayer.setVisible(getSelectedMapLayer() == FORECAST_LAYER); // Constant at top of this file
433	map.addLayer(forecastLayer);
434}
435
436
437
438
439/**
440 * Rendering the filteredObservations
441 */
442var renderObservationFeatures = function(){
443	var geoJSON = {"type":"FeatureCollection","features":[]};
444    for(var i=0;i<filteredObservations.length;i++)
445    {
446    	 var observation = filteredObservations[i];
447         var obsFeatures = null;
448         if(!observation.locationIsPrivate && observation.geoInfo !== null && observation.geoInfo.trim() !== "")
449         {
450             obsFeatures = JSON.parse(observation.geoInfo).features;
451         }
452         else
453         {
454             continue;
455         }
456         
457         for(var j=0; j<obsFeatures.length; j++)
458         {
459             geoJSON.features.push(obsFeatures[j]);
460         }
461
462    }
463
464    var format = new ol.format.GeoJSON();
465    drawnfeatures = format.readFeatures(geoJSON, {
466      dataProjection: 'EPSG:4326',
467      featureProjection: map.getView().getProjection().getCode()
468    });
469    observationLayer.getSource().clear();
470    observationLayer.getSource().addFeatures(drawnfeatures);
471}
472
473/**
474 * 
475 * @param coordinate [longitude,latitude] 
476 * @param projection 
477 * @returns boolean
478 */
479function isCoordinateOnVisibleMap(coordinate, projection){
480	var mapExtent = map.getView().calculateExtent(map.getSize());
481	var coordinate = ol.proj.transform(coordinate, 'EPSG:4326', map.getView().getProjection().getCode())
482	return ol.extent.containsCoordinate(mapExtent,coordinate);
483}
484
485/**
486 * Get the correct style for an observation feature, based on observation age
487 * @param feature the ol feature object
488 */
489var getCorrectStyle = function(feature){
490    var age = getObservationFeatureRelativeAge(feature);
491    if(age == null)
492    {
493        return;
494    }
495    if(age < 0)
496    {
497        return styleInvisible;
498    }
499    for(var i in ageStyles)
500    {
501        if(age < ageStyles[i][0])
502        {
503            return ageStyles[i][1];
504        }
505    }
506    return styleOld;
507};
508
509/**
510 * How many days old is the observation connected to this map feature?
511 */
512var getObservationFeatureRelativeAge = function(feature)
513{
514	var observation = getObservation(feature.get("observationId")); // TODO include observations timestamp in feature. Speeds things up
515	if(observation == null)
516	{
517		//console.info("No obs with id=" + feature.get("observationId"));
518		return null; // Means invisible
519	}
520	return getDaysSince(observation.timeOfObservation);
521};
522
523/**
524 * Using current system time, counting days since this day
525 */
526var getDaysSince = function(JSONDate)
527{
528	return Math.floor((currentDateInMillis - getUnixTimestampFromJSON(JSONDate)) / (1000 * 60 * 60 * 24)) + 1;
529};
530
531/**
532 * Pull the observation from global list
533 */
534var getObservation = function(observationId)
535{
536    for(var i=0; i<allObservations.length;i++)
537    {
538        if(allObservations[i].observationId == parseInt(observationId))
539        {
540            return allObservations[i];
541        }
542    }
543    //console.info("Could not find observation with ID=" + observationId)
544    return null;
545};
546
547/**
548 * Using config at top of file for dynamic style creation
549 */
550var initAgeStyles = function(){
551    ageStyles = [];
552    for(var i in ageColors)
553    {
554        ageStyles.push([
555            ageColors[i][0],
556            new ol.style.Style({
557                fill: new ol.style.Fill({
558                  color: ageColors[i][1].replace("1.0","0.2")
559                }),
560                stroke: new ol.style.Stroke({
561                  color: ageColors[i][1],
562                  width: 2
563                }),
564                image: new ol.style.Circle({
565                  radius: 6,
566                  fill: new ol.style.Fill({
567                    color: ageColors[i][1]
568                  }),
569                  stroke: new ol.style.Stroke({
570                      color: '#000000',
571                      width: 1
572                    })
573                })
574              })
575          ]); 
576    }
577};
578
579/**
580 * Dynamic init of the observation legend
581 * @returns
582 */
583function initObservationLegend()
584{
585    var lBox = document.getElementById("observationLegend");
586    var html = "<div id='observationLegendHeading'>" + gettext("Days since observation") + "</div><ul>";
587    for(var i in ageColors)
588    {
589            html += '<li><i style="color: ' + ageColors[i][1] + ';" class="fa fa-square" aria-hidden="true"></i> ' + (i > 0 ? ageColors[i-1][0] + 1 : '0') + '-' + ageColors[i][0] + ' ' + gettext("Days").toLowerCase() + ' </li>';
590    }
591        html += '<li><i style="color: ' + styleOld.getImage().getFill().getColor() + ';" class="fa fa-square" aria-hidden="true"></i> ' + gettext("Older") + '</li>';
592    html += "</ul>";
593    lBox.innerHTML = html;
594}
595
596/**
597 * Using configured ageColors 
598 * @param aDate
599 * @returns
600 */
601function getObservationAgeColor(aDate)
602{
603	var age = getDaysSince(aDate);
604    if(age == null)
605    {
606        return;
607    }
608    if(age < 0)
609    {
610        return styleInvisible.getFill().getColor();
611    }
612    for(var i in ageColors)
613    {
614        if(age < ageColors[i][0])
615        {
616            return ageColors[i][1];
617        }
618    }
619    return styleOld.getFill().getColor();
620}
621
622/**
623 * Default coloring
624 * @type Array
625 */
626var ageColors = [
627    [7, 'rgba(233, 13, 0, 1.0)'], // Red #e90d00
628    [30, 'rgba(253, 202, 00, 1.0)'], // Orange #fdca00
629    [60, 'rgba(0, 120, 253, 1.0)'], // Blue #0078fd
630];
631
632//Global configs
633var styleOld = 
634          new ol.style.Style({
635                fill: new ol.style.Fill({
636                  color: 'rgba(173, 173, 173, 0.5)'
637                }),
638                stroke: new ol.style.Stroke({
639                  color: '#000000',
640                  width: 1
641                }),
642                image: new ol.style.Circle({
643                  radius: 6,
644                  fill: new ol.style.Fill({
645                    color: '#adadad'
646                  }),
647                  stroke: new ol.style.Stroke({
648                      color: '#000000',
649                      width: 1
650                    })
651                })
652              });
653  var styleInvisible = new ol.style.Style({
654                fill: new ol.style.Fill({
655                  color: 'rgba(0, 0, 0, 0.0)'
656                }),
657                stroke: new ol.style.Stroke({
658                  color: 'rgba(0, 0, 0, 0.0)',
659                  width: 0
660                }),
661                image: new ol.style.Circle({
662                  radius: 0,
663                  fill: new ol.style.Fill({
664                    color: 'rgba(0, 0, 0, 0.0)'
665                  })
666                })
667              });
668

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.