Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Map: loop slider and disable zoom on click
Browse files Browse the repository at this point in the history
Norric1Admin committed Mar 1, 2019
1 parent 4d2a9f1 commit c8a8a2b
Showing 2 changed files with 462 additions and 0 deletions.
407 changes: 407 additions & 0 deletions _includes/assets/js/plugins/jquery.sdgMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,407 @@
/**
* TODO:
* Integrate with high-contrast switcher.
*/
(function($, L, chroma, window, document, undefined) {

// Create the defaults once
var defaults = {

// Options for using tile imagery with leaflet.
tileURL: '[replace me]',
tileOptions: {
id: '[relace me]',
accessToken: '[replace me]',
attribution: '[replace me]',
},
// Zoom limits.
minZoom: 5,
maxZoom: 10,
// Visual/choropleth considerations.
colorRange: chroma.brewer.BuGn,
noValueColor: '#f0f0f0',
styleNormal: {
weight: 1,
opacity: 1,
color: '#888',
fillOpacity: 0.7
},
styleHighlighted: {
weight: 1,
opacity: 1,
color: '#111',
fillOpacity: 0.7
},
styleStatic: {
weight: 2,
opacity: 1,
fillOpacity: 0,
color: '#172d44',
dashArray: '5,5',
},
};

// Defaults for each map layer.
var mapLayerDefaults = {
min_zoom: 0,
max_zoom: 10,
serviceUrl: '[replace me]',
nameProperty: '[replace me]',
idProperty: '[replace me]',
staticBorders: false,
};

function Plugin(element, options) {

this.element = element;
this.options = $.extend(true, {}, defaults, options.mapOptions);
this.mapLayers = [];
this.geoData = options.geoData;
this.geoCodeRegEx = options.geoCodeRegEx;

// Require at least one geoLayer.
if (!options.mapLayers.length) {
console.log('Map disabled, no mapLayers in options.');
return;
}

// Apply geoLayer defaults.
for (var i = 0; i < options.mapLayers.length; i++) {
this.mapLayers[i] = $.extend(true, {}, mapLayerDefaults, options.mapLayers[i]);
}

this._defaults = defaults;
this._name = 'sdgMap';

this.valueRange = [_.min(_.pluck(this.geoData, 'Value')), _.max(_.pluck(this.geoData, 'Value'))];
this.colorScale = chroma.scale(this.options.colorRange)
.domain(this.valueRange)
.classes(this.options.colorRange.length);

this.years = _.uniq(_.pluck(this.geoData, 'Year'));
this.currentYear = this.years[0];

this.init();
}

Plugin.prototype = {

// Add time series to GeoJSON data and normalize the name and geocode.
prepareGeoJson: function(geoJson, idProperty, nameProperty) {
var geoData = this.geoData;
geoJson.features.forEach(function(feature) {
var geocode = feature.properties[idProperty];
var name = feature.properties[nameProperty];
// First add the time series data.
var records = _.where(geoData, { GeoCode: geocode });
records.forEach(function(record) {
// Add the Year data into the properties.
feature.properties[record.Year] = record.Value;
});
// Next normalize the geocode and name.
feature.properties.name = name;
feature.properties.geocode = geocode;
delete feature.properties[idProperty];
delete feature.properties[nameProperty];
});
return geoJson;
},

// Zoom to a feature.
zoomToFeature: function(layer) {
this.map.fitBounds(layer.getBounds());
},

// Select a feature.
highlightFeature: function(layer) {
// Abort if the layer is not on the map.
if (!this.map.hasLayer(layer)) {
return;
}
// Update the style.
layer.setStyle(this.options.styleHighlighted);
// Add a tooltip if not already there.
if (!layer.getTooltip()) {
var tooltipContent = layer.feature.properties.name;
var tooltipData = this.getData(layer.feature.properties);
if (tooltipData) {
tooltipContent += ': ' + tooltipData;
}
layer.bindTooltip(tooltipContent, {
permanent: true,
}).addTo(this.map);
}
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
layer.bringToFront();
}
this.updateStaticLayers();
},

// Unselect a feature.
unhighlightFeature: function(layer) {

// Reset the feature's style.
layer.setStyle(this.options.styleNormal);

// Remove the tooltip if necessary.
if (layer.getTooltip()) {
layer.unbindTooltip();
}

// Make sure other selections are still highlighted.
var plugin = this;
this.selectionLegend.selections.forEach(function(selection) {
plugin.highlightFeature(selection);
});
},

// Get all of the GeoJSON layers.
getAllLayers: function() {
return L.featureGroup(this.dynamicLayers.layers);
},

// Get only the visible GeoJSON layers.
getVisibleLayers: function() {
// Unfortunately relies on an internal of the ZoomShowHide library.
return this.dynamicLayers._layerGroup;
},

updateStaticLayers: function() {
// Make sure the static borders are always visible.
this.staticLayers._layerGroup.eachLayer(function(layer) {
layer.bringToFront();
});
},

// Update the colors of the Features on the map.
updateColors: function() {
var plugin = this;
this.getAllLayers().eachLayer(function(layer) {
layer.setStyle(function(feature) {
return {
fillColor: plugin.getColor(feature.properties),
}
});
});
},

// Get the data from a feature's properties, according to the current year.
getData: function(props) {
if (props[this.currentYear]) {
return props[this.currentYear];
}
return false;
},

// Choose a color for a GeoJSON feature.
getColor: function(props) {
var data = this.getData(props);
if (data) {
return this.colorScale(data).hex();
}
else {
return this.options.noValueColor;
}
},

// Initialize the map itself.
init: function() {

// Create the map.
this.map = L.map(this.element, {
minZoom: this.options.minZoom,
maxZoom: this.options.maxZoom,
zoomControl: false,
});
this.map.setView([0, 0], 0);
this.dynamicLayers = new ZoomShowHide();
this.dynamicLayers.addTo(this.map);
this.staticLayers = new ZoomShowHide();
this.staticLayers.addTo(this.map);

// Add zoom control.
this.map.addControl(L.Control.zoomHome());

// Add full-screen functionality.
this.map.addControl(new L.Control.Fullscreen());

// Add scale.
this.map.addControl(L.control.scale({position: 'bottomright'}));

// Add tile imagery.
L.tileLayer(this.options.tileURL, this.options.tileOptions).addTo(this.map);

// Because after this point, "this" rarely works.
var plugin = this;

// Add the year slider.
this.map.addControl(L.Control.yearSlider({
yearStart: this.years[0],
yearEnd: this.years[this.years.length - 1],
yearChangeCallback: function(e) {
plugin.currentYear = new Date(e.time).getFullYear();
plugin.updateColors();
plugin.selectionLegend.update();
}
}));

// Add the selection legend.
this.selectionLegend = L.Control.selectionLegend(plugin);
this.map.addControl(this.selectionLegend);

// Add the download button.
this.map.addControl(L.Control.downloadGeoJson(plugin));

// At this point we need to load the GeoJSON layer/s.
var geoURLs = this.mapLayers.map(function(item) {
return $.getJSON(item.serviceUrl);
});
$.when.apply($, geoURLs).done(function() {

var geoJsons = arguments;
for (var i in geoJsons) {
// First add the geoJson as static (non-interactive) borders.
if (plugin.mapLayers[i].staticBorders) {
var staticLayer = L.geoJson(geoJsons[i][0], {
style: plugin.options.styleStatic,
interactive: false,
});
// Static layers should start appear when zooming past their dynamic
// layer, and stay visible after that.
staticLayer.min_zoom = plugin.mapLayers[i].max_zoom + 1;
staticLayer.max_zoom = plugin.options.maxZoom;
plugin.staticLayers.addLayer(staticLayer);
}
// Now go on to add the geoJson again as choropleth dynamic regions.
var idProperty = plugin.mapLayers[i].idProperty;
var nameProperty = plugin.mapLayers[i].nameProperty;
var geoJson = plugin.prepareGeoJson(geoJsons[i][0], idProperty, nameProperty);

var layer = L.geoJson(geoJson, {
style: plugin.options.styleNormal,
onEachFeature: onEachFeature,
});
// Set the "boundaries" for when this layer should be zoomed out of.
layer.min_zoom = plugin.mapLayers[i].min_zoom;
layer.max_zoom = plugin.mapLayers[i].max_zoom;
// Listen for when this layer gets zoomed in or out of.
layer.on('remove', zoomOutHandler);
layer.on('add', zoomInHandler);
// Save the GeoJSON object for direct access (download) later.
layer.geoJsonObject = geoJson;
// Add the layer to the ZoomShowHide group.
plugin.dynamicLayers.addLayer(layer);
}
plugin.updateColors();

// Now that we have layers, we can add the search feature.
plugin.searchControl = new L.Control.Search({
layer: plugin.getAllLayers(),
propertyName: 'name',
marker: false,
moveToLocation: function(latlng) {
plugin.zoomToFeature(latlng.layer);
if (!plugin.selectionLegend.isSelected(latlng.layer)) {
plugin.highlightFeature(latlng.layer);
plugin.selectionLegend.addSelection(latlng.layer);
}
},
autoCollapse: true,
});
plugin.map.addControl(plugin.searchControl);
// The search plugin messes up zoomShowHide, so we have to reset that
// with this hacky method. Is there a better way?
var zoom = plugin.map.getZoom();
plugin.map.setZoom(plugin.options.maxZoom);
plugin.map.setZoom(zoom);

// The list of handlers to apply to each feature on a GeoJson layer.
function onEachFeature(feature, layer) {
layer.on('click', clickHandler);
layer.on('mouseover', mouseoverHandler);
layer.on('mouseout', mouseoutHandler);
}
// Event handler for click/touch.
function clickHandler(e) {
var layer = e.target;
if (plugin.selectionLegend.isSelected(layer)) {
plugin.selectionLegend.removeSelection(layer);
plugin.unhighlightFeature(layer);
}
else {
plugin.selectionLegend.addSelection(layer);
plugin.highlightFeature(layer);
#plugin.zoomToFeature(layer);
}
}
// Event handler for mouseover.
function mouseoverHandler(e) {
var layer = e.target;
if (!plugin.selectionLegend.isSelected(layer)) {
plugin.highlightFeature(layer);
}
}
// Event handler for mouseout.
function mouseoutHandler(e) {
var layer = e.target;
if (!plugin.selectionLegend.isSelected(layer)) {
plugin.unhighlightFeature(layer);
}
}
// Event handler for when a geoJson layer is zoomed out of.
function zoomOutHandler(e) {
var geoJsonLayer = e.target;
// For desktop, we have to make sure that no features remain
// highlighted, as they might have been highlighted on mouseover.
geoJsonLayer.eachLayer(function(layer) {
if (!plugin.selectionLegend.isSelected(layer)) {
plugin.unhighlightFeature(layer);
}
});
plugin.updateStaticLayers();
}
// Event handler for when a geoJson layer is zoomed into.
function zoomInHandler(e) {
plugin.updateStaticLayers();
}
});

// Perform some last-minute tasks when the user clicks on the "Map" tab.
$('.map .nav-link').click(function() {
setTimeout(function() {
$('#map #loader-container').hide();
// Leaflet needs "invalidateSize()" if it was originally rendered in a
// hidden element. So we need to do that when the tab is clicked.
plugin.map.invalidateSize();
// Also zoom in/out as needed.
plugin.map.fitBounds(plugin.getVisibleLayers().getBounds());
// Limit the panning to what we care about.
plugin.map.setMaxBounds(plugin.getVisibleLayers().getBounds());
// Make sure the info pane is not too wide for the map.
var $legendPane = $('.selection-legend.leaflet-control');
var widthPadding = 20;
var maxWidth = $('#map').width() - widthPadding;
if ($legendPane.width() > maxWidth) {
$legendPane.width(maxWidth);
}
// Make sure the map is not too high.
var heightPadding = 75;
var maxHeight = $(window).height() - heightPadding;
if ($('#map').height() > maxHeight) {
$('#map').height(maxHeight);
}
}, 500);
});
},
};

// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn['sdgMap'] = function(options) {
return this.each(function() {
if (!$.data(this, 'plugin_sdgMap')) {
$.data(this, 'plugin_sdgMap', new Plugin(this, options));
}
});
};
})(jQuery, L, chroma, window, document);
55 changes: 55 additions & 0 deletions _includes/assets/js/plugins/leaflet.yearSlider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Leaflet year Slider.
*
* This is merely a specific configuration of Leaflet of L.TimeDimension.
* See here: https://github.com/socib/Leaflet.TimeDimension
*/
(function () {
"use strict";

var defaultOptions = {
// YearSlider options.
yearChangeCallback: null,
yearStart: 2000,
yearEnd: 2018,
// TimeDimensionControl options.
timeSliderDragUpdate: true,
speedSlider: false,
position: 'bottomleft',
// Player options.
playerOptions: {
transitionTime: 1000,
loop: true,
startOver: true
},
};

L.Control.YearSlider = L.Control.TimeDimension.extend({

// Hijack the displayed date format.
_getDisplayDateFormat: function(date){
return date.getFullYear();
}

});

// Helper function to compose the full widget.
L.Control.yearSlider = function(options) {
// Extend the defaults.
options = L.Util.extend(defaultOptions, options);
// Hardcode the timeDimension to year intervals.
options.timeDimension = new L.TimeDimension({
period: 'P1Y',
timeInterval: options.yearStart + '-01-02/' + options.yearEnd + '-01-02',
currentTime: new Date(options.yearStart + '-01-02').getTime(),
});
// Create the player.
options.player = new L.TimeDimension.Player(options.playerOptions, options.timeDimension);
// Listen for time changes.
if (typeof options.yearChangeCallback === 'function') {
options.timeDimension.on('timeload', options.yearChangeCallback);
};
// Return the control.
return new L.Control.YearSlider(options);
};
}());

1 comment on commit c8a8a2b

@Norric1Admin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidentally commented out with '#' instead of '//'
e53d68c

Please sign in to comment.