function SvgMap(svg, width, height, margin, cfg) { 'use strict'; var maxScale = 3; var self; var focused; var arrowStart; var prevScale; var referenceSize = Math.sqrt(height * height + width * width) * cfg.durationScale; var clip = d3.geo.clipExtent().extent([[-width / 2, -height / 2], [width / 2, height / 2]]); var projection = d3.geo.mercator() .translate([0, 0]) .precision(0) .scale(width / 2 / Math.PI); var simplify = d3.geo.transform({ point: function (x, y, z) { if (z >= projectionSimplified.area) { this.stream.point(~~(x * width), ~~(y * width)); } } }); var round = d3.geo.transform({ point: function (x, y) { this.stream.point(~~(x * 10) / 10, ~~(y * 10) / 10); } }); var projectionRounded = { stream: function (s) { return projection.stream(round.stream(s)); }, baseArea: 4e-3 / width }; var projectionLinesRounded = { stream: function (s) { return projectionLines.stream(round.stream(s)); }, baseArea: 4e-3 / width }; var projectionSimplified = { stream: function (s) { return simplify.stream(clip.stream(s)); }, baseArea: 4e-3 / width }; projectionSimplified.area = projectionSimplified.baseArea; var projectionLines = d3.geo.mercator() .rotate([cfg.shift, 0, 0]) .translate([0, 0]) .precision(1) .scale(width / 2 / Math.PI); var path = d3.geo.path() .projection(projectionSimplified); var pathRaw = d3.geo.path() .projection(projection); var pathRawRounded = d3.geo.path() .projection(projectionRounded); var pathLines = d3.geo.path() .projection(projectionLines); var pathLinesRounded = d3.geo.path() .projection(projectionLinesRounded); var zoom = d3.behavior.zoom() .scaleExtent([1, maxScale]) .on('zoom', zoomed); var defs = svg.append('defs'); defs.append('clipPath') .attr('id', 'clip-map') .append('rect') .attr("x", -width / 2) .attr("y", -height / 2) .attr("width", width) .attr("height", height); defs.append('clipPath') .attr('id', 'clip-arrow') .append('rect') .attr("x", margin) .attr("y", margin) .attr("width", width) .attr("height", height); var clipGroup = svg.append("g") .attr("transform", "translate(" + [width / 2 + margin, height / 2 + margin] + ")") .style('clip-path', 'url(#clip-map)') .call(zoom); clipGroup.append("rect") .attr("class", "map water") .attr("x", -width / 2) .attr("y", -height / 2) .attr("width", width) .attr("height", height); clipGroup.append("rect") .attr("class", "overlay") .attr("x", -width / 2) .attr("y", -height / 2) .attr("width", width) .attr("height", height); var g = clipGroup.append('g'); var graticulePath = g.append('path') .datum(d3.geo.graticule()()) .attr('class', 'graticule') .style('display', 'none') .attr('d', pathRaw); var land = g.append('path').attr('class', 'map land'); var countries = g.append('path').attr('class', 'map country'); var h = d3.geo.hexakisIcosahedron; var coolLines = Utils.svgLines(g, pathLines, h.icosahedronEdges(), 'cool-line'); var hotLines = Utils.svgLines(g, pathLines, h.hexakisCenterEdges(), 'hot-line'); var balancedLines = Utils.svgLines(g, pathLines, h.hexakisSideEdges(), 'balanced-line'); var coolPointsData = Utils.pointsToFeatures(h.icosahedronPoints(), 'cool-point', 'Cool point', cfg.shift); var hotPointsData = Utils.pointsToFeatures(h.hexakisCenterPoints(), 'hot-point', 'Hot point', cfg.shift); var balancedPointsData = Utils.pointsToFeatures(h.hexakisCrossPoints(), 'balanced-point', 'Balanced point', cfg.shift); Utils.svgPoints(g, coolPointsData, 'cool-point', projectionLines, clickedPoint, 'none'); Utils.svgPoints(g, hotPointsData, 'hot-point', projectionLines, clickedPoint, 'none'); Utils.svgPoints(g, balancedPointsData, 'balanced-point', projectionLines, clickedPoint, 'none'); function clickedPoint(d) { self.clickedPoint(d); } var placesGroup = g.append('g'); var selectedCircleGroup = svg.append('g') .style('display', 'none') .style('clip-path', 'url(#clip-arrow)'); var selectedCircle = selectedCircleGroup.append('circle') .attr('cx', margin) .attr('cy', margin) .attr('r', 20) .attr('class', 'border'); var selectedArrow = selectedCircleGroup.append('line') .attr('x1', margin * 4) .attr('y1', margin * 4) .attr('class', 'border'); svg.append("rect") .attr("class", "border") .style('stroke-width', cfg.frameLineWidth) .attr("transform", "translate(" + [width / 2 + margin, height / 2 + margin] + ")") .attr("x", -width / 2 + cfg.frameLineWidth / 2) .attr("y", -height / 2 + cfg.frameLineWidth / 2) .attr("width", width - cfg.frameLineWidth) .attr("height", height - cfg.frameLineWidth); function getFixedZoom(t, s) { var S = (width - height) / 2; t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0])); t[1] = Math.min(height / 2 * (s - 1) + S * s, Math.max(height / 2 * (1 - s) - S * s, t[1])); } function fixZoom() { var t = zoom.translate(); var s = zoom.scale(); getFixedZoom(t, s); zoom.translate(t); } function roundTranslate(t) { t[0] = Math.round(t[0]); t[1] = Math.round(t[1]); } function zoomed() { fixZoom(); var t = zoom.translate(); var s = zoom.scale(); var i = projection.invert([t[0] / s, t[1] / s]); update(); if (self.onZoomed) { self.onZoomed(t, s, i); } } function update(running) { var t = zoom.translate(); var s = Math.min(maxScale, d3.round(zoom.scale(), 6)); roundTranslate(t); g.attr('transform', 'translate(' + t + ')scale(' + s + ')'); var ps = (running === true) ? 1 : zoom.scale(); var c = translateToCenter(t, s); var ce = [[ -width / 2 + c[0] - width / 2 / s, -height / 2 + c[1] - height / 2 / s], [ -width / 2 + c[0] + width / 2 / s, -height / 2 + c[1] + height / 2 / s]]; clip.extent(ce); projectionLines.precision(running ? 1 : Math.sqrt(1 / 2) / s / s); projectionSimplified.area = projectionSimplified.baseArea / ps / ps; projection.clipExtent(ce); projectionLines.clipExtent(ce); var localPathRaw, localPathLines; if (running) { localPathRaw = pathRawRounded; localPathLines = pathLinesRounded; } else { localPathRaw = pathRaw; localPathLines = pathLines; } if (!cfg.filter['graticule'].off) { graticulePath.attr('d', localPathRaw); } if (!cfg.filter['cool-line'].off) { coolLines.attr('d', localPathLines); } if (!cfg.filter['hot-line'].off) { hotLines.attr('d', localPathLines); } if (!cfg.filter['balanced-line'].off) { balancedLines.attr('d', localPathLines); } if (!cfg.filter.map.off) { land.attr('d', path); countries.attr('d', path); } if (prevScale !== s) { g.style('stroke-width', 1 / s); if (!cfg.filter['graticule'].off) { graticulePath.style('stroke-dasharray', (2 / s ) + ',' + (3 / s)); } //self.placesSelection.attr('transform', function (d) { // return d.translate + 'scale(' + 1 / s + ')'; //}); self.placesSelection.defs.attr('transform', function () { return 'scale(' + 1 / s + ')'; }); prevScale = s; } if (focused) { var f = [width / 2 + focused[0] * s + t[0], height / 2 + focused[1] * s + t[1]]; var l = [[arrowStart.width / 2 + 2 * margin, arrowStart.height / 2 + 2 * margin], [f[0] + margin, f[1] + margin]]; var d = Math.sqrt(Math.pow(l[1][0] - l[0][0], 2) + Math.pow(l[1][1] - l[0][1], 2)); var r = (d - 20) / d; l[1][0] = l[0][0] + r * (l[1][0] - l[0][0]); l[1][1] = l[0][1] + r * (l[1][1] - l[0][1]); selectedCircle .attr('transform', 'translate(' + [d3.round(f[0]), d3.round(f[1])] + ')'); selectedArrow .attr('x1', l[0][0]) .attr('y1', l[0][1]) .attr('x2', l[1][0]) .attr('y2', l[1][1]); selectedCircleGroup.style('display', null); } else { selectedCircleGroup.style('display', 'none'); } } function setZoom(t, s, i) { var p = projection(i); zoom.scale(s); zoom.translate([p[0] * s, p[1] * s]); fixZoom(); update(); } function translateToCenter(t, s) { return [-t[0] / s + width / 2, -t[1] / s + height / 2]; } function centerToTranslate(c, s) { return [s * (-c[0] + width / 2), s * (-c[1] + height / 2)]; } function zoomTransition(p, toScale) { var fromScale = zoom.scale(); var fromTranslate = zoom.translate(); var toTranslate = [-p[0] * toScale, -p[1] * toScale]; getFixedZoom(toTranslate, toScale); var from = translateToCenter(fromTranslate, fromScale); from[2] = referenceSize / fromScale; var to = translateToCenter(toTranslate, toScale); to[2] = referenceSize / toScale; var zi = d3.interpolateZoom(from, to); var dur = Math.min(cfg.durationMax, Math.max(cfg.durationMin, zi.duration * cfg.durationSpeed)); d3.transition().duration(dur).tween('tween', tween); function tween() { return function (t) { var z = zi(t); var s = referenceSize / z[2]; var tr = centerToTranslate(z, s); getFixedZoom(tr, s); zoom.translate(tr); zoom.scale(s); update(t < 1); if (self.onZoomed) { var i = projection.invert([tr[0] / s, tr[1] / s]); self.onZoomed(z, s, i, t < 1); } }; } } function zoomTo(d, ast) { if (!d) { focused = null; selectedCircleGroup.style('display', 'none'); return; } var c = d.geometry ? [d.geometry.coordinates[0], d.geometry.coordinates[1]] : [d[0] + cfg.shift, d[1]]; var p = projection(c); arrowStart = ast; focused = p; zoomTransition(p, maxScale) } function reset() { zoomTransition(projection([0, 0]), 1); } self = { root: svg, land: land, countries: countries, path: path, pathRaw: pathRaw, g: placesGroup, projection: projection, setZoom: setZoom, zoomTo: zoomTo, reset: reset, update: update }; return self; }