/* * * -*- coding: utf-8 -*- * * warden-map.js * * Copyright (C) 2016 Cesnet z.s.p.o * Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. * */ // NOTE: Change path in a function d3.json() if you separate backend and frontend! // Zooming functionality is based on WunderBart's implementation // Please see following links: // https://github.com/wunderbart // https://jsfiddle.net/wunderbart/Lom3b0gb/ function Zoom(args) { $.extend(this, { $buttons: $(".zoom-button"), $info: $("#zoom-info"), scale: { max: 50, currentShift: 0 }, $container: args.$container, datamap: args.datamap }); this.init(); } Zoom.prototype.init = function() { var paths = this.datamap.svg.selectAll("path"), subunits = this.datamap.svg.selectAll(".datamaps-subunit"); // preserve stroke thickness paths.style("vector-effect", "non-scaling-stroke"); // disable click on drag end subunits.call( d3.behavior.drag().on("dragend", function() { d3.event.sourceEvent.stopPropagation(); }) ); this.scale.set = this._getScalesArray(); this.d3Zoom = d3.behavior.zoom().scaleExtent([ 1, this.scale.max ]); this._displayPercentage(1); this.listen(); }; Zoom.prototype.listen = function() { this.$buttons.off("click").on("click", this._handleClick.bind(this)); this.datamap.svg .call(this.d3Zoom.on("zoom", this._handleScroll.bind(this))) .on("dblclick.zoom", null); // disable zoom on double-click }; Zoom.prototype.reset = function() { this._shift("reset"); }; Zoom.prototype._handleScroll = function() { var translate = d3.event.translate, scale = d3.event.scale, limited = this._bound(translate, scale); this.scrolled = true; this._update(limited.translate, limited.scale); }; Zoom.prototype._handleClick = function(event) { var direction = $(event.target).data("zoom"); this._shift(direction); }; Zoom.prototype._shift = function(direction) { var center = [ this.$container.width() / 2, this.$container.height() / 2 ], translate = this.d3Zoom.translate(), translate0 = [], l = [], view = { x: translate[0], y: translate[1], k: this.d3Zoom.scale() }, bounded; translate0 = [ (center[0] - view.x) / view.k, (center[1] - view.y) / view.k ]; if (direction == "reset") { view.k = 1; this.scrolled = true; } else { view.k = this._getNextScale(direction); } l = [ translate0[0] * view.k + view.x, translate0[1] * view.k + view.y ]; view.x += center[0] - l[0]; view.y += center[1] - l[1]; bounded = this._bound([ view.x, view.y ], view.k); this._animate(bounded.translate, bounded.scale); }; Zoom.prototype._bound = function(translate, scale) { var width = this.$container.width(), height = this.$container.height(); translate[0] = Math.min( (width / height) * (scale - 1), Math.max( width * (1 - scale), translate[0] ) ); translate[1] = Math.min(0, Math.max(height * (1 - scale), translate[1])); return { translate: translate, scale: scale }; }; Zoom.prototype._update = function(translate, scale) { this.d3Zoom .translate(translate) .scale(scale); this.datamap.svg.selectAll("g") .attr("transform", "translate(" + translate + ")scale(" + scale + ")"); this._displayPercentage(scale); }; Zoom.prototype._animate = function(translate, scale) { var _this = this, d3Zoom = this.d3Zoom; d3.transition().duration(350).tween("zoom", function() { var iTranslate = d3.interpolate(d3Zoom.translate(), translate), iScale = d3.interpolate(d3Zoom.scale(), scale); return function(t) { _this._update(iTranslate(t), iScale(t)); }; }); }; Zoom.prototype._displayPercentage = function(scale) { var value; value = Math.round(Math.log(scale) / Math.log(this.scale.max) * 100); this.$info.text(value + "%"); }; Zoom.prototype._getScalesArray = function() { var array = [], scaleMaxLog = Math.log(this.scale.max); for (var i = 0; i <= 10; i++) { array.push(Math.pow(Math.E, 0.1 * i * scaleMaxLog)); } return array; }; Zoom.prototype._getNextScale = function(direction) { var scaleSet = this.scale.set, currentScale = this.d3Zoom.scale(), lastShift = scaleSet.length - 1, shift, temp = []; if (this.scrolled) { for (shift = 0; shift <= lastShift; shift++) { temp.push(Math.abs(scaleSet[shift] - currentScale)); } shift = temp.indexOf(Math.min.apply(null, temp)); if (currentScale >= scaleSet[shift] && shift < lastShift) { shift++; } if (direction == "out" && shift > 0) { shift--; } this.scrolled = false; } else { shift = this.scale.currentShift; if (direction == "out") { shift > 0 && shift--; } else { shift < lastShift && shift++; } } this.scale.currentShift = shift; return scaleSet[shift]; }; // Configuration of datamap canvas // Futher reading can be found at https://datamaps.github.io/ function Datamap() { this.$container = $("#container"); this.instance = new Datamaps({ scope: 'world', element: this.$container.get(0), done: this._handleMapReady.bind(this), projection: 'mercator', fills: { defaultFill: '#dcdcda' }, geographyConfig: { borderColor: '#fdfdfd', highlightFillColor: '#4b4d4a', highlightBorderColor: '#fdfdfd', popupOnHover: true, popupTemplate: function(geography, data) { return '<div class="hoverinfo" id="country">' + geography.properties.name + '</div>'; }, }, arcConfig: { strokeColor: '#0062a2', strokeWidth: 1, arcSharpness: 5, animationSpeed: 4000, // Milliseconds popupOnHover: true, // Case with latitude and longitude popupTemplate: function(geography, data) { if ( ( data.origin && data.destination ) && data.origin.latitude && data.origin.longitude && data.destination.latitude && data.destination.longitude ) { // Content of info table str = '<div class="hoverinfo"><table id="event"><tr><th>Warden Event</th></tr><tr><td>Type</td><td>'+ JSON.stringify(data.event) +'</td></tr><tr><td>Detect Time</td><td>'+ JSON.stringify(data.time) +'</td></tr><tr><th>Event origin</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.origin.ip) + '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.origin.city) + ', ' + JSON.stringify(data.origin.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.origin.latitude) + ', ' + JSON.stringify(data.origin.longitude) + '</td></tr><tr><th>Event Destination</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.destination.ip) + '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.destination.city) + ', ' + JSON.stringify(data.destination.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.destination.latitude) + ', ' + JSON.stringify(data.destination.longitude) + '</td></tr></table></div>'; return str.replace(/"/g,""); } // Missing information else { return ''; } } } }); var instance = this.instance; // Link to a json file with information for drawing. // NOTE: Change path if you separate backend and fronend! d3.json("./warden-map.json", function(error, data) { instance.arc(data); }); }; Datamap.prototype._handleMapReady = function(datamap) { this.zoom = new Zoom({ $container: this.$container, datamap: datamap }); }