/**
* @namespace
*/
var ALA = ALA || {};
/**
* @class
* @memberOf ALA
*/
ALA.MapConstants = {
/**
* Types of drawing objects
*/
DRAW_TYPE: {
POINT_TYPE: "Point",
CIRCLE_TYPE: "Circle",
POLYGON_TYPE: "Polygon",
LINE_TYPE: "LineString"
},
/**
* Types of layers
*/
LAYER_TYPE: {
MARKER: "marker"
}
};
/**
* Object for interacting with a generic map.
*
* <p/>
* <b>Options</b>
* <ul>
* <li><code>autoZIndex</code> If true, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off. Default: true</li>
* <li><code>preserveZIndex</code> If true, a added layer will not be brought to front. It will be at the zIndex assigned. Default: false</li>
* <li><code>baseLayer</code> The base layer shown by default.
* Can be one of:
* - a Leaflet.Layer object, or
* - a string containing the name of one of the built-in base layers.
* Available built-in base layers: 'Minimal', 'WorldImagery', 'Detailed', 'Topographic'.
* Default: 'Minimal'.
* </li>
* <li><code>otherLayers</code> Specifies the base layers that will be available (including the default baseLayer).
* This is an object, where the keys are the display text, and values are either:
* - a Leaflet.Layer object, or
* - the name of one of the built-in base layers.
* Default: {'Minimal': 'Minimal', 'World Imagery': 'WorldImagery'}.
* </li>
* <li><code>overlays</code> Specify the overlays that should be added to map. The expected format is
* {
* 'Australian Corals': L.tileLayer.wms
* }
* </li>
* <li>
* <code>overlayLayersSelectedByDefault</code> List of overlays to be shown on map by default. This works in conjunction
* with 'overlays' option. Overlays not on this list will have a checkbox to show on map. The expected format is
* ['Australian Corals']
* </li>
* <li><code>center</code> Centre position of the map. Default: -28, 134</li>
* <li><code>zoom</code> the initial zoom level. Default: 4</li>
* <li><code>maxZoom</code> the maximum allowed zoom level. Default: 21</li>
* <li><code>maxAutoZoom</code> the maximum zoom level to automatically zoom to (when zoomToObject = true). Default: 15</li>
* <li><code>defaultLayersControl</code> true to use the default layers control, false to use your own. Default: true</li>
* <li><code>addLayersControlHeading</code> true to add heading to base layer and overlay sections of the selector, false to use your own. Default: false</li>
* <li><code>scrollWheelZoom</code> whether to enable zooming in/out by scrolling the mouse. Default: false</li>
* <li><code>fullscreenControl</code> whether to include a full-screen option. Default: true</li>
* <li><code>fullscreenControlOptions:</code>
* <ul>
* <li><code>position</code> position of the button on the map. Default: topleft</li>
* </ul>
* <li><code>drawControl</code> whether to include drawing controls or not. Default: true</li>
* <li><code>drawOptions</code> if drawing control is to be included, then specify options to pass to drawing control here.</li>
* <li><code>editOptions</code> if edit option in drawing control is enabled, then specify options to pass to edit control here.</li>
* <li><code>singleDraw</code> whether to allow more than 1 shape or region to be drawn at a time. This does NOT apply to markers - only layers and other shapes. See also singleMarker and markerOrShapeNotBoth. Default: true</li>
* <li><code>singleMarker</code> whether to allow more than 1 marker to be drawn at a time.. Default: true</li>
* <li><code>markerZoomToMax</code> whether to allow zoom to maximum permitted level of current base layer</li>
* <li><code>markerOrShapeNotBoth</code> whether to allow users to draw both markers and regions/shapes at the same time. Default: true</li>
* <li><code>showFitBoundsToggle</code> whether to include a button to toggle between the initial map zoom and the bounds of the data. Default: false</li>
* <li><code>useMyLocation</code> whether to include a "Use My Location" button to place a marker on the map at the user's location. Default: true</li>
* <li><code>addGeometryFromLocalFile</code> add a shape from a file. Accepted formats are shapefile, geojson, kml, gpx. Default: false</li>
* <li><code>simplifyImportedShapes</code> True to simplify shapes imported from files. Only relevant if addGeometryFromLocalFile = true. Default: false</li>
* <li><code>simplifyOptions</code> Options to pass to the turf's simplify method. Only relevant if addGeometryFromLocalFile = true and simplifyImportedShapes = true. Default: {tolerance: 0.001, highQuality: true, mutate: true}</li>
* <li><code>allowSearchLocationByAddress</code> whether to allow the user to search by address to place a marker on the map. Default: true</li>
* <li><code>allowSearchRegionByAddress</code> whether to allow the user to search by address to draw a polygon on the map. Default: true</li>
* <li><code>geocodeRegionOptions</code> additional configuration options when using the allowSearchRegionByAddress control. Only relevant when allowSearchRegionByAddress = true:</li>
* <ul>
* <li><code>pointToCircle</code> true to render any point locations returned by the geocode service as circles. Default: true</li>
* <li><code>pointRadiusMeters</code> The radius of the circle to use when pointToCircle = true. Units are in meters. Default: 500</li>
* </ul>
* <li><code>zoomToObject</code> whether to automatically fit the map to the bounds of a new object when added. Default: true</li>
* <li><code>draggableMarkers</code> whether to allow point markers to be draggable by default. Default: true</li>
* <li><code>wmsFeatureUrl</code> the URL to call to fetch features of a WMS layer (e.g. http://spatial-dev.ala.org.au/geoserver/wms/reflect?). The PID will be appended to this URL. Default: </li>
* <li><code>wmsLayerUrl</code> the URL to call to retrieve a WMS layer. The PID will be appended to this URL. Default: </li>
* <li><code>sleep</code> True to disable mouse wheel zooming once the mouse has been out of the map for a certain time. Clicking in the map re-enables it. Overrides scrollWheelZoom. Default: true</li>
* <li><code>sleepTime</code> Time (milliseconds) until the map 'sleeps' up after the mouse moves away from the map. Only relevant if sleep = true. Default: 750</li>
* <li><code>hoverToWake</code> True to wake the map up after the mouse has been held over the map for a certain time (wakeTime). Only relevant if sleep = true. Default: true</li>
* <li><code>wakeTime</code> Time (milliseconds) until the map wakes up after mouse over. Only relevant if sleep = true and hoverToWake = true. Default: 750</li>
* <li><code>sleepNote</code> True to display text over the map when it is sleeping. Only relevant if sleep = true. Default: false</li>
* <li><code>sleepOpacity</code> Opacity of the sleep text. Only relevant if sleep = true and sleepNote = true. Default: 0.7</li>
* <li><code>wakeMessage</code> Text to display over the map when it is sleeping. Only relevant if sleep = true and sleepNote = true. Default: 'Click or hover to wake'.</li>
* <li><code>trackWindowHeight<code> Map will adjust its height according to the height of browser window when set to true. Default: false. </li>
* <li><code>minMapHeight<code> The height of map will not go below this value. It is only active when trackWindowHeight is true. Default: 250. </li>
* </ul>
*
* @class
* @memberOf ALA
* @param {String} id Unique id of the map container div. Mandatory.
* @param {Object} options Configuration options for the map. Optional - sensible defaults will be used if not provided. See the list above.
*/
ALA.Map = function (id, options) {
var self = this;
self.DEFAULT_CENTRE = {
lat: -28,
lng: 134
};
var DEFAULT_ZOOM = 4;
var SINGLE_POINT_ZOOM = 16;
var MAX_AUTO_ZOOM = 15;
var DEFAULT_MAX_ZOOM = 21;
var DEFAULT_OPACITY = 0.5;
var DEFAULT_LINE_WEIGHT = 4;
var DEFAULT_FILL_COLOUR = "#000";
var DEFAULT_MAP_HEIGHT_BUFFER = 40;
// There is a bug with Leaflet prior to v1.0 which causes drawing issues with animations enabled.
// E.g. Calling fitBounds multiple times sometimes causes the drawing of the map to fail, usually leaving some or
// all of the base tile layer's tiles out. Until that is fixed, we will disable animations.
// https://github.com/Leaflet/Leaflet/issues/3249
var ANIMATE = false;
var DEFAULT_SHAPE_OPTIONS = {
weight: DEFAULT_LINE_WEIGHT,
fillOpacity: DEFAULT_OPACITY,
color: DEFAULT_FILL_COLOUR
};
var AVAILABLE_BASE_LAYERS = {
Minimal: {
// See https://cartodb.com/location-data-services/basemaps/
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
title: 'Minimal',
defaultSelected: true,
defaultInList: true,
options: {
subdomains: "abcd",
attribution: 'Map data © <a target="_blank" rel="noopener noreferrer" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, imagery © <a target="_blank" rel="noopener noreferrer" href="https://cartodb.com/attributions">CartoDB</a>',
maxZoom: DEFAULT_MAX_ZOOM,
maxNativeZoom: 21
}
},
WorldImagery: {
// see https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
title: 'World Imagery',
defaultSelected: false,
defaultInList: true,
options: {
attribution: '<a target="_blank" rel="noopener noreferrer" href="https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9">Tiles from Esri</a> — Sources: Esri, DigitalGlobe, Earthstar Geographics, CNES/Airbus DS, GeoEye, USDA FSA, USGS, Aerogrid, IGN, IGP, and the GIS User Community',
maxZoom: DEFAULT_MAX_ZOOM,
maxNativeZoom: 17
}
},
Detailed: {
// see https://wiki.openstreetmap.org/wiki/Standard_tile_layer
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
title: 'Detailed',
defaultSelected: false,
defaultInList: false,
options: {
subdomains: "abc",
attribution: '© <a target="_blank" rel="noopener noreferrer" href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
maxZoom: DEFAULT_MAX_ZOOM,
maxNativeZoom: 18
}
},
Topographic: {
// see https://www.arcgis.com/home/item.html?id=30e5fe3149c34df1ba922e6f5bbf808f
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
title: 'Topographic',
defaultSelected: false,
defaultInList: false,
options: {
attribution: '<a target="_blank" rel="noopener noreferrer" href="https://www.arcgis.com/home/item.html?id=30e5fe3149c34df1ba922e6f5bbf808f">Tiles from Esri</a> — Sources: Esri, HERE, Garmin, Intermap, INCREMENT P, GEBCO, USGS, FAO, NPS, NRCAN, GeoBase, IGN, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), © OpenStreetMap contributors, GIS User Community',
maxZoom: DEFAULT_MAX_ZOOM,
maxNativeZoom: 17
}
}
};
var DEFAULT_LAYER_CONTROL_OPTIONS = {
position: "topright"
};
var DEFAULT_DRAW_OPTIONS = {
polyline: false,
polygon: {
allowIntersection: false,
shapeOptions: DEFAULT_SHAPE_OPTIONS
},
rectangle: {
shapeOptions: DEFAULT_SHAPE_OPTIONS
},
circle: {
shapeOptions: DEFAULT_SHAPE_OPTIONS
},
circlemarker: false,
edit: true
};
var DEFAULT_EDIT_DRAW_OPTIONS = {
featureGroup: undefined,
poly: {
allowIntersection: false
}
};
var HIDDEN_LAYER = {
weight: 0,
fillOpacity: 0
};
var VISIBLE_LAYER = {
weight: DEFAULT_LINE_WEIGHT,
fillOpacity: DEFAULT_OPACITY,
color: DEFAULT_FILL_COLOUR
};
var DEFAULT_WMS_PROPERTIES = {
tiled: true,
format: 'image/png',
opacity: 0.5,
transparent: true,
layers: "ALA:Objects",
version: "1.1.0",
srs: "EPSG:900913"
};
var DEFAULT_SIMPLIFICATION_OPTIONS = {
tolerance: 0.001,
highQuality: true,
mutate: true
}
/**
* Default Map options
*
* @memberOf ALA.Map
* @var
*/
var DEFAULT_OPTIONS = {
center: [self.DEFAULT_CENTRE.lat, self.DEFAULT_CENTRE.lng],
zoom: DEFAULT_ZOOM,
maxZoom: DEFAULT_MAX_ZOOM,
maxAutoZoom: MAX_AUTO_ZOOM,
defaultLayersControl: true,
addLayersControlHeading: false,
scrollWheelZoom: false,
fullscreenControl: true,
fullscreenControlOptions: {
position: 'topleft'
},
drawControl: true,
singleDraw: true,
singleMarker: true,
markerOrShapeNotBoth: true,
showFitBoundsToggle: false,
useMyLocation: true,
allowSearchLocationByAddress: true,
allowSearchRegionByAddress: true,
geocodeRegionOptions: {
pointToCircle: true,
pointRadiusMeters: 500
},
zoomToObject: true,
worldCopyJump: true,
draggableMarkers: true,
showReset: true,
wmsLayerUrl: null,
wmsFeatureUrl: null,
myLocationControlTitle: "Use my location",
drawOptions: DEFAULT_DRAW_OPTIONS,
editOptions: DEFAULT_EDIT_DRAW_OPTIONS,
sleep: true,
sleepTime: 750,
wakeTime: 750,
sleepNote: false,
hoverToWake: true,
trackWindowHeight: false,
minMapHeight: 250,
autoZIndex: true,
preserveZIndex: false,
addGeometryFromLocalFile: false,
simplifyImportedShapes: false,
simplifyOptions: DEFAULT_SIMPLIFICATION_OPTIONS
};
/**
* Supported Layer options.
*
* The following configuration options are available:
* <ul>
* <li><code>markerWithMouseOver</code> - show a marker at the center of the layer, and hide the layer outline until the mouse is moved over the marker. Default: false</li>
* <li><code>markerLocation</code> - used with 'markerWithMouseOver' to specify the position of the marker - if not provided, the center of the layer's bounds will be used. Only works with a single feature.</li>
* <li><code>popup</code> - text or HTML to display when the layer (or the placeholder marker if markerWithMouseOver = true) is clicked.</li>
* </ul>
*
* @memberOf ALA.Map
* @var
*/
var LAYER_OPTIONS = {
markerWithMouseOver: false,
markerLocation: undefined,
popup: undefined
};
/**
* Default options for clustered point markers.
*
* See http://leafletjs.com/reference.html#path-options for the options: this object overrides the standard Leaflet
* options with our default values, where they are different.
*
* @memberOf ALA.Map
* @var
*/
var DEFAULT_POINT_MARKER_OPTIONS = {};
if (!id) {
console.error("[ALA-Map] You must define a unique id for your map.")
}
if (_.isUndefined(options)) {
options = {};
}
populateDefaultOptions(options);
var mapImpl = null;
var fitToBoundsOfLayer = null;
var drawControl = null;
var drawnItems = DEFAULT_EDIT_DRAW_OPTIONS.featureGroup = new L.FeatureGroup();
var markers = [];
var editModeSnapshotGeoJSON = null;
var snapshotMode = null;
var suppressEditModeNotify = false;
var subscribers = [];
var fitToBoundsToggle = options.zoomToObject;
var layerControl = null;
var layerCreatedByGeoJSON = null;
/* Keep track of all available overlay layers. */
var overlayLayersAvailable = [];
/* Keep track of the selected overlay layers. */
var overlayLayersSelected = [];
/**
* Destroy the map and clear all related event listeners
*
* @memberOf ALA.Map
* @function destroy
*/
self.destroy = function () {
mapImpl.remove();
fitToBoundsOfLayer = null;
drawControl = null;
drawnItems = null;
editModeSnapshotGeoJSON = null;
snapshotMode = null;
suppressEditModeNotify = false;
markers = [];
subscribers = [];
};
/**
* Subscribe to all update events on the map.
*
* To listen for specific events, use {@link ALA.Map#registerListener} instead.
*
* @memberOf ALA.Map
* @function subscribe
* @param callback {function} the callback function to be invoked when the map is updated
* @returns {Object} An object with a 'cancel' method that you can use remove this subscription.
*/
self.subscribe = function (callback) {
subscribers.push(callback);
return {
cancel: function() {
self.unsubscribe(callback);
}
}
};
/**
* Stop listening to map update events.
*
* @memberOf ALA.Map
* @function unsubscribe
* @param callback {function} the callback function used to subscribe to events
*/
self.unsubscribe = function (callback) {
var index = subscribers.indexOf(callback);
if (index != -1) {
subscribers.splice(index,1);
}
};
/**
* Notify all subscribers of an update to the map
*
* @memberOf ALA.Map
* @function notifyAll
*/
self.notifyAll = function () {
subscribers.forEach(function (sub) {
sub();
});
};
/**
* Adds a listener for the specified event
*
* @memberOf ALA.Map
* @function registerListener
* @param eventName {String} the Leaflet event to listen to. Mandatory.
* @param callback {function} The function to invoke when the event occurs. Mandatory.
*/
self.registerListener = function (eventName, callback) {
if (!eventName || !callback) {
console.error("[ALA-Map] You must specify the eventName and callback when registering a listener");
}
mapImpl.on(eventName, callback);
};
/**
* Removes a listener for the specified event. Leaflet identifies listeners by references, so the original
* callback needs to be provided in order to find the listener to remove.
*
* @memberOf ALA.Map
* @function removeListener
* @param eventName {String} the Leaflet event being listened to. Mandatory.
* @param callback {function} The function to invoke when the event occurs. Mandatory.
*/
self.removeListener = function (eventName, callback) {
mapImpl.off(eventName, callback)
};
/**
* Retrieves the standard GeoJSON representation of all features that have been drawn on the map.
*
* Note that Circle objects are not supported by GeoJSON. Therefore, they are represented as Points in the resulting
* geometry. A workaround has been added to include 'point_type' = 'circle' and 'radius' properties in the feature's
* property object (this is still valid GeoJSON).
*
* @memberOf ALA.Map
* @function getGeoJSON
* @return {GeoJSON} Standard GeoJSON representation of the map's drawn features
*/
self.getGeoJSON = function () {
return drawnItems.toGeoJSON();
};
/**
* Populate the map with the provided GeoJSON data.
*
* If a Point feature's properties object contains "point_type: 'Circle' and "radius: {m}", then the point will be
* rendered as a circle.
*
* If the properties object of a feature includes a 'pid', then a new WMS layer will be added to the map instead of
* a polygon layer.
*
* See {@link LAYER_OPTIONS} for details of supported options.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function setGeoJSON
* @param geoJSON {GeoJSON} Standard GeoJSON metadata for map features. This can be a JSON string, or a GeoJSON object.
* @param layerOptions {Object} Configuration options for the layer. See {@link LAYER_OPTIONS} for details of supported options. Optional.
*/
self.setGeoJSON = function (geoJSON, layerOptions) {
if (typeof geoJSON === 'string') {
geoJSON = JSON.parse(geoJSON);
}
layerCreatedByGeoJSON = null;
L.geoJson(geoJSON, {
pointToLayer: pointToLayerCircleSupport,
onEachFeature: function (feature, layer) {
onEachFeatureWithWMSLayerSupport(feature, layer, layerOptions);
},
style: DEFAULT_SHAPE_OPTIONS
});
if (options.zoomToObject) {
self.fitBounds();
}
self.notifyAll();
return layerCreatedByGeoJSON;
};
/**
* Retrieve the unique id for this map
*
* @memberOf ALA.Map
* @function getMapId
* @returns {String} unique id of the map
*/
self.getMapId = function () {
return id;
};
/**
* Retrieve the underlying map implementation
*
* @memberOf ALA.Map
* @function getMapImpl
* @returns {Object} Leaflet Map object
*/
self.getMapImpl = function () {
return mapImpl;
};
/**
* Removes all custom layers, reverts to the initial base layer, resets the zoom level to the default and re-centres
* the map.
*
* If {@link fitToBoundsOf} has been called previously, then resetMap will fit the map to the previously supplied
* bounds. To reset to the default map centre & zoom, call {@link clearBoundLimits} before resetMap.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function resetMap
*/
self.resetMap = function () {
drawnItems.clearLayers();
markers = [];
if (!_.isUndefined(fitToBoundsOfLayer) && fitToBoundsOfLayer != null) {
self.fitToBoundsOf(fitToBoundsOfLayer);
} else {
mapImpl.setZoom(DEFAULT_ZOOM, {animate: ANIMATE});
mapImpl.panTo(self.DEFAULT_CENTRE, {animate: ANIMATE});
}
self.notifyAll();
};
/**
* Remove all drawn layers from the map. This does not remove markers: see {@link self#clearMarkers}.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function clearLayers
*/
self.clearLayers = function () {
drawnItems.eachLayer(function (layer) {
if (markers.indexOf(layer) == -1) {
drawnItems.removeLayer(layer);
}
});
self.fitBounds();
self.notifyAll();
};
/**
* Removes the specified layer from the map.
*
* @memberOf ALA.Map
* @function removeLayer
* @param layer {Object} the Leaflet Layer object to remove
*/
self.removeLayer = function (layer) {
drawnItems.removeLayer(layer);
self.fitBounds();
self.notifyAll();
};
/**
* Remove all markers from the map. This does not remove other layers or shapes: see {@link self#clearLayers}.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function clearMarkers
*/
self.clearMarkers = function () {
clearMarkers();
self.notifyAll();
};
/**
* Get the lat/lng coordinates of the centre of the map view
*
* @memberOf ALA.Map
* @function getCentre
* @returns {Object} lat/lng object
*/
self.getCentre = function () {
var centre = mapImpl.getCenter();
return {lat: centre.lat, lng: centre.lng};
};
/**
* Adds a marker at the specified point.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function addMarker
* @param lat {Number} Latitude for the marker. Mandatory.
* @param lng {Number} Longitude for the marker. Mandatory.
* @param popup {String} Text or HTML to display in a popup when the marker is clicked. Optional.
* @param markerOptions {Object} Object containing configuration items to override the defaults. Optional,
* @returns {L.Marker} The L.marker object
*/
self.addMarker = function (lat, lng, popup, markerOptions) {
if (_.isUndefined(markerOptions)) {
markerOptions = {};
}
markerOptions.draggable = options.draggableMarkers;
var marker = L.marker([lat, lng], markerOptions);
addMarker(marker, true);
if (popup) {
marker.bindPopup(popup);
}
return marker;
};
/**
* Adds a layer of clustered L.circleMarker to the map. Clustered points are treated the same as other layers/shapes:
* i.e. they are NOT considered to be 'markers' like those added with the {@link #addMarker} function.
*
* See http://leafletjs.com/reference.html#path-options for the possible options.
*
* Each point object in the points array must have the following structure:
* <ul>
* <li><code>lat</code> - the latitude for the point. Mandatory.</li>
* <li><code>lng</code> - the longitude for the point. Mandatory.</li>
* <li><code>popup</code> - Text or HTML to be used as the popup when the marker is clicked. Optional.</li>
* <li><code>options</code> - options object to override specified options for the individual point. Optional.</li>
* </ul>
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function addClusteredPoints
* @param points {Array} Mandatory array of objects with mandatory properties 'lat' and 'lng', and optionally an 'options' object.
* @param pointOptions {Object} Optional object containing configuration options to be applied to ALL points.
*/
self.addClusteredPoints = function (points, pointOptions) {
self.startLoading();
if (options.singleDraw) {
drawnItems.clearLayers();
}
if (options.markerOrShapeNotBoth) {
clearMarkers();
}
var groupOptions = {
chunkedLoading: true
};
var cluster = L.markerClusterGroup(groupOptions);
_.defaults(pointOptions, DEFAULT_POINT_MARKER_OPTIONS);
var layers = [];
points.forEach(function (point) {
var newPointOptions = _.clone(pointOptions);
if (point.options) {
_.defaults(newPointOptions, point.options);
}
var layer = L.circleMarker(new L.LatLng(point.lat, point.lng), newPointOptions);
if (point.popup) {
layer.bindPopup(point.popup);
}
layers.push(layer);
});
cluster.addLayers(layers);
addLayer(cluster, true);
};
/**
* Adds a layer of points or icons to map. Icons gives the flexibility of adding shape to map.
*
* Each point object in the points array must have the following structure:
* <ul>
* <li><code>lat</code> - the latitude for the point. Mandatory.</li>
* <li><code>lng</code> - the longitude for the point. Mandatory.</li>
* <li><code>type</code> - supported values 'point' or 'icon'. point draws a circle and icon renders the provided image. Default 'point'. </li>
* <li><code>popup</code> - Text or HTML to be used as the popup when the marker is clicked. Optional.</li>
* <li><code>options</code> - options object to override specified options for the individual point. Optional.</li>
* </ul>
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function addPointsOrIcons
* @param points {Array} Mandatory array of objects with mandatory properties 'lat' and 'lng', and optionally an 'options' object.
* @param pointOptions {Object} Optional object containing configuration options to be applied to ALL points.
* @param iconUrl {String} Optional image URL
* @param iconOptions {Object} Optional object describing the icon metadata like size, anchor point, popup location.
*/
self.addPointsOrIcons = function (points, pointOptions, iconUrl, iconOptions) {
self.startLoading();
if (options.singleDraw) {
drawnItems.clearLayers();
}
if (options.markerOrShapeNotBoth) {
clearMarkers();
}
_.defaults(pointOptions, DEFAULT_POINT_MARKER_OPTIONS);
var icon = ALA.MapUtils.createIcon(iconUrl, iconOptions);
points.forEach(function (point) {
var newPointOptions = _.clone(pointOptions);
if (point.options) {
_.defaults(newPointOptions, point.options);
}
var layer;
switch (point.type){
case 'icon':
newPointOptions.icon = icon;
layer = L.marker([point.lat, point.lng], newPointOptions);
break;
case 'circle':
default:
layer = L.circleMarker(new L.LatLng(point.lat, point.lng), newPointOptions);
break;
}
if (point.popup) {
layer.bindPopup(point.popup);
}
drawnItems.addLayer(layer)
});
};
/**
* Adds a marker at the user's current location.
* Will notify all subscribers.
*
* Fire a 'SearchEventFired' for site creation
* @memberOf ALA.Map
* @function markMyLocation
*/
self.markMyLocation = function () {
self.startLoading();
mapImpl.locate({setView: true, enableHighAccuracy: true});
mapImpl.on("locationfound", function (locationEvent) {
self.addMarker(locationEvent.latlng.lat, locationEvent.latlng.lng, null);
mapImpl.off("locationfound", arguments.callee);
mapImpl.fire("searchEventFired");
self.finishLoading();
});
mapImpl.on("locationerror", function (event) {
mapImpl.off("locationerror", arguments.callee);
console.log("[ALA-Map] location lookup failed - " + event.message);
console.log("[ALA-Map] location lookup error code - " + event.code);
alert("Location lookup failed for the following reason - " + event.message);
self.finishLoading();
});
};
/**
* Add a new layer to the map.
*
* Will notify all subscribers.
*
* @memberOf ALA.Map
* @function addLayer
* @param {Object} layer The Leaflet ILayer to add
* @param layerOptions {Object} Configuration options for the layer. See {@link LAYER_OPTIONS} for details of supported options. Optional.
*/
self.addLayer = function (layer, layerOptions) {
if (options.singleDraw) {
drawnItems.clearLayers();
}
if (options.markerOrShapeNotBoth) {
clearMarkers();
}
addLayer(layer, true);
applyLayerOptions(layer, layerOptions);
};
/**
* Utility method to add a WMS layer to the map.
*
* Triggers events pm:drawstart and pm:create, and will notify all subscribers
*
* @memberOf ALA.Map
* @function addWmsLayer
* @param pid {String} the PID of the region to be displayed in the WMS layer - set as undefined if you do not need to use an existing region.
* @param layerOptions {Object} Configuration options for the layer. If pid is undefined, then the layerOptions object must contain the required WMS configuration parameters. See {@link LAYER_OPTIONS} for details of additional supported options. Optional.
* @returns {L.TileLayer.SmartWMS} the L.TileLayer.WMS object
*/
self.addWmsLayer = function (pid, layerOptions) {
self.startLoading();
var layer = createWmsLayer(pid, layerOptions);
if (options.singleDraw) {
drawnItems.clearLayers();
}
if (options.markerOrShapeNotBoth) {
clearMarkers();
}
addLayer(layer, false);
layer.bringToFront(); // make sure the new layer sits on top of the other tile layers (like the base layer)
applyLayerOptions(layer, layerOptions);
return layer;
};
/**
* Add a layer to the map as an overlay layer.
*
* @memberOf ALA.Map
* @function addOverlayLayer
* @param {L.Class} layer The tile layer or other type of layer to add as an overlay layer.
* @param {string} name The name text to show for this layer.
* @param {boolean} isSelected Whether the added overlay layer should start selected, and therefore visible, or not.
*/
self.addOverlayLayer = function (layer, name, isSelected) {
overlayLayerAdd(layer, name, isSelected);
};
/**
* Remove an overlay layer from the layers control.
* @param {L.Class} layer The layer to remove.
*/
self.removeOverlayLayer = function(layer) {
overlayLayerRemove(layer);
};
/**
* Get the overlay layers.
*
* @memberOf ALA.Map
* @function getOverlayLayers
* @param {string} filter Which layers to include: 'all': all layers; 'selected': only selected layers. Default 'all'.
* @return {array} selected or all overlay layers, depending on the filter parameter.
*/
self.getOverlayLayers = function (filter) {
filter = filter || 'all';
switch (filter) {
case 'selected':
return overlayLayersSelected;
case 'all':
default:
return overlayLayersAvailable;
}
};
/**
* Get the most recently selected overlay layer.
*
* @memberOf ALA.Map
* @function getTopSelectedOverlayLayer
* @return {null|L.Class} The most recently selected layer or null if no layers are selected.
*/
self.getTopSelectedOverlayLayer = function () {
if (overlayLayersSelected.length > 0) {
return overlayLayersSelected[overlayLayersSelected.length - 1];
}
return null;
};
/**
* Retrieve the current bounds for the map, if possible.
*
* @memberOf ALA.Map
* @function getBounds
* @returns {Object} Leaflet Bounds object, or null if the bounds cannot be determined
*/
self.getBounds = function () {
var bounds = null;
if (self.countFeatures() > 0) {
var hasGetBounds = true;
drawnItems.eachLayer(function (layer) {
hasGetBounds |= _.isUndefined(layer.getBounds);
});
if (hasGetBounds) {
bounds = drawnItems.getBounds();
}
}
return bounds;
};
/**
* Zoom to the specified level and centre the map at the specified coordinates
*
* @memberOf ALA.Map
* @function zoom
* @param {Number} zoom The zoom level
* @param {Object} centre The coordinates to centre the map on. Must be an object with 'lat' and 'lng' attributes. Defaults to the map's default centre if not provided.
*/
self.zoom = function (zoom, centre) {
mapImpl.setZoom(zoom, {animate: ANIMATE});
mapImpl.panTo(centre || self.DEFAULT_CENTRE, {animate: ANIMATE});
};
/**
* Zoom and centre the map to fit the bounds of the current feature(s). If there are no features, then the map will
* be set to the default zoom and centre.
*
* @memberOf ALA.Map
* @function fitBounds
*/
self.fitBounds = function () {
self.startLoading();
if (self.countFeatures() > 0) {
var hasGetBounds = true;
drawnItems.eachLayer(function (layer) {
hasGetBounds |= _.isUndefined(layer.getBounds);
});
if (hasGetBounds) {
mapImpl.fitBounds(drawnItems.getBounds(), {maxZoom: options.maxAutoZoom, animate: ANIMATE});
} else {
// cannot determine the bounds from the layers, set the map centre and zoom level to the defaults
mapImpl.setZoom(DEFAULT_ZOOM, {animate: ANIMATE});
mapImpl.panTo(self.DEFAULT_CENTRE, {animate: ANIMATE});
}
} else {
mapImpl.setZoom(DEFAULT_ZOOM, {animate: ANIMATE});
mapImpl.panTo(self.DEFAULT_CENTRE, {animate: ANIMATE});
}
self.finishLoading();
};
/**
* Zoom and centre the map to fit the bounds of the provided GeoJSON object, but do NOT draw the object.
*
* Subsequent calls to Reset Map will automatically fit the map to the bounds of this object.
*
* The difference between this and {@link setMaxBounds} is that setMaxBounds will prevent the user from panning
* outside the provided bounds, whereas this method will allow that.
*
* @memberOf ALA.Map
* @function fitToBoundsOf
* @param {Object} geoJSON Valid GeoJSON object to fit the map to
*/
self.fitToBoundsOf = function (geoJSON) {
if (typeof geoJSON === 'string') {
geoJSON = JSON.parse(geoJSON);
}
fitToBoundsOfLayer = geoJSON;
var geoJsonLayer = new L.FeatureGroup();
L.geoJson(geoJSON, {
pointToLayer: pointToLayerCircleSupport,
onEachFeature: function (feature, layer) {
if (feature.properties && feature.properties.pid) {
var wmsOptions = {
pid: feature.properties.pid,
viewparams: "s:" + feature.properties.pid,
wmsFeatureUrl: options.wmsFeatureUrl + feature.properties.pid,
callback: function () {
if (geoJsonLayer.getBounds && !_.isUndefined(geoJsonLayer.getBounds())) {
mapImpl.fitBounds(geoJsonLayer.getBounds(), {maxZoom: options.maxAutoZoom, animate: ANIMATE});
}
}
};
_.defaults(wmsOptions, DEFAULT_WMS_PROPERTIES);
layer = L.tileLayer.smartWms(options.wmsLayerUrl, wmsOptions);
layer.retrieveLayer();
}
geoJsonLayer.addLayer(layer);
}
});
// WMS layers may not have the bounds, or the fully populated bounds, until the layer is retrieved from the server.
// There is a callback above which fits the bounds for WMS layers after they are populated.
if (geoJsonLayer.getBounds && !_.isUndefined(geoJsonLayer.getBounds()) && !_.isUndefined(geoJsonLayer.getBounds().getNorthEast())) {
mapImpl.fitBounds(geoJsonLayer.getBounds(), {maxZoom: options.maxAutoZoom, animate: ANIMATE});
}
};
/**
* Zoom and centre the map to fit the provided bounds, and prevent the user from moving outside that boundary.
*
* Subsequent calls to Reset Map will automatically fit the map to the bounds of this object.
*
* The difference between this and {@link fitToBoundsOf} is that fitToBoundsOf will allow the user to pan outside
* the provided bounds, whereas this method will not.
*
* @memberOf ALA.Map
* @function setMaxBounds
* @param {LatLngBounds} latLngBounds The Leaflet LatLngBounds object to restrict the map to
*/
self.setMaxBounds = function (latLngBounds) {
mapImpl.setMaxBounds(latLngBounds);
};
/**
* Clears any limits on the map bounds. This will remove the max bounds (from {@link setMaxBounds}) and the display
* bounds (from {@link fitToBoundsOf}) if they exist. The map will be reset to the bounds of the current content
* (as per {@link fitBounds}).
*
* @memberOf ALA.Map
* @function clearBoundLimits
*/
self.clearBoundLimits = function () {
if (!_.isUndefined(fitToBoundsOfLayer) && fitToBoundsOfLayer != null) {
fitToBoundsOfLayer = null;
}
self.fitBounds();
};
/**
* Invalidates the size of the map to trigger a re-draw, then fits the viewport to the current bounds.
*
* @memberOf ALA.Map
* @function redraw
*/
self.redraw = function () {
mapImpl.invalidateSize();
self.fitBounds();
};
/**
* Adds a control widget to the map
*
* @memberOf ALA.Map
* @function addControl
* @param {L.Control} control The Leaflet Control to add
*/
self.addControl = function (control) {
mapImpl.addControl(control);
};
/**
* Adds a button to the map..
*
* E.g.
* <code>
* addButton('fa fa-map-marker', function (button, map) { ... }
* </code>
*
* @memberOf ALA.Map
* @function addButton
* @param {String} style The CSS style, or a HTML string with a span or image etc, for the button
* @param {function} closure A function pointer for the function to be invoked. The first parameter of the function will be the
* leaflet button. The second parameter will be the leaflet map. I.e.
* @param {String} position Leaflet Position value. Defaults to 'topleft'
*/
self.addButton = function (style, closure, position) {
var button = L.easyButton(style, closure).addTo(mapImpl);
if (!_.isUndefined(position)) {
button.setPosition(position);
}
};
/**
* Retrieve all leaflet markers on the map
*
* @memberOf ALA.Map
* @function getAllMarkers
* @returns {Array} of all L.Markers on the map
*/
self.getAllMarkers = function () {
return _.clone(markers);
};
/**
* Retrieve an array of objects of format [lat: x, lng: y] containing the coordinates of all markers on the map
*
* @memberOf ALA.Map
* @function getMarkerLocations
* @returns {Array} [lat: x, lng: y] of all markers
*/
self.getMarkerLocations = function () {
var locations = [];
markers.forEach(function (marker) {
locations.push({lat: marker.getLatLng().lat, lng: marker.getLatLng().lng});
});
return locations;
};
/**
* Retrieve a count of all features (shapes, layers, markers, etc) on the map
*
* @memberOf ALA.Map
* @function countFeatures
* @return {Integer} count of all features (shapes, layers, markers, etc) on the map
*/
self.countFeatures = function () {
var count = 0;
drawnItems && drawnItems.eachLayer(function () {
count++;
});
return count;
};
/**
* Display a loading spinner on the map.
*
* Note: This is done automatically for any direct changes on the map.
*
* @memberOf ALA.Map
* @function startLoading
*/
self.startLoading = function () {
mapImpl.fire("dataloading");
};
/**
* Stop the loading spinner on the map.
*
* Note: This is done automatically for any direct changes on the map.
*
* @memberOf ALA.Map
* @function finishLoading
*/
self.finishLoading = function () {
mapImpl.fire("dataload");
};
/**
* Toggle the view between fitting the bounds of the data and the initial centre and zoom
*
* @memberOf ALA.Map
* @function toggleFitBounds
*/
self.toggleFitBounds = function () {
fitToBoundsToggle = !fitToBoundsToggle;
var button = $(".ala-map-fit-bounds");
if (fitToBoundsToggle) {
self.fitBounds();
button.removeClass("fa-search-plus");
button.addClass("fa-search-minus");
} else {
self.zoom(options.zoom, options.centre);
button.removeClass("fa-search-minus");
button.addClass("fa-search-plus");
}
};
/**
* Add a layer selection control to the map. If options#defaultLayersControl = false, then this function can be used
* to place the layers control in a position other than the default (top right).
*
* If the layer control has already been added, then the baselayers and overlays will be added to the existing control.
* The controlOptions will be ignored.
*
* See http://leafletjs.com/reference.html#control-layers for details.
*
* @memberOf ALA.Map
* @function addLayersControl
* @param {Object} baseLayers Collection of base layers with names to add to the control
* @param {Object} overlays Collection of overlay layers with names to add to the control
* @param {Object} controlOptions config options, including overlayLayersSelectedByDefault: List of String
* @return {L.Control.Layers} added layer control
*/
self.addLayersControl = function (baseLayers, overlays, controlOptions) {
// get overlay layers to select by default
var overlayLayersSelectedByDefault = [];
if (controlOptions && controlOptions.overlayLayersSelectedByDefault) {
overlayLayersSelectedByDefault = controlOptions.overlayLayersSelectedByDefault;
delete controlOptions.overlayLayersSelectedByDefault;
}
var baseLayersToAdd = baseLayers || options.otherLayers || {};
var overlayLayersToAdd = overlays || {};
var baseLayerCount = Object.keys(baseLayersToAdd).length;
var overlayLayerCount = Object.keys(overlayLayersToAdd).length;
// create control with layers or update layers
if (layerControl) {
if (baseLayers) {
for (var baseLayerName in baseLayers) {
layerControl.addBaseLayer(baseLayers[baseLayerName], baseLayerName);
}
}
if (overlays) {
for (var existingControlOverlayName in overlays) {
var existingControlOverlayLayer = overlays[existingControlOverlayName];
var existingControlOverlaySelected = overlayLayersSelectedByDefault
.indexOf(existingControlOverlayName) > -1;
overlayLayerAdd(
existingControlOverlayLayer,
existingControlOverlayName,
existingControlOverlaySelected);
}
}
console.log("[ALA-Map] Updated layer control to add " + baseLayerCount + " base layers and " + overlayLayerCount + " overlay layers.");
} else {
_.defaults(controlOptions, DEFAULT_LAYER_CONTROL_OPTIONS);
layerControl = L.control.layers(baseLayersToAdd, overlayLayersToAdd, controlOptions);
layerControl.addTo(mapImpl);
// always display the loading indicator below the layer control
addLoadingControl();
// add the overlays to the list of overlays
for (var newControlOverlayName in overlays) {
var newControlOverlayLayer = overlays[newControlOverlayName];
var newControlOverlaySelected = overlayLayersSelectedByDefault.indexOf(newControlOverlayName) > -1;
overlayLayerAdd(
newControlOverlayLayer,
newControlOverlayName,
newControlOverlaySelected);
}
console.log("[ALA-Map] Created layer control with " + baseLayerCount + " base layers and " + overlayLayerCount + " overlay layers.");
}
if (options.addLayersControlHeading)
addBaseLayerAndOverlayHeading('#' + id);
return layerControl;
};
/**
* Highlight all features on the map with a property matching the provided name and value.
* @param propertyName - feature property name to match
* @param propertyValue - feature property value to match
*/
self.highlightFeaturesByProperty = function(propertyName, propertyValue) {
self.unHighlightAllFeatures();
drawnItems.eachLayer(function (layer) {
if (layer.feature && layer.feature.properties && layer.feature.properties[propertyName] === propertyValue) {
self.highlightLayer(layer);
}
});
}
/**
* Remove highlight from all features on the map with a property matching the provided name and value.
* @param propertyName - feature property name to match
* @param propertyValue - feature property value to match
*/
self.unHighlightFeaturesByProperty = function(propertyName, propertyValue) {
drawnItems.eachLayer(function (layer) {
if (layer.feature && layer.feature.properties && layer.feature.properties[propertyName] === propertyValue) {
self.unHighlightLayer(layer);
}
});
}
/**
* Remove highlight from all features on the map.
*/
self.unHighlightAllFeatures = function() {
drawnItems.eachLayer(function (layer) {
self.unHighlightLayer(layer);
});
};
/**
* Removes highlight of the provided layer on the map. For shapes, this is done by reducing the stroke weight and fill opacity.
* For markers, this is done by reducing the icon size.
* @param layer
*/
self.unHighlightLayer = function (layer) {
if (!layer) {
return;
}
var options = layer._originalOptions;
if (layer.setStyle && options) {
if (options && layer.setStyle) {
var style = {
weight: options.weight / 3,
fillOpacity: DEFAULT_OPACITY,
color: options.color
};
layer.setStyle(style);
}
}
else if (options && options.icon) {
icon.options.iconSize = [icon.options.iconSize[0]/1.5, icon.options.iconSize[1]/1.5];
icon.options.iconAnchor = [icon.options.iconAnchor[0]/1.5, icon.options.iconAnchor[1]/1.5];
layer.setIcon(icon);
}
};
/**
* Highlight the provided layer on the map. For shapes, this is done by increasing the stroke weight and fill opacity.
* For markers, this is done by increasing the icon size.
* @param layer
*/
self.highlightLayer = function (layer) {
if (!layer) {
return;
}
if (!layer._originalOptions) {
// deep clone the options to preserve the original values for unHighlightLayer
layer._originalOptions = JSON.parse(JSON.stringify(layer.options));
}
var options = layer._originalOptions;
if (!options) {
return; // TODO Known shapes don't have options
}
if (layer.setStyle) {
var style = {
weight: options.weight * 3,
fillOpacity: 1,
color: options.color
};
layer.setStyle(style);
if (layer.bringToFront) {
layer.bringToFront();
}
}
else if (options.icon) {
var icon = options.icon;
icon.options.iconSize = [icon.options.iconSize[0]*1.5, icon.options.iconSize[1]*1.5];
icon.options.iconAnchor = [icon.options.iconAnchor[0]*1.5, icon.options.iconAnchor[1]*1.5];
layer.setIcon(icon);
}
};
// ----------------------
// Private functions
// ----------------------
// Main initialiser
function initialiseMap() {
var enableDrawing = options.drawControl;
options.drawControl = false;
mapImpl = L.map(id, options);
mapImpl.addLayer(drawnItems);
addCoordinates();
L.Icon.Default.imagePath = getLeafletImageLocation();
mapImpl.addLayer(options.baseLayer);
if (options.defaultLayersControl) {
self.addLayersControl(options.otherLayers, options.overlays, {overlayLayersSelectedByDefault: options.overlayLayersSelectedByDefault, autoZIndex: options.autoZIndex});
}
if (options.showFitBoundsToggle) {
var css = "ala-map-fit-bounds fa " + (options.zoomToObject ? "fa-search-minus" : "fa-search-plus");
self.addButton("<span class='" + css + "' title='Toggle between the full map and the bounds of the data'></span>", self.toggleFitBounds, "topleft");
}
if (options.useMyLocation) {
var title = options.myLocationControlTitle || "Use my location";
self.addButton("<span class='ala-map-my-location fa fa-location-arrow' title='" + title + "'></span>", self.markMyLocation, "topleft");
}
if (options.allowSearchLocationByAddress) {
addGeocodeControl(ALA.MapConstants.DRAW_TYPE.POINT_TYPE);
}
if (options.allowSearchRegionByAddress) {
addGeocodeControl(ALA.MapConstants.DRAW_TYPE.POLYGON_TYPE);
}
if (enableDrawing) {
initDrawingControls(options);
}
if (options.allowKnownShapesControl && options.knownShapesOptions && options.knownShapesOptions.featuresServiceUrl && options.knownShapesOptions.regionListUrl) {
addKnownShapeControl(options.knownShapesOptions);
}
if(options.trackWindowHeight) {
addWindowResizeListener();
adjustMapContainerHeight();
}
if (options.showReset) {
self.addButton("<span class='ala-map-reset fa fa-refresh reset-map' title='Reset map'></span>", self.resetMap, "bottomright");
}
if (options.addGeometryFromLocalFile) {
addFileInputControl(options);
}
// If the map container is not visible, add a listener to trigger a redraw once it becomes visible.
// This avoids problems with the map viewport being initialised to an incorrect size because Leaflet could not
// determine the size of the container.
var container = $("#" + id);
if (!container.is(":visible")) {
container.onImpression({
callback: self.redraw
});
}
// make sure the base layers never sit on top of other layers when the base layer is changed
mapImpl.on('baselayerchange', function (event) {
currentBaseLayer = event.layer;
if (event.layer.setZIndex) {
event.layer.setZIndex(-1);
}
});
// when an overlay layer is selected, bring it to the front and mark it as selected
mapImpl.on('overlayadd', function (e) {
if (e && e.layer) {
overlayLayerSelect(e.layer);
}
});
// when an overlay layer is de-selected, remove selected marker
mapImpl.on('overlayremove', function (e) {
if (e && e.layer) {
overlayLayerDeselect(e.layer);
}
});
}
/**
* Populate any missing configuration items with the default values.
* @param currentOptions
*/
function populateDefaultOptions(currentOptions) {
_.defaults(currentOptions, DEFAULT_OPTIONS);
populateDefaultBaseLayerOptions(currentOptions);
console.log("[ALA-Map] Default options have been set.");
}
/**
* Sets up map base layers - options.baseLayer specifies the default and options.otherLayers specifies the available base layers.
* If baseLayer is not set, it will be set to the default base layer.
* If otherLayers is not set, it will be set to the default list, with the default base layer included if it is one of the pre-defined base layers.
* If otherLayers is provided, the layer set as baseLayer must be included in the otherLayers object.
*
* @param currentOptions
*/
function populateDefaultBaseLayerOptions(currentOptions){
var selectedBaseLayerName = '';
// get the name and object for the default selected base layer
var defaultBaseLayerName = '';
var defaultBaseLayerObject = null;
for (var availableDefaultLayerName in AVAILABLE_BASE_LAYERS) {
var availableDefaultLayerData = AVAILABLE_BASE_LAYERS[availableDefaultLayerName];
if (availableDefaultLayerData.defaultSelected) {
defaultBaseLayerName = availableDefaultLayerName;
defaultBaseLayerObject = L.tileLayer(availableDefaultLayerData.url, availableDefaultLayerData.options);
break;
}
}
console.log("[ALA-Map] Found default base layer from settings '" + defaultBaseLayerName + "'.");
// if options.baseLayer is not set, set to default selected base layer object
if (_.isUndefined(currentOptions.baseLayer)) {
currentOptions.baseLayer = defaultBaseLayerObject;
selectedBaseLayerName = defaultBaseLayerName;
console.log("[ALA-Map] No default base layer specified, set default base layer '" + selectedBaseLayerName + "'.");
}
// if options.baseLayer is a string, set to matching built-in base layer object
if (_.isString(currentOptions.baseLayer)) {
for (var availableStringLayerName in AVAILABLE_BASE_LAYERS) {
var availableStringLayerData = AVAILABLE_BASE_LAYERS[availableStringLayerName];
if (currentOptions.baseLayer.replace(' ', '').toUpperCase() === defaultBaseLayerName.replace(' ', '').toUpperCase()) {
currentOptions.baseLayer = defaultBaseLayerObject;
selectedBaseLayerName = defaultBaseLayerName;
console.log("[ALA-Map] Set default base layer from specified string '" + selectedBaseLayerName + "'.");
break;
} else if (
availableStringLayerName.toString().replace(' ', '').toUpperCase() ===
currentOptions.baseLayer.replace(' ', '').toUpperCase() ||
availableStringLayerData.title.replace(' ', '').toUpperCase() ===
currentOptions.baseLayer.replace(' ', '').toUpperCase()) {
currentOptions.baseLayer = L.tileLayer(availableStringLayerData.url, availableStringLayerData.options);
selectedBaseLayerName = availableStringLayerName;
console.log("[ALA-Map] Set default base layer from specified string matching title or id '" + selectedBaseLayerName + "'.");
break;
}
}
if (_.isString(currentOptions.baseLayer)) {
console.warn("[ALA-Map] Unrecognised base layer name: '" + currentOptions.baseLayer + "', " +
"supported base layers are '" + Object.keys(AVAILABLE_BASE_LAYERS).sort().join("', '") + "'. " +
"Using default base layer '" + defaultBaseLayerName + "'.");
currentOptions.baseLayer = defaultBaseLayerObject;
selectedBaseLayerName = defaultBaseLayerName;
}
}
// if options.baseLayer is an object, assume it is a Layer
if(!_.isObject(currentOptions.baseLayer)){
// anything else for options.baseLayer is an error
throw new Error("[ALA-Map] Unexpected value for options.baseLayer: '" + typeof currentOptions.baseLayer + "' - '" + currentOptions.baseLayer + "'.")
}
// if options.otherLayers is not set, set to default base layers (including options.baseLayer)
if (_.isNull(currentOptions.otherLayers) || _.isUndefined(currentOptions.otherLayers)) {
currentOptions.otherLayers = {};
for (var availableOtherLayerName in AVAILABLE_BASE_LAYERS) {
var availableOtherLayerData = AVAILABLE_BASE_LAYERS[availableOtherLayerName];
if (availableOtherLayerName.replace(' ', '').toUpperCase() === selectedBaseLayerName.replace(' ', '').toUpperCase()) {
currentOptions.otherLayers[availableOtherLayerData.title] = currentOptions.baseLayer;
console.log("[ALA-Map] Added base layer to collection because it matched default base layer name '" + availableOtherLayerData.title + "'.");
} else if (availableOtherLayerData.url === currentOptions.baseLayer._url) {
currentOptions.otherLayers[availableOtherLayerData.title] = currentOptions.baseLayer;
console.log("[ALA-Map] Added base layer to collection because it matched default base layer url '" + availableOtherLayerData.title + "'.");
} else if (availableOtherLayerData.defaultInList && currentOptions.defaultLayersControl) {
currentOptions.otherLayers[availableOtherLayerData.title] = L.tileLayer(availableOtherLayerData.url, availableOtherLayerData.options);
console.log("[ALA-Map] Added base layer to collection because defaultLayersControl is true and settings say add by default '" + availableOtherLayerData.title + "'.");
}
}
}
// if options.otherLayers is set, it must be an object.
if(!_.isNull(currentOptions.otherLayers) && !_.isUndefined(currentOptions.otherLayers) && !_.isObject(currentOptions.otherLayers)){
throw new Error("[ALA-Map] Unexpected value for options.otherLayers: '" + typeof currentOptions.otherLayers + "' - '" + currentOptions.otherLayers + "'.")
}
// For each property of the object,
// - if the property is an object, assume it is a Layer.
// - if the property is a string, set to the matching built-in layer.
// The options.baseLayer object must be one of the property values, it is an error if not.
for (var optionsOtherLayerKey in currentOptions.otherLayers) {
var optionsOtherLayerValue = currentOptions.otherLayers[optionsOtherLayerKey];
if(_.isString(optionsOtherLayerValue)){
var optionsOtherLayerData = AVAILABLE_BASE_LAYERS[optionsOtherLayerValue];
if (optionsOtherLayerKey.replace(' ', '').toUpperCase() === selectedBaseLayerName.replace(' ', '').toUpperCase()) {
currentOptions.otherLayers[optionsOtherLayerKey] = currentOptions.baseLayer;
console.log("[ALA-Map] Added named base layer to collection because it matched default base layer name '" + optionsOtherLayerKey + "'.");
} else if (optionsOtherLayerData.url === currentOptions.baseLayer._url) {
currentOptions.otherLayers[optionsOtherLayerKey] = currentOptions.baseLayer;
console.log("[ALA-Map] Added named base layer to collection because it matched default base layer url '" + optionsOtherLayerKey + "'.");
} else {
currentOptions.otherLayers[optionsOtherLayerKey] = L.tileLayer(optionsOtherLayerData.url, optionsOtherLayerData.options);
console.log("[ALA-Map] Added base layer matching string in otherLayers '" + optionsOtherLayerKey + "'.");
}
} else if(!_.isObject(optionsOtherLayerValue)){
throw new Error("[ALA-Map] Unexpected value for options.otherLayers['" + optionsOtherLayerKey + "']: '" + typeof optionsOtherLayerValue + "' - '" + optionsOtherLayerValue + "'.")
}
}
console.log("[ALA-Map] Populated defaults for options.baseLayer with url '" + (currentOptions.baseLayer ? currentOptions.baseLayer._url : '') + "'" +
" and options.otherLayers '" + (currentOptions.otherLayers ? Object.keys(currentOptions.otherLayers).sort().join(', ') : '') + "'.");
}
/**
* Initialise the drawing controls that appear on the left side of the map panel
* @param currentOptions
*/
function initDrawingControls(currentOptions) {
var drawOptions = currentOptions.drawOptions || {},
editOptions;
_.defaults(drawOptions, DEFAULT_DRAW_OPTIONS);
if(drawOptions.edit){
editOptions = currentOptions.editOptions || {};
_.defaults(editOptions, DEFAULT_EDIT_DRAW_OPTIONS);
} else {
editOptions = false;
}
drawControl = {
edit: editOptions,
draw: drawOptions
};
if (editOptions) {
mapImpl.pm.addControls({
position: "topleft",
drawMarker: drawOptions.marker !== false,
drawPolyline: drawOptions.polyline !== false,
drawRectangle: drawOptions.rectangle !== false,
drawPolygon: drawOptions.polygon !== false,
drawCircle: drawOptions.circle !== false,
drawCircleMarker:false,
drawText: false,
cutPolygon: true,
dragMode: true,
removalMode: true,
rotateMode: true,
snappingOption: true
});
mapImpl.pm.setGlobalOptions({
limitMarkersToCount: 100, // adding all marker can cause performance issues when site is large
allowSelfIntersection: false, // disallow self-intersection for polygons
hideMiddleMarkers: false
});
addCustomCancelActionToEditModes();
}
function onLayerCreated(layerType, layer) {
if (normaliseLayerType(layerType) === ALA.MapConstants.LAYER_TYPE.MARKER) {
if (currentOptions.singleMarker) {
markers = [];
}
if (currentOptions.draggableMarkers) {
layer.options.draggable = true;
layer.on("dragend", self.notifyAll);
}
addMarker(layer, true);
} else {
addLayer(layer, true);
}
}
mapImpl.on("pm:create", function (event) {
onLayerCreated(event.shape, event.layer);
});
mapImpl.on("pm:globaleditmodetoggled", function (event) {
handleGlobalModeToggle("editMode", event.enabled);
});
mapImpl.on("pm:globaldragmodetoggled", function (event) {
handleGlobalModeToggle("dragMode", event.enabled);
});
mapImpl.on("pm:globalremovalmodetoggled", function (event) {
handleGlobalModeToggle("removalMode", event.enabled);
});
mapImpl.on("pm:globalcutmodetoggled", function (event) {
handleGlobalModeToggle("cutPolygon", event.enabled);
});
mapImpl.on("pm:globalrotatemodetoggled", function (event) {
handleGlobalModeToggle("rotateMode", event.enabled);
});
mapImpl.on("pm:drawstart", function (event) {
drawingStarted(event.shape);
});
mapImpl.on("pm:cut", function (event) {
drawnItems.removeLayer(event.originalLayer);
});
registerSpinnerEvents();
updateCircleFeaturesToIncludeTypeAndRadius();
}
function addWindowResizeListener() {
window.addEventListener('resize', adjustMapContainerHeight);
}
function adjustMapContainerHeight() {
var height = window.innerHeight - DEFAULT_MAP_HEIGHT_BUFFER;
if (height >= options.minMapHeight) {
$('#'+id).height(height);
mapImpl && mapImpl.invalidateSize();
}
}
function registerSpinnerEvents() {
var loadingEvents = ["movestart", "dragstart", "zoomstart"];
loadingEvents.forEach(function (eventName) {
mapImpl.on(eventName, function () {
self.startLoading();
})
});
var loadingFinishedEvents = ["moveend", "dragend", "zoomend"];
loadingFinishedEvents.forEach(function (eventName) {
mapImpl.on(eventName, function () {
self.finishLoading();
})
});
}
// Determines if existing items need to be removed before adding a new item
function drawingStarted(layerType) {
layerType = normaliseLayerType(layerType);
if (layerType === ALA.MapConstants.LAYER_TYPE.MARKER) {
if (options.singleMarker) {
markers.forEach(function (marker) {
drawnItems.removeLayer(marker);
});
markers = [];
if (options.markerOrShapeNotBoth) {
drawnItems.clearLayers();
}
}
} else if (options.singleDraw) {
drawnItems.clearLayers();
if (options.markerOrShapeNotBoth) {
markers.forEach(function (marker) {
drawnItems.removeLayer(marker);
});
markers = [];
}
}
}
function normaliseLayerType(layerType) {
if (!layerType) {
return layerType;
}
if (!_.isString(layerType)) {
return layerType;
}
if (layerType.toLowerCase() === "line") {
return "polyline";
}
return layerType.toLowerCase();
}
/**
* Adding custom cancel button as Geoman free version does not come with cancel button.
*/
function addCustomCancelActionToEditModes() {
if (!mapImpl.pm || !mapImpl.pm.Toolbar || !mapImpl.pm.Toolbar.changeActionsOfControl) {
return;
}
var modeConfigs = [
{name: "editMode", title: "Cancel edit changes", cancelActionName: "editModeCancel"},
{name: "dragMode", title: "Cancel drag changes", cancelActionName: "dragModeCancel"},
{name: "cutPolygon", title: "Cancel cut changes", cancelActionName: "cutPolygonCancel"},
{name: "removalMode", title: "Cancel remove changes", cancelActionName: "removalModeCancel"},
{name: "rotateMode", title: "Cancel rotate changes", cancelActionName: "rotateModeCancel"}
];
modeConfigs.forEach(function (modeConfig) {
mapImpl.pm.Toolbar.changeActionsOfControl(modeConfig.name, [
"finishMode", // complete the mode action and save changes
{ // custom cancel action
text: "Cancel",
name: modeConfig.cancelActionName,
title: modeConfig.title,
onClick: function () {
cancelGlobalModeChanges(modeConfig.name);
}
}
]);
});
}
/**
* Take a snapshot of the current map state when an edit mode is enabled to restore state if the action is cancelled in the future.
* @param modeName
* @param enabled
*/
function handleGlobalModeToggle(modeName, enabled) {
if (enabled) {
editModeSnapshotGeoJSON = JSON.stringify(self.getGeoJSON());
snapshotMode = modeName;
return;
}
// Clear the snapshot if the mode has finished operation only if it is the same mode that was turned on
// when the snapshot was taken.
if (snapshotMode === modeName) {
editModeSnapshotGeoJSON = null;
snapshotMode = null;
}
if (!suppressEditModeNotify) {
self.notifyAll();
}
}
/**
* Restore map to state before mode was enabled, and disable the mode.
* This is triggered when the user clicks the custom cancel button added to the mode actions.
* @param modeName
*/
function cancelGlobalModeChanges(modeName) {
if (!editModeSnapshotGeoJSON) {
return;
}
if (snapshotMode && snapshotMode !== modeName) {
return;
}
var snapshot = editModeSnapshotGeoJSON;
suppressEditModeNotify = true;
disableGlobalMode(modeName);
suppressEditModeNotify = false;
restoreGeoJSONSnapshot(snapshot);
editModeSnapshotGeoJSON = null;
snapshotMode = null;
}
/**
* Disable the global mode if it is enabled.
* This is used when cancelling an edit action to turn off the mode that was turned on when the snapshot was taken.
* @param modeName
*/
function disableGlobalMode(modeName) {
if (modeName === "editMode" && mapImpl.pm.globalEditModeEnabled()) {
mapImpl.pm.disableGlobalEditMode();
} else if (modeName === "dragMode" && mapImpl.pm.globalDragModeEnabled()) {
mapImpl.pm.disableGlobalDragMode();
} else if (modeName === "cutPolygon" && mapImpl.pm.globalCutModeEnabled()) {
mapImpl.pm.disableGlobalCutMode();
} else if (modeName === "removalMode" && mapImpl.pm.globalRemovalModeEnabled()) {
mapImpl.pm.disableGlobalRemovalMode();
} else if (modeName === "rotateMode" && mapImpl.pm.globalRotateModeEnabled()) {
mapImpl.pm.disableGlobalRotateMode();
}
}
function restoreGeoJSONSnapshot(snapshotGeoJSON) {
drawnItems.clearLayers();
markers = [];
if (!snapshotGeoJSON) {
self.notifyAll();
return;
}
self.setGeoJSON(snapshotGeoJSON);
}
// This is a workaround for https://github.com/Leaflet/Leaflet/issues/2888
// The GeoJSON standard does not support Circle types, so Leaflet treats circles as points. This workaround adds a
// 'point_type' attribute to the the feature's properties with value 'Circle', and adds the radius, so at least the
// feature properties include the circle details. This is still valid GeoJSON.
function updateCircleFeaturesToIncludeTypeAndRadius() {
var circleToGeoJSON = L.Circle.prototype.toGeoJSON;
L.Circle.include({
toGeoJSON: function () {
var feature = circleToGeoJSON.call(this);
feature.properties = {
// adding a generic type property to distinguish MERIT Point of Interest (POI) from types such as circle, pid, marker.
type: ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE,
point_type: ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE,
radius: this.getRadius()
};
return feature;
}
});
}
// Render any GeoJSON feature where the geometry type = Point but the properties contains point_type = 'Circle'
// as a circle instead of a point. This is because GeoJSON does not support Circle types.
function pointToLayerCircleSupport(feature, latlng) {
if (feature.properties && feature.properties.point_type === ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE) {
if (feature.properties.circleOptions)
return L.circle(latlng, feature.properties.radius, feature.properties.circleOptions);
else
return L.circle(latlng, feature.properties.radius, {});
} else {
var marker = L.marker(latlng, {draggable: options.draggableMarkers});
if (options.draggableMarkers) {
marker.on("dragend", self.notifyAll);
}
markers.push(marker);
return marker;
}
}
function defaultPopupContent(feature) {
var popupContent = "";
var rows = [];
for (var property in feature.properties) {
if (feature.properties.hasOwnProperty(property))
rows.push("<tr><td>" + property + "</td><td>" + feature.properties[property] + "</td></tr>");
}
if (rows.length > 0)
popupContent = "<table><tbody>" + rows.join("") + "</tbody></table>";
return popupContent;
}
/**
* Callback function for rendering GeoJSON features. This function supports rendering WMS layers for features with
* a 'pid' property, and also supports creating popups based on feature properties.
* @param feature
* @param layer
* @param layerOptions
*/
function onEachFeatureWithWMSLayerSupport(feature, layer, layerOptions) {
var wmsOptions = {};
updateFeatureProperties(feature, layer);
assignFeatureId(layer, feature);
//Create a popup content
if(feature.properties && feature.properties.popupContent)
layer.bindPopup(feature.properties.popupContent);
else if (feature.properties) {
var popupContent = defaultPopupContent(feature);
popupContent && layer.bindPopup(popupContent);
}
if (feature.properties && feature.properties.pid) {
if (feature.geometry.type == ALA.MapConstants.DRAW_TYPE.POINT_TYPE){
wmsOptions.layers = "ALA:Points"
wmsOptions.opacity = 1.0
}
layer = createWmsLayer(feature.properties.pid, wmsOptions);
}
if (options.singleDraw) {
drawnItems.clearLayers();
}
if (options.markerOrShapeNotBoth) {
clearMarkers();
}
drawnItems.addLayer(layer);
if (layer.bringToFront) {
layer.bringToFront();
}
applyLayerOptions(layer, layerOptions);
layerCreatedByGeoJSON = layer;
}
/**
* Assign the feature properties to the layer, ensuring that if the layer already has feature properties,
* they are not overwritten but merged with the new properties.
* @param feature
* @param layer
*/
function updateFeatureProperties(feature, layer) {
if (feature.properties) {
if (!layer.feature) {
layer.feature = {
type: "Feature",
properties: feature.properties
};
}
else if (!layer.feature.properties) {
layer.feature.properties = feature.properties;
}
else {
layer.feature.properties = Object.assign(layer.feature.properties, feature.properties);
}
}
}
/**
* Assign a unique featureId property to the feature and layer if it does not already exist.
* @param layer
* @param feature
*/
function assignFeatureId(layer, feature) {
if (typeof UUID === "undefined") {
console.error("[ALA-Map] UUID library is not included, cannot assign featureId to layer. Please include a UUID library.");
return;
}
var currentFeatureId = feature.properties && feature.properties.featureId;
if (!_.contains([undefined, null, ""], currentFeatureId)) {
return;
}
var featureId = UUID.generate();
feature.properties = feature.properties || {};
feature.properties.featureId = featureId;
layer.feature = layer.feature || {};
layer.feature.properties = layer.feature.properties || {};
layer.feature.properties.featureId = featureId;
}
// Adds the lat/lng coordinates to the bottom of the map panel
function addCoordinates() {
L.control.coordinates({
position: "bottomright",
decimals: 2,
enableUserInput: false,
useLatLngOrder: true
}).addTo(mapImpl);
}
// Adds a loading spinner to the top right of the map
function addLoadingControl() {
var loadingControl = L.Control.loading({
separate: true,
position: "topright"
});
mapImpl.addControl(loadingControl);
}
// The container div is expected to have an attribute 'data-leaflet-img' which contains the path to the Leaflet images.
// The path is the absolute URL, minus the host/port.
function getLeafletImageLocation() {
return $("#" + id).attr("data-leaflet-img");
}
// Display an input field and search button to allow the user to enter an address, then perform a geocode address
// search and place a marker on the map
function addGeocodeControl(drawType) {
var prompt = "Search for an address or location";
if (drawType == ALA.MapConstants.DRAW_TYPE.POLYGON_TYPE) {
prompt = "Search for a region, area or address";
}
var geocodeControl = L.Control.geocoder({
position: "topleft",
placeholder: prompt,
defaultMarkGeocode: false,
geocoder: new L.Control.Geocoder.Nominatim({
geocodingQueryParams: {polygon_geojson: 1, dedupe: 1}
})
}).addTo(mapImpl);
var button = $(geocodeControl._container).find("a");
button.attr("title", prompt);
if (drawType == ALA.MapConstants.DRAW_TYPE.POINT_TYPE) {
button.addClass("geocoder-icon-override fa fa-crosshairs");
}
geocodeControl.on('markgeocode', function (result) {
if (drawType == ALA.MapConstants.DRAW_TYPE.POINT_TYPE) {
self.addMarker(result.geocode.center.lat, result.geocode.center.lng, null);
} else if (drawType == ALA.MapConstants.DRAW_TYPE.POLYGON_TYPE) {
var geojson = {
type: "Feature",
geometry: result.geocode.properties.geojson,
properties: {}
};
if (options.geocodeRegionOptions.pointToCircle) {
// when selecting a geocoded region that has a Point type, we will convert it to a circle with a
// specific radius
if (geojson.geometry.type == ALA.MapConstants.DRAW_TYPE.POINT_TYPE) {
geojson.properties.type = ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE;
geojson.properties.point_type = ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE;
geojson.properties.radius = options.geocodeRegionOptions.pointRadiusMeters;
}
}
var layer = self.setGeoJSON(geojson);
if (options.zoomToObject) {
layer && layer.getBounds && mapImpl.fitBounds(layer.getBounds(), {maxZoom: options.maxAutoZoom, animate: ANIMATE});
}
} else {
self.addMarker(result.geocode.center.lat, result.geocode.center.lng, null);
}
//Fire a 'SearchEventFired' for site creation
mapImpl.fire("searchEventFired");
}, geocodeControl);
}
/**
* Adds a file input control to the map using the leaflet-filelayer plugin, which allows the user to load
* GeoJSON, KML, GPX or Shapefile (in zip format) files from their local machine onto the map.
* The file is parsed and added as a layer to the map, with support for WMS layers if the GeoJSON features have a 'pid' property.
* @param options
* @returns {*}
*/
function addFileInputControl(options) {
if (typeof L.Control.advancedFileLayerLoad === "undefined") {
console.error("[ALA-Map] L.Control.advancedFileLayerLoad is not defined. You must include the leaflet-filelayer plugin to use the file input control.");
return;
}
var controlOptions = _.defaults( options.fileInputControlOptions || {}, {
addToMap: false,
formats: [
'.geojson',
'.json', // geojson with non-standard file extension
'.kml',
'.gpx',
'.zip' // shapefile in zip format
],
layerOptions: {
onEachFeature: onEachFeatureWithWMSLayerSupport,
},
layer: function (geoJSON) {
if (options.simplifyImportedShapes) {
if (typeof turf === "undefined") {
console.error("[ALA-Map] turf is not defined. You must include the turf library to use the simplifyImportedShapes option.");
return ;
}
var config = _.defaults(options.simplifyOptions || {}, DEFAULT_SIMPLIFICATION_OPTIONS);
geoJSON = turf.simplify(geoJSON, config);
}
return self.setGeoJSON(geoJSON);
}
});
var fileInputControl = L.Control.advancedFileLayerLoad(controlOptions);
mapImpl.addControl(fileInputControl);
return fileInputControl;
}
/**
* Creates an instance of the L.Control.TwoStepSelector control and adds it to the map.
* This control allows the user to select a layer from a list of layers, and then select a shape from that layer, which is then added to the map as a WMS layer.
* @param options
*/
function addKnownShapeControl (options) {
options = Object.assign({
id: 'regionSelection',
title: 'Select layer',
firstStepPlaceholder: 'Choose a layer...',
secondStepPlaceholder: 'Choose a shape...',
}, options);
var featuresServiceUrl = options.featuresServiceUrl,
regionListUrl = options.regionListUrl,
regionOptions = {
firstStepItemLookup: function (populateStep1Callback) {
$.ajax({
url: regionListUrl,
dataType: 'json'
}).done(function (data) {
var regions = _.sortBy(data.regions, 'value');
populateStep1Callback(regions);
});
},
secondStepItemLookup: function (selectedLayerKey, populateStep2Callback) {
$.ajax({
url: featuresServiceUrl + '?layerId=' + selectedLayerKey,
dataType: 'json'
}).done(function (data) {
var layers = [];
data.forEach(function (layer) {
layers.push({key: layer.pid, value: layer.name});
});
layers = _.sortBy(layers, 'value');
populateStep2Callback(layers);
});
},
selectionAction: function (selectedValue) {
self.addWmsLayer(selectedValue);
}
};
mapImpl.addControl(new L.Control.TwoStepSelector(Object.assign({}, options, regionOptions)));
}
// Internal method to add a non-Marker layer to the map, to fit the map bounds if configured to do so, and optionally
// to notify all subscribers that the map has changed.
function addLayer(layer, notify) {
self.startLoading();
layer.addTo(drawnItems);
if (options.zoomToObject && layer.getBounds) {
mapImpl.fitBounds(drawnItems.getBounds(), {maxZoom: options.maxAutoZoom, animate: ANIMATE});
mapImpl.invalidateSize();
}
if (notify) {
self.notifyAll();
}
self.finishLoading();
}
// Internal method to add a Marker to the map, to fit the map bounds if configured to do so, and optionally to notify
// all subscribers that the map has changed.
function addMarker(marker, notify) {
self.startLoading();
drawingStarted(ALA.MapConstants.LAYER_TYPE.MARKER);
if (options.draggableMarkers) {
marker.on("dragend", self.notifyAll);
}
marker.addTo(drawnItems);
markers.push(marker);
self.finishLoading();
if (options.zoomToObject) {
mapImpl.panTo(marker.getLatLng(), {animate: ANIMATE});
mapImpl.fitBounds(new L.LatLngBounds(marker.getLatLng(), marker.getLatLng()), {maxZoom: SINGLE_POINT_ZOOM});
}
if (notify) {
self.notifyAll();
}
}
// Internal function to create a new WMS layer, but not to add it to the map, or trigger any notifications
function createWmsLayer(pid, wmsOptions) {
wmsOptions = wmsOptions || {};
if (!_.isUndefined(pid) && pid != null) {
wmsOptions.pid = pid;
wmsOptions.viewparams = "s:" + pid;
wmsOptions.wmsFeatureUrl = options.wmsFeatureUrl + pid;
}
wmsOptions.callback = function () {
if (options.zoomToObject) {
self.fitBounds();
}
self.notifyAll();
self.finishLoading();
};
_.defaults(wmsOptions, DEFAULT_WMS_PROPERTIES);
if ((_.isUndefined(options.wmsLayerUrl) || options.wmsLayerUrl == null) && (_.isUndefined(wmsOptions.wmsLayerUrl) || wmsOptions.wmsLayerUrl == null)) {
console.error("[ALA-Map] You must specify the wmsLayerUrl option for this map or for the layer.")
}
return L.tileLayer.smartWms(wmsOptions.wmsLayerUrl || options.wmsLayerUrl, wmsOptions);
}
// Internal function to apply layer-specific options to the provided layer. Handles all the logic to determine if
// particular options can be applied to different layer types
function applyLayerOptions(layer, layerOptions) {
if (_.isUndefined(layerOptions)) {
layerOptions = {};
}
_.defaults(layerOptions, LAYER_OPTIONS);
if (layer.bindPopup && layerOptions.popup && (!layerOptions.markerWithMouseOver || !layer.setStyle)) {
layer.bindPopup(layerOptions.popup);
}
if (layerOptions.markerWithMouseOver) {
hideLayer(layer);
var centre = layerOptions.markerLocation ? layerOptions.markerLocation : layer.getBounds().getCenter();
var placeholder = L.marker(centre);
placeholder.on("mouseover", function () {
showLayer(layer);
});
placeholder.on("mouseout", function () {
hideLayer(layer);
});
if (layerOptions.popup) {
placeholder.bindPopup(layerOptions.popup);
}
// this is not considered to be a 'marker' as with those added via addMarker, so don't add it to
// the 'markers' list.
drawnItems.addLayer(placeholder);
}
}
function hideLayer(layer) {
if (layer.setStyle) {
layer.setStyle(HIDDEN_LAYER)
} else if (layer.setOpacity) {
layer.setOpacity(0);
} else if (layer.options) {
layer.options.opacity = 0;
}
}
function showLayer(layer) {
if (layer.setStyle) {
layer.setStyle(VISIBLE_LAYER)
} else if (layer.setOpacity) {
layer.setOpacity(DEFAULT_OPACITY);
} else if (layer.options) {
layer.options.opacity = DEFAULT_OPACITY;
layer.options.weight = DEFAULT_LINE_WEIGHT;
}
if (layer.bringToFront) {
layer.bringToFront();
}
}
function clearMarkers() {
markers.forEach(function (marker) {
drawnItems.removeLayer(marker);
});
markers = [];
}
/**
* Internal function to add an overlay layer to the layers control.
* @param {L.Class} layer Layer to add.
* @param {String} name Text to display for the layer.
* @param {Boolean} isSelected Whether to add the layer in the selected state or not.
*/
function overlayLayerAdd(layer, name, isSelected){
if (layerControl) {
layerControl.addOverlay(layer, name);
overlayLayersAvailable.push(layer);
if (isSelected) {
mapImpl.addLayer(layer);
overlayLayerSelect(layer);
}
console.log("[ALA-Map] Added " + (isSelected ? 'selected' : 'de-selected') + " overlay layer '" + name + "' to map.");
} else {
console.warn("[ALA-Map] Could not add overlay layer '" + name + "' because the layer control is not available. " +
"Create the layer control before adding an overlay layer.")
}
}
/**
* Internal function to remove an overlay layer from the layers control.
* @param {L.Class} layer The layer to remove.
*/
function overlayLayerRemove(layer){
if (layerControl) {
overlayLayerDeselect(layer);
layerControl.removeLayer(layer);
var index = overlayLayersAvailable.indexOf(layer);
if (index > -1) {
overlayLayersAvailable.splice(index, 1);
}
}
mapImpl.removeLayer(layer);
}
/**
* Internal function to select an overlay layer.
* @param {L.Class} layer
*/
function overlayLayerSelect(layer){
if (!options.preserveZIndex) {
layer.bringToFront();
}
overlayLayersSelected.push(layer);
}
/**
* Internal function to de-select an overlay layer.
* @param {L.Class} layer
*/
function overlayLayerDeselect(layer){
var index = overlayLayersSelected.indexOf(layer);
if (index > -1) {
overlayLayersSelected.splice(index, 1);
}
}
function addBaseLayerHeading (containerSelector) {
var classHeading = 'leaflet-control-layers-base-heading',
classBaseLayer = 'leaflet-control-layers-base',
isHeadingPresent = $(containerSelector + ' .' + classHeading).length == 1,
isBaseLayerPresent = $(containerSelector + ' .' + classBaseLayer).children().length >= 1;
if (!isHeadingPresent && isBaseLayerPresent)
$('<label class="' + classHeading + '"><strong>Base layer</strong></label>').insertBefore(containerSelector + ' .' + classBaseLayer);
};
function addOverlayHeading(containerSelector) {
var classHeading = 'leaflet-control-layers-overlays-heading',
classOverlay = 'leaflet-control-layers-overlays',
isHeadingPresent = $(containerSelector + ' .' + classHeading).length == 1,
isOverlayPresent = $(containerSelector + ' .' + classOverlay).children().length >= 1;
if (!isHeadingPresent && isOverlayPresent)
$('<label class="' + classHeading + '"><strong>Overlay</strong></label>').insertBefore(containerSelector + ' .' + classOverlay);
};
function addBaseLayerAndOverlayHeading (containerSelector) {
addBaseLayerHeading(containerSelector);
addOverlayHeading(containerSelector);
};
initialiseMap();
};
/**
* @class
* @memberOf ALA
*/
ALA.MapUtils = {
/**
* Utility method to take a standard GeoJSON object and change the geometry of Point types to Circle types (not
* supported by the GeoJSON standard). This function assumes that the properties object of the circle features has
* a property of 'point_type' with a value of 'circle', and a property of 'radius'.
*
* I.e. this function will convert
* <code>
* {
* "type": "FeatureCollection",
* "features": [
* {
* "type": "Feature",
* "properties": {
* "point_type": "circle",
* "radius": 1027747.8506951465
* },
* "geometry": {
* "type": "Point",
* "coordinates": [
* 122.51953124999999,
* -13.410994034321702
* ]
* }
* }
* ]
* }
* </code>
* into
* <code>
* {
* "type": "FeatureCollection",
* "features": [
* {
* "type": "Feature",
* "properties": {
* "point_type": "circle",
* "radius": 1027747.8506951465
* },
* "geometry": {
* "type": "Circle",
* "coordinates": [
* 122.51953124999999,
* -13.410994034321702
* ],
* "centre": [
* 122.51953124999999,
* -13.410994034321702
* ],
* "radius": 1027747.8506951465
* }
* }
* ]
* }
* </code>
* @param geoJSON
* @returns {*}
*/
getGeometryWithCirclesFromGeoJSON: function (geoJSON) {
if (!_.isUndefined(geoJSON) && !_.isEmpty(geoJSON.features)) {
_.each(geoJSON.features, function (feature) {
if (feature.properties.point_type == ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE) {
feature.geometry.type = ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE;
feature.geometry.centre = feature.geometry.coordinates;
feature.geometry.radius = feature.properties.radius;
}
})
}
return geoJSON;
},
/**
* Performs the opposite of {@link #getGeometryWithCirclesFromGeoJSON} to produce valid standard GeoJSON.
*
* @param geoJSON The modified GeoJSON to be converted back to the standard format.
* @returns {GeoJSON}
*/
getStandardGeoJSONForCircleGeometry: function (geoJSON) {
if (!_.isUndefined(geoJSON) && !_.isEmpty(geoJSON.features)) {
_.each(geoJSON.features, function (feature) {
if (feature.geometry.type === ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE) {
feature.properties.type = ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE;
feature.properties.point_type = ALA.MapConstants.DRAW_TYPE.CIRCLE_TYPE;
feature.properties.radius = feature.geometry.radius;
delete feature.geometry.radius;
feature.geometry.type = ALA.MapConstants.DRAW_TYPE.POINT_TYPE;
}
})
}
return geoJSON;
},
/**
* Takes a bare geoJSON geometry object, and wraps it in a full GeoJSON structure with type=FeatureCollection
*
* @param geometry The geoJSON geometry object to wrap
* @returns {Object} Full GeoJSON structure with type=FeatureCollection
*/
wrapGeometryInGeoJSONFeatureCol: function (geometry) {
return {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: geometry
}
]
};
},
/**
* Converts a bounding box string of format POLYGON((lng1 lat1, lng2 lat2,...)) (POLYGON or LINESTRING) into an
* array of coordinates. The order of the latitude and longitude can be controlled by the latFirst parameter.
*
* @param bboxString Bounding box string to convert
* @param latFirst True to produce LAT/LNG points, false to produce LNG/LAT points.
* @returns {Array} array of points
*/
bboxToPointArray: function (bboxString, latFirst) {
var coords = bboxString.replace(/POLYGON|LINESTRING|POINT/g, "").replace(/[\\(|\\)]/g, "");
var coordsArray = [];
coords.split(",").forEach(function (item) {
var coordinates = latFirst ? item.split(" ").reverse() : item.split(" ");
coordinates = coordinates || [];
coordinates.forEach(function (t, index) {
coordinates[index] = Number.parseFloat(t);
});
coordsArray.push(coordinates);
});
return coordsArray;
},
/**
* Utility function to create a new marker icon with sensible defaults
*
* @param iconUrl {String} The URL to the image for the marker. Mandatory.
* @param iconOptions {Object} Object containing leaflet Icon configuration options to override the defaults. Optional.
* @returns {Object} Leaflet Icon object
*/
createIcon: function (iconUrl, iconOptions) {
if (_.isUndefined(iconOptions)) {
iconOptions = {};
}
var defaultOptions = {
iconUrl: iconUrl,
iconRetinaUrl: iconUrl,
iconSize: [25, 40],
popupAnchor: [0, -25]
};
_.defaults(iconOptions, defaultOptions);
return L.icon(iconOptions);
},
/**
* Convenience utility for creating a new marker with optional configuration parameters and popup.
*
* @param lat {Number} Latitude of the marker. Mandatory.
* @param lng {Number} Longitude of the marker. Mandatory.
* @param popup {String} Text or HTML to display in a popup when the marker is clicked. Optional.
* @param markerOptions {Object} Standard Leaflet L.Marker configuration options. Optional.
* @return {Object} Leaflet L.Marker object
*/
createMarker: function (lat, lng, popup, markerOptions) {
var marker = L.marker([lat, lng], markerOptions);
if (popup) {
marker.bindPopup(popup);
}
return marker;
},
/**
* Calculate the area of a given GeoJSON object in square kilometers. The GeoJSON object can be a FeatureCollection or a Feature.
*
* For circle geometries, the Properties object must contain an attribute called Radius, with the radius in meters.
* For features with a 'pid' property, the Properties object must contain an attribute called area_km with the area in square kilometers.
* @param geoJson {Object} GeoJSON object to calculate the area for
* @returns {number} The calculated area in square kilometers
*/
calculateAreaKmSq: function (geoJson) {
// uses Turf from MapBox. The turf.area function returns the area in square meters.
var areaSqKm = turf.area(geoJson) / 1000000;
// Turf (and GeoJSON) doesn't support circles, so check if there are any and add them to the total
// This will work as long as the radius has been included in the properties object of the feature
if (geoJson.type == "FeatureCollection") {
geoJson.features.forEach(function (feature) {
if (feature.properties.radius) {
areaSqKm += ((3.14 * feature.properties.radius * feature.properties.radius) / 1000) / 1000;
}
else if (feature.properties && feature.properties.pid && feature.properties.area_km) {
areaSqKm += feature.properties.area_km;
}
});
} else if (geoJson.type == "Feature") {
if (geoJson.properties.radius) {
areaSqKm += ((3.14 * geoJson.properties.radius * geoJson.properties.radius) / 1000) / 1000;
}
else if (geoJson.properties && geoJson.properties.pid && geoJson.properties.area_km) {
areaSqKm += geoJson.properties.area_km;
}
}
return areaSqKm;
}
};
Source