There is no core LimeSurvey facility to record multiple locations from a map but I have been working on this for a new custom question theme, using
.
Add the following three blocks of code to the source of a multiple-short-text question. It will insert a Leaflet map and allow dropping of multiple location markers. When a marker is dropped, its coordinates and Geolocation data are recorded in the question text inputs.
In lines 20-30 of the JavaScript, you will need to replace "yourAPIKey" with an
Code:
<!-- Load Leaflet from CDN -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" crossorigin=""></script>
<!-- Load Esri Leaflet from CDN -->
<script src="https://unpkg.com/esri-leaflet@3.0.10/dist/esri-leaflet.js"></script>
<script src="https://unpkg.com/esri-leaflet-vector@4.0.1/dist/esri-leaflet-vector.js"></script>
<!-- Load Esri Leaflet Geocoder from CDN -->
<link rel="stylesheet" href="https://unpkg.com/esri-leaflet-geocoder@^3.1.3/dist/esri-leaflet-geocoder.css">
<script src="https://unpkg.com/esri-leaflet-geocoder@^3.1.3/dist/esri-leaflet-geocoder.js"></script>
Code:
<style type="text/css" data-author="Tony Partner">
.custom-map-outer-container .custom-map {
max-width: 100%;
}
.custom-map-outer-container .leaflet-popup-content {
line-height: 1.3;
font-size: 13px;
}
.custom-map-outer-container .leaflet-popup-content p {
margin: 0 0 10px 0;
}
.custom-map-outer-container .leaflet-popup-content p:last-child {
margin: 0 0 0 0;
}
@media only screen and (max-width: 400px) {
.custom-map-outer-container .leaflet-popup-content {
max-width: 200px;
}
}
</style>
Code:
<script type="text/javascript" data-author="Tony Partner">
$(document).on('ready pjax:scriptcomplete',function(){
// Your Your Esri Leaflet (ArcGIS) API key
var apiKey = 'yourAPIKey';
// Initial map parameters
var position = '51.5072 -0.1279'; // Lat/Long coordinates, separated by a space
var zoom = 12;
var mapWidth = '800px';
var mapHeight = '400px';
// Some text definitions
var removeText = 'Remove marker';
var warningText = 'Only 6 markers are allowed. You must remove a marker before adding another.';
//// NO EDITING REQUIRED BELOW HERE ////
var qID = {QID};
var thisQuestion = $('#question'+qID);
thisQuestion.addClass('custom-map-question');
// Disable the text inputs
$(':text.form-control', thisQuestion).prop('readonly', true);
// Insert a map wrapper
$('.answer-container', thisQuestion).prepend('<div class="custom-map-outer-container">\
<div class="custom-map" id="custom-map-'+qID+'" style="width:'+mapWidth+'; height:'+mapHeight+';"> </div>\
</div>');
// Initiate the map
const basemapEnum = "ArcGIS:Navigation";
const map = L.map('custom-map-'+qID+'', {
minZoom: 2
});
var coordsArr = $.trim(position).split(' ');
var latAtt = Number($.trim(coordsArr[0]));
var lngAtt = Number($.trim(coordsArr[1]));
mapLat = latAtt;
mapLng = lngAtt;
map.setView([mapLat, mapLng], zoom);
L.esri.Vector.vectorBasemapLayer(basemapEnum, {
apiKey: apiKey
}).addTo(map);
// Add a LayerGroup to the map to contain the markers and geocoding results.
const markerLayer = L.layerGroup().addTo(map);
// An array to hold the markers (so we can loop through them later)
var markers = [];
// A function to insert markers and pop-ups
function createMarker(coords, latLngString, addressString, newMarker) {
var id;
if (markers.length < 1) {
//id = Number(qID)+0;
id = qID+'_'+0;
}
else {
const lastIDNum = Number(markers[markers.length - 1]._markerID.split('_')[1]);
const newIDNum = lastIDNum + 1;
//id = (markers[markers.length - 1]._markerID + 1);
id = qID+'_'+newIDNum;
}
// The marker pop-up content
var popupContent = '<p>'+latLngString+', '+addressString+'</p>\
<p><button type="button" class="remove-marker btn btn-sm btn-primary" data-marker-id="'+id+'">Remove</button></p>';
// Define and insert a marker and associated pop-up
myMarker = L.marker(coords, {
draggable: false
});
myMarker._markerID = id;
myMarker._address_string = addressString;
var myPopup = myMarker.bindPopup(popupContent, {
//closeButton: false
});
map.addLayer(myMarker);
// Add the marker to the array
markers.push(myMarker);
// This is a new user-created marker
if(newMarker == true) {
myMarker.openPopup();
loadMapData();
}
}
// A function to remove markers
function clearMarker(id) {
var new_markers = [];
// Loop through the markers
markers.forEach(function(marker) {
// Remove it if the ID matches
if (marker._markerID == id) {
map.removeLayer(marker)
}
// Otherwise add it to the new array
else {
new_markers.push(marker)
}
})
// Update the original markers array
markers = new_markers;
// Load the LS data
loadMapData();
}
// Listener on "Remove" buttons
$('body').on('click', 'button.remove-marker', function (e) {
clearMarker($(this).attr('data-marker-id'));
});
// Listener on map for reverse geocoding
map.on('click', function (e) {
if(markers.length < $(':text.form-control', thisQuestion).length) {
L.esri.Geocoding
.reverseGeocode({
apikey: apiKey
})
.latlng(e.latlng)
.run(function (error, result) {
if (error) {
return;
}
const latLngString = Math.round(result.latlng.lat * 100000) / 100000+', '+Math.round(result.latlng.lng * 100000) / 100000;
createMarker(result.latlng, latLngString, result.address.LongLabel, true);
});
}
else {
// To-do - this could be a modal
alert(warningText);
}
});
// A function to load the results into LimeSurvey
function loadMapData() {
// Clear everything
$(':text.form-control', thisQuestion).val('').trigger('keyup');
// Loop through the markers
$.each(markers, function(i, marker) {
const latLngString = this._latlng.lat+', '+this._latlng.lng;
const addressString = this._address_string;
// Insert the LS data
$(':text.form-control:eq('+i+')', thisQuestion).val(latLngString+' || '+addressString).trigger('keyup');
})
}
// Returning to the page
$(':text.form-control', thisQuestion).each(function(i) {
if($.trim($(this).val()) != '') {
var dataCoords = $.trim($(':text.form-control:eq('+i+')', thisQuestion).val().split('||')[0]).split(',');
var dataAddress = $.trim($(':text.form-control:eq('+i+')', thisQuestion).val().split('||')[1]);
var dataLatLngString = Math.round($.trim(dataCoords[0]) * 100000) / 100000+', '+Math.round($.trim(dataCoords[1]) * 100000) / 100000;
var dataMarkerCoords = { 'lat': $.trim(dataCoords[0]), 'lng': $.trim(dataCoords[1]) };
createMarker(dataMarkerCoords, dataLatLngString, dataAddress, false);
}
});
if(markers.length > 0) {
// Zoom to fit all markers
var markerBoundsGroup = new L.featureGroup(markers);
map.fitBounds(markerBoundsGroup.getBounds());
}
});
</script>