function CanvasMap(ctx, width, height, margin, cfg) { 'use strict'; var maxScale = 3; var self; var focused; var focusedFeature; var cache = {}; var cachePath = false; var cachePathLines = []; var translate; var scale; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = cfg.colors.bg; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); var referenceSize = Math.sqrt(height * height + width * width) * cfg.durationScale; var clickMapCanvas = document.createElement('canvas'); clickMapCanvas.width = width; clickMapCanvas.height = height; var clickCtx = clickMapCanvas.getContext('2d', {alpha: false}); var clip = d3.geo.clipExtent(); 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 >= simplifiedProjection.area) { this.stream.point(x * width | 0, y * width | 0); } } }); var simplifiedProjection = { stream: function (s) { return simplify.stream(clip.stream(s)); }, baseArea: 4e-3 / width }; simplifiedProjection.area = simplifiedProjection.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() .context(!cachePath ? ctx : null) .projection(simplifiedProjection); var pathRaw = d3.geo.path() .context(/*!cachePathLines ? */ctx/* : null*/) .projection(projection); var pathLines = d3.geo.path() .context(!cachePathLines ? ctx : null) .projection(projectionLines); var zoom = d3.behavior.zoom() .scaleExtent([1, maxScale]) .on('zoom', zoomed); var graticuleDatum = d3.geo.graticule()(); var h = d3.geo.hexakisIcosahedron; var coolLinesData = h.icosahedronEdges(); var hotLinesData = h.hexakisCenterEdges(); var balancedLinesData = h.hexakisSideEdges(); 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); d3.select(ctx.canvas).call(zoom).on('mouseup', click); function draw() { var t = zoom.translate(); var s = zoom.scale(); ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); // Clip ctx.beginPath(); ctx.moveTo(cfg.frameLineWidth, cfg.frameLineWidth); ctx.lineTo(cfg.frameLineWidth, height - cfg.frameLineWidth); ctx.lineTo(width - cfg.globeRadius, height - cfg.frameLineWidth); ctx.arcTo(width - cfg.globeRadius, height - cfg.globeRadius, width - cfg.frameLineWidth, height - cfg.globeRadius, cfg.globeRadius); ctx.lineTo(width - cfg.frameLineWidth, height - cfg.globeRadius); ctx.lineTo(width - cfg.frameLineWidth, cfg.frameLineWidth); ctx.lineTo(cfg.frameLineWidth, cfg.frameLineWidth); ctx.clip(); // Water if (!cfg.filter.map.off) { ctx.fillStyle = cfg.colors.water; ctx.fillRect(cfg.frameLineWidth, cfg.frameLineWidth, width - cfg.frameLineWidth * 2, height - cfg.frameLineWidth * 2); } else { ctx.fillStyle = cfg.colors.bg; ctx.fillRect(cfg.frameLineWidth, cfg.frameLineWidth, width - cfg.frameLineWidth * 2, height - cfg.frameLineWidth * 2); } // Transform ctx.translate(Math.round(s * width / 2 + t[0]), Math.round(s * height / 2 + t[1])); ctx.scale(s, s); // Clip var c = [-t[0] / s - (s - 1) * (width / s) / 2, -t[1] / s - (s - 1) * (height / s) / 2]; var ce = [[-width / 2 / s + c[0], -height / 2 / s + c[1]], [width / 2 / s + c[0], height / 2 / s + c[1]]]; clip.extent(ce); if (!cachePathLines) { projectionLines.clipExtent(ce); } projection.clipExtent(ce); ctx.lineWidth = 0.5 / s; // Graticule if (!cfg.filter.graticule.off) { ctx.beginPath(); ctx.setLineDash([cfg.dash[0] / s, cfg.dash[1] / s]); ctx.strokeStyle = cfg.colors.graticule; if (false && cachePathLines) { ctx.stroke(cachePathLines[3] = cachePathLines[3] || new Path2D(pathRaw(graticuleDatum))); } else { pathRaw(graticuleDatum); ctx.stroke(); } ctx.setLineDash([]); } // Land if (!cachePath && !cfg.filter.map.off && self.landDatum) { ctx.beginPath(); ctx.fillStyle = cfg.colors.land; path(self.landDatum); ctx.fill(); } else if (cachePath && !cfg.filter.map.off && self.landDatum) { ctx.beginPath(); ctx.fillStyle = cfg.colors.land; path(self.landDatum); ctx.fill(cachePath[10 + Math.round(s * 10)] = cachePath[10 + Math.round(s * 2)] || new Path2D(path(self.landDatum))); } // Borders if (!cachePath && !cfg.filter.map.off && self.countriesDatum) { ctx.beginPath(); path(self.countriesDatum); ctx.strokeStyle = cfg.colors.border; ctx.stroke(); } ctx.lineWidth = 1 / s; // Lines if (!cfg.filter['cool-line'].off) { ctx.beginPath(); ctx.strokeStyle = cfg.colors.cool; if (cachePathLines) { ctx.stroke(cachePathLines[0] = cachePathLines[0] || new Path2D(pathLines(coolLinesData))); } else { pathLines(coolLinesData); ctx.stroke(); } } if (!cfg.filter['hot-line'].off) { ctx.beginPath(); ctx.strokeStyle = cfg.colors.hot; if (cachePathLines) { ctx.stroke(cachePathLines[1] = cachePathLines[1] || new Path2D(pathLines(hotLinesData))); } else { pathLines(hotLinesData); ctx.stroke(); } } if (!cfg.filter['balanced-line'].off) { ctx.beginPath(); ctx.strokeStyle = cfg.colors.balanced; if (cachePathLines) { ctx.stroke(cachePathLines[2] = cachePathLines[2] || new Path2D(pathLines(balancedLinesData))); } else { pathLines(balancedLinesData); ctx.stroke(); } } // Points if (!cfg.filter['cool-point'].off) { coolPointsData.forEach(function (d) { drawPoint(d, ctx, projectionLines, cfg.colors.cool, 10 / s); }); } if (!cfg.filter['hot-point'].off) { hotPointsData.forEach(function (d) { drawPoint(d, ctx, projectionLines, cfg.colors.hot, 10 / s); }); } if (!cfg.filter['balanced-point'].off) { balancedPointsData.forEach(function (d) { drawPoint(d, ctx, projectionLines, cfg.colors.balanced, 10 / s); }); } // Symbols if (self.geo) { ctx.lineWidth = cfg.symbolLine / s; ctx.strokeStyle = cfg.colors.shape; self.geo.features.forEach(function (d) { drawSymbol(d, s, ctx, cfg.shapes[d.properties.description]); }); } if (focused) { ctx.beginPath(); ctx.arc(focused[0], focused[1], 25 / s, 0, 2 * Math.PI); ctx.strokeStyle = cfg.colors.focus; ctx.fillStyle = cfg.colors.selection; ctx.lineWidth = 6 / s; ctx.fill(); ctx.stroke(); if (focusedFeature && focusedFeature.type === 'Feature') { ctx.lineWidth = cfg.symbolLine / s; ctx.strokeStyle = cfg.colors.shape; drawSymbol(focusedFeature, s, ctx, cfg.shapes[focusedFeature.properties.description]); } var n = translateToCenter(t, s); var l = [[n[0] + (-width / 2 + margin + self.size.width / 2) / s, n[1] + (-height / 2 + margin + self.size.height / 2) / s], [focused[0], focused[1]]]; 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 - 25 / s) / 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]); ctx.beginPath(); ctx.moveTo(l[0][0], l[0][1]); ctx.lineTo(l[1][0], l[1][1]); ctx.lineWidth = 3 / s; ctx.strokeStyle = cfg.colors.frame; ctx.stroke(); } ctx.restore(); // Frame ctx.beginPath(); ctx.moveTo(width - cfg.frameLineWidth / 2, height - cfg.globeRadius); ctx.lineTo(width - cfg.frameLineWidth / 2, cfg.frameLineWidth / 2); ctx.lineTo(cfg.frameLineWidth / 2, cfg.frameLineWidth / 2); ctx.lineTo(cfg.frameLineWidth / 2, height - cfg.frameLineWidth / 2); ctx.lineTo(width - cfg.globeRadius, height - cfg.frameLineWidth / 2); ctx.strokeStyle = cfg.colors.frame; ctx.lineWidth = cfg.frameLineWidth; ctx.stroke(); } function drawSymbol(d, s, ctx, color) { if (cfg.filter[d.properties.description].off) { return; } var t = pathRaw.centroid(d); if (isNaN(t[0]) || isNaN(t[1])) { return; } var symbolSize = cfg.sizes[d.properties.description] / s; var cp; if (cache[cfg.symbols[d.properties.description] + symbolSize]) { cp = cache[cfg.symbols[d.properties.description] + symbolSize] } else { var p = d3.svg.symbol().size(symbolSize).type(cfg.symbols[d.properties.description])(); cp = cache[cfg.symbols[d.properties.description] + symbolSize] = new Path2D(p); } ctx.save(); ctx.translate(t[0], t[1]); ctx.stroke(cp); ctx.fillStyle = color; ctx.fill(cp); ctx.restore(); } function drawPoint(d, ctx, projection, color, s) { var p = projection(d.coordinates); if (Math.abs(p[0]) !== Infinity && Math.abs(p[1]) !== Infinity) { ctx.beginPath(); ctx.arc(p[0], p[1], s, 0, 2 * Math.PI); ctx.strokeStyle = color; ctx.stroke(); } } function numberToColor(n) { var b = 0xff & n; n >>= 8; var g = 0xff & n; n >>= 8; return 'rgb(' + n + ',' + g + ',' + b + ')'; } function pixelToNumber(p) { return (p[0] << 16) + (p[1] << 8) + p[2]; } function click() { var m = d3.mouse(this); var t = zoom.translate(); var s = zoom.scale(); var n = 1; var map = {}; var c; clickCtx.setTransform(1, 0, 0, 1, 0, 0); clickCtx.clearRect(0, 0, width, height); clickCtx.translate(s * width / 2 + t[0], s * height / 2 + t[1]); clickCtx.scale(s, s); if (!cfg.filter['cool-point'].off) { coolPointsData.forEach(function (d) { map[n] = d; c = numberToColor(n++); drawPoint(d, clickCtx, projectionLines, c, 10 / s); clickCtx.fillStyle = c; clickCtx.fill(); }); } if (!cfg.filter['hot-point'].off) { hotPointsData.forEach(function (d) { map[n] = d; c = numberToColor(n++); drawPoint(d, clickCtx, projectionLines, c, 10 / s); clickCtx.fillStyle = c; clickCtx.fill(); }); } if (!cfg.filter['balanced-point'].off) { balancedPointsData.forEach(function (d) { map[n] = d; c = numberToColor(n++); drawPoint(d, clickCtx, projectionLines, c, 10 / s); clickCtx.fillStyle = c; clickCtx.fill(); }); } if (self.geo) { clickCtx.lineWidth = cfg.symbolLine / s; self.geo.features.forEach(function (d) { map[n] = d; c = numberToColor(n++); clickCtx.strokeStyle = c; drawSymbol(d, s, clickCtx, c); }); if (focusedFeature && focusedFeature.type === 'Feature') { map[n] = focusedFeature; c = numberToColor(n++); clickCtx.strokeStyle = c; drawSymbol(focusedFeature, s, clickCtx, c); } } var p = clickCtx.getImageData(m[0], m[1], 1, 1).data; var b = pixelToNumber(p); if (map[b] && self.clickedPoint) { self.clickedPoint(map[b]); } } function getFixedZoom(t, s) { var S = (width - height) / 2; t[0] = Math.min(0, Math.max(-width * (s - 1), t[0])); t[1] = Math.min(+S * s, Math.max(-S * s - height * (s - 1), t[1])); } function fixZoom() { var t = zoom.translate(); var s = zoom.scale(); getFixedZoom(t, s); zoom.translate(t); } function zoomed() { if (d3.event && d3.event.sourceEvent) { d3.event.sourceEvent.stopPropagation(); d3.event.sourceEvent.preventDefault(); } fixZoom(); var t = zoom.translate(); var s = zoom.scale(); if ((s === scale) && translate && (t[0] === translate[0]) && (t[1] === translate[1])) { return; } translate = t; scale = s; var c = translateToCenter(t, s); var i = projection.invert([-c[0], -c[1]]); if (cfg.mraf) { requestAnimationFrame(function () { update(); if (self.onZoomed) { self.onZoomed(t, s, i); } }); } else { update(); if (self.onZoomed) { self.onZoomed(t, s, i); } } } function update(f, running) { var s = (running === true) ? 1 : zoom.scale(); projectionLines.precision(cachePathLines ? Math.sqrt(1 / 2) / maxScale / maxScale : Math.sqrt(1 / 2) / s / s); simplifiedProjection.area = simplifiedProjection.baseArea / s / s; draw(); if (self.drawGlobe) { self.drawGlobe(running); } } function setZoom(t, s, i) { var p = projection(i); zoom.scale(s); t = centerToTranslate([-p[0], -p[1]], s); zoom.translate(t); fixZoom(); update(); } function translateToCenter(t, s) { return [-t[0] / s - (s - 1) * (width / s) / 2, -t[1] / s - (s - 1) * (height / s) / 2]; } function centerToTranslate(c, s) { return [-(c[0] + (s - 1) * (width / s) / 2) * s, -(c[1] + (s - 1) * (height / s) / 2) * s]; } function zoomTransition(p, toScale) { var fromScale = zoom.scale(); var fromTranslate = zoom.translate(); var toTranslate = toScale === 1 ? p : [-p[0] * toScale - width, -p[1] * toScale - height]; var from = translateToCenter(fromTranslate, fromScale); from[2] = referenceSize / fromScale; getFixedZoom(toTranslate, toScale); 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 translate = centerToTranslate(z, s); getFixedZoom(translate, s); zoom.translate(translate); zoom.scale(s); update(t === 1 ? 1 : 0, t < 1); if (self.onZoomed) { var c = translateToCenter(translate, s); var i = projection.invert([-c[0], -c[1]]); self.onZoomed(translate, s, i); } }; } } function zoomTo(d, infoSize) { self.size = infoSize; if (!d) { focusedFeature = null; focused = null; update(); return; } var c = d.geometry ? [d.geometry.coordinates[0], d.geometry.coordinates[1]] : [d[0] + cfg.shift, d[1]]; var p = projection(c); focusedFeature = d; focused = p; zoomTransition(p, maxScale); } function reset() { zoomTransition(projection([0, 0]), 1); } return self = { ctx: ctx, path: path, pathRaw: pathRaw, projection: projection, update: update, setZoom: setZoom, zoomTo: zoomTo, reset: reset }; }