UNPKG

@amcharts/amcharts5

Version:
1,127 lines • 48.1 kB
import { MapChartDefaultTheme } from "./MapChartDefaultTheme"; import { SerialChart } from "../../core/render/SerialChart"; import { Rectangle } from "../../core/render/Rectangle"; import { geoPath, geoProjection, geoProjectionMutator } from "d3-geo"; import { Color } from "../../core/util/Color"; import { registry } from "../../core/Registry"; import * as $math from "../../core/util/Math"; import * as $array from "../../core/util/Array"; import * as $type from "../../core/util/Type"; import * as $mapUtils from "./MapUtils"; import * as $object from "../../core/util/Object"; import * as $utils from "../../core/util/Utils"; /** * Registry of projections that can be looked up by name via the `projectionName` * setting on `MapChart`. The `am5map` entry point populates this with all * bundled d3-geo projections. Custom projections can be added via * `registerProjection()`. */ export const projectionRegistry = {}; /** * Wraps a projection factory so the projections it produces carry their * registered `name`. The tag lets `ChartSerializer` emit a matching * `projectionName` even for charts that set `projection` directly in code. */ export function tagProjection(name, factory) { return () => { const projection = factory(); projection.__projectionName = name; return projection; }; } /** * Registers a projection factory under the given name so it can be referenced * by `MapChart`'s `projectionName` setting. * * Returns a name-tagged factory: use its return value (e.g. `const cc = * registerProjection("geoConicConformal", geoConicConformal)`) as * `projection: cc()` and the chart will round-trip through serialization the * same way the bundled projections do. */ export function registerProjection(name, factory) { const tagged = tagProjection(name, factory); projectionRegistry[name] = tagged; return tagged; } export class MapChart extends SerialChart { constructor() { super(...arguments); this._pLat = 0; this._pLon = 0; this._movePoints = {}; this._downZoomLevel = 1; this._doubleDownDistance = 0; /** * A [[Rectangle]] element that shows the area being drawn when `boxZoom` is * enabled. * * @since 5.20.2 */ this.boxZoomSelection = Rectangle.new(this._root, { themeTags: ["map", "selection"], isMeasured: false, visible: false }); this._panSuspended = false; this._dirtyGeometries = false; this._geometryColection = { type: "GeometryCollection", geometries: [] }; this._centerLocation = null; this._mapBounds = [[0, 0], [0, 0]]; this._geoCentroid = { longitude: 0, latitude: 0 }; this._geoBounds = { left: 0, right: 0, top: 0, bottom: 0 }; this._prevGeoBounds = { left: 0, right: 0, top: 0, bottom: 0 }; this._dispatchBounds = false; this._mapFitted = false; this._centerX = 0; this._centerY = 0; } _makeGeoPath() { const projection = this.get("projection"); const path = geoPath(); path.projection(projection); this.setPrivateRaw("geoPath", path); } /** * Returns a geoPoint of the current zoom position. * * You can later use it to restore zoom position, e.g.: `chart.zoomToGeoPoint(geoPoint, zoomLevel, true)`. * * @since 5.2.19 */ geoPoint() { return this.invert(this.seriesContainer.toGlobal({ x: this.width() / 2, y: this.height() / 2 })); } /** * Returns coordinates to geographical center of the map. */ geoCentroid() { return this._geoCentroid; } /** * Returns geographical bounds of the map. */ geoBounds() { return this._geoBounds; } _handleSetWheel() { const wheelX = this.get("wheelX"); const wheelY = this.get("wheelY"); const chartContainer = this.chartContainer; if (wheelX != "none" || wheelY != "none") { if (this._wheelDp) { this._wheelDp.dispose(); } this._wheelDp = chartContainer.events.on("wheel", (event) => { const wheelEasing = this.get("wheelEasing"); const wheelSensitivity = this.get("wheelSensitivity", 1); const wheelDuration = this.get("wheelDuration", 0); const wheelEvent = event.originalEvent; // Ignore wheel event if it is happening on a non-chart element, e.g. if // some page element is over the chart. let prevent = false; if ($utils.isLocalEvent(wheelEvent, this)) { prevent = true; } else { return; } const point = chartContainer._display.toLocal(event.point); if ((wheelY == "zoom")) { if (this.get("zoomLevel") == this.get("minZoomLevel", 1) && wheelEvent.deltaY > 0) { return; } this._handleWheelZoom(wheelEvent.deltaY, point); } else if (wheelY == "rotateY") { this._handleWheelRotateY(wheelEvent.deltaY / 5 * wheelSensitivity, wheelDuration, wheelEasing); } else if (wheelY == "rotateX") { this._handleWheelRotateX(wheelEvent.deltaY / 5 * wheelSensitivity, wheelDuration, wheelEasing); } if ((wheelX == "zoom")) { this._handleWheelZoom(wheelEvent.deltaX, point); } else if (wheelX == "rotateY") { this._handleWheelRotateY(wheelEvent.deltaX / 5 * wheelSensitivity, wheelDuration, wheelEasing); } else if (wheelX == "rotateX") { this._handleWheelRotateX(wheelEvent.deltaX / 5 * wheelSensitivity, wheelDuration, wheelEasing); } if (prevent) { wheelEvent.preventDefault(); } }); this._disposers.push(this._wheelDp); } else { if (this._wheelDp) { this._wheelDp.dispose(); } } } _handleDoubleClick(event) { if (!this.get("doubleClickZoom")) { return; } if (event.originalEvent.shiftKey) { this.zoomOut(); } else { const level = this.get("zoomLevel", 1) * this.get("zoomStep", 2); this._zoomToScreenPoint(this.chartContainer._display.toLocal(event.point), level); } } /** * Zooms to a point on the screen, rotating the map so that the point ends up * in the center of it, if the map is panned by rotating. */ _zoomToScreenPoint(point, level) { const projection = this.get("projection"); const rotateX = this.get("panX") == "rotateX"; const rotateY = this.get("panY") == "rotateY"; if ((rotateX || rotateY) && projection.rotate && projection.invert) { const geoPoint = this.invert(point); // A point outside the globe does not translate to a location if ($type.isNumber(geoPoint.longitude) && $type.isNumber(geoPoint.latitude)) { this.zoomToGeoPoint(geoPoint, level, true, undefined, rotateX ? -geoPoint.longitude : this.get("rotationX", 0), rotateY ? -geoPoint.latitude : this.get("rotationY", 0)); return; } } this.zoomToPoint(point, level); } _isBoxZoomEvent(event) { const originalEvent = event.originalEvent; // Right and middle button presses come through as "pointerdown" too if (originalEvent.button) { return false; } switch (this.get("boxZoom", "none")) { case "drag": return true; case "shift": return originalEvent.shiftKey; case "ctrl": return originalEvent.ctrlKey; case "alt": return originalEvent.altKey; } return false; } _handleBoxDown(event) { // Root simulates pointer events to drag a focused element with a keyboard if (event.simulated || !this._isBoxZoomEvent(event)) { return; } this._boxDownPoint = this.chartContainer._display.toLocal(event.point); // Panning has to be off while the box is being drawn, otherwise the map // would move along with the pointer if (!this._panSuspended) { this._panSuspended = true; this._suspendedPanX = this.get("panX"); this._suspendedPanY = this.get("panY"); this.setRaw("panX", "none"); this.setRaw("panY", "none"); } this._drawBoxSelection(this._boxDownPoint); this.boxZoomSelection.set("visible", true); } _handleBoxMove(event) { if (this._boxDownPoint) { this._drawBoxSelection(this.chartContainer._display.toLocal(event.point)); } } _handleBoxUp(event) { const downPoint = this._boxDownPoint; const upPoint = this.chartContainer._display.toLocal(event.point); this._endBoxZoom(); if (!downPoint) { return; } const width = Math.abs(upPoint.x - downPoint.x); const height = Math.abs(upPoint.y - downPoint.y); // Ignore accidental micro-drags, e.g. a double click with a key held down if (width < 10 || height < 10) { return; } const level = this.get("zoomLevel", 1) * Math.min(this.width() / width, this.height() / height); this._zoomToScreenPoint({ x: (downPoint.x + upPoint.x) / 2, y: (downPoint.y + upPoint.y) / 2 }, level); } _endBoxZoom() { this._boxDownPoint = undefined; this.boxZoomSelection.set("visible", false); if (this._panSuspended) { this._panSuspended = false; // Do not undo a panning change made while the box was being drawn if (this.get("panX") == "none") { this.setRaw("panX", this._suspendedPanX); } if (this.get("panY") == "none") { this.setRaw("panY", this._suspendedPanY); } } } _drawBoxSelection(point) { const downPoint = this._boxDownPoint; this.boxZoomSelection.setAll({ x: Math.min(downPoint.x, point.x), y: Math.min(downPoint.y, point.y), width: Math.abs(point.x - downPoint.x), height: Math.abs(point.y - downPoint.y) }); } _prepareChildren() { super._prepareChildren(); if (this.isDirty("projectionName")) { const name = this.get("projectionName"); if (name && name !== this._appliedProjectionName) { const factory = projectionRegistry[name]; if (factory) { this._appliedProjectionName = name; this.set("projection", factory()); } } } const projection = this.get("projection"); const w = this.innerWidth(); const h = this.innerHeight(); const previousGeometries = this._geometryColection.geometries; if (this.isDirty("projection")) { this._makeGeoPath(); this.markDirtyProjection(); this._fitMap(); projection.scale(this.getPrivate("mapScale") * this.get("zoomLevel", 1)); if (projection.rotate) { projection.rotate([this.get("rotationX", 0), this.get("rotationY", 0), this.get("rotationZ", 0)]); } let prev = this._prevSettings.projection; if (prev && prev != projection) { let hw = w / 2; let hh = h / 2; if (prev.invert) { let centerLocation = prev.invert([hw, hh]); if (centerLocation) { let xy = projection(centerLocation); if (xy) { let translate = projection.translate(); let xx = hw - ((xy[0] - translate[0])); let yy = hh - ((xy[1] - translate[1])); projection.translate([xx, yy]); this.setRaw("translateX", xx); this.setRaw("translateY", yy); } } } } } // Handle projection blend animation (driven by animatePrivate on projectionBlend) if (this.isPrivateDirty("projectionBlend") && this._projectionBlendData) { const d = this._projectionBlendData; const t = this.getPrivate("projectionBlend", 0); // Update the blended raw function d.mutate(t); // Interpolate scale and translate const scale = d.sourceScale + (d.targetScale - d.sourceScale) * t; const tx = d.sourceTranslateX + (d.targetTranslateX - d.sourceTranslateX) * t; const ty = d.sourceTranslateY + (d.targetTranslateY - d.sourceTranslateY) * t; d.blended.scale(scale * this.get("zoomLevel", 1)); d.blended.translate([tx, ty]); if (d.blended.rotate) { d.blended.rotate([ this.get("rotationX", 0), this.get("rotationY", 0), this.get("rotationZ", 0) ]); } this.setPrivateRaw("mapScale", scale); this.setRaw("translateX", tx); this.setRaw("translateY", ty); this.markDirtyProjection(); // Animation complete — install the real target projection if (t >= 1) { this._projectionRaw = d.targetRaw; this._projectionBlendData = undefined; this._projectionBlendAnim = undefined; this.set("projection", d.target); } } if (this.isDirty("wheelX") || this.isDirty("wheelY")) { this._handleSetWheel(); } if (this._dirtyGeometries) { this._geometryColection.geometries = []; this.series.each((series) => { $array.pushAll(this._geometryColection.geometries, series._geometries); }); this._fitMap(); } if (previousGeometries.length != 0 && (w != this._pw || h != this._ph || this._dirtyGeometries)) { if (w > 0 && h > 0) { let hw = w / 2; let hh = h / 2; projection.fitSize([w, h], this._geometryColection); const newScale = projection.scale(); this.setPrivateRaw("mapScale", newScale); projection.scale(newScale * this.get("zoomLevel", 1)); if (this._centerLocation) { let xy = projection(this._centerLocation); if (xy) { let translate = projection.translate(); let xx = hw - ((xy[0] - translate[0])); let yy = hh - ((xy[1] - translate[1])); projection.translate([xx, yy]); this.setRaw("translateX", xx); this.setRaw("translateY", yy); this._centerX = translate[0]; this._centerY = translate[1]; } } this.markDirtyProjection(); const geoPath = this.getPrivate("geoPath"); this._mapBounds = geoPath.bounds(this._geometryColection); } } this._pw = w; this._ph = h; if (this.isDirty("zoomControl")) { const previous = this._prevSettings.zoomControl; const zoomControl = this.get("zoomControl"); if (zoomControl !== previous) { this._disposeProperty("zoomControl"); if (previous) { previous.dispose(); } if (zoomControl) { zoomControl.setPrivate("chart", this); this.children.push(zoomControl); } this.setRaw("zoomControl", zoomControl); } } if (this.isDirty("zoomLevel")) { projection.scale(this.getPrivate("mapScale") * this.get("zoomLevel", 1)); this.markDirtyProjection(); this.series.each((series) => { if (series.isType("MapPointSeries")) { if (series.get("autoScale")) { $array.each(series.dataItems, (dataItem) => { const bullets = dataItem.bullets; if (bullets) { $array.each(bullets, (bullet) => { const sprite = bullet.get("sprite"); if (sprite) { sprite.set("scale", this.get("zoomLevel")); } }); } }); } } }); const zoomControl = this.get("zoomControl"); if (zoomControl) { const zoomLevel = this.get("zoomLevel", 1); if (zoomLevel == this.get("minZoomLevel", 1)) { zoomControl.minusButton._setC("disabled", true); } else { zoomControl.minusButton._setC("disabled", false); } if (zoomLevel == this.get("maxZoomLevel", 32)) { zoomControl.plusButton._setC("disabled", true); } else { zoomControl.plusButton._setC("disabled", false); } } } if (this.isDirty("translateX") || this.isDirty("translateY")) { projection.translate([this.get("translateX", this.width() / 2), this.get("translateY", this.height() / 2)]); this.markDirtyProjection(); } if (projection.rotate) { if (this.isDirty("rotationX") || this.isDirty("rotationY") || this.isDirty("rotationZ")) { projection.rotate([this.get("rotationX", 0), this.get("rotationY", 0), this.get("rotationZ", 0)]); this.markDirtyProjection(); } } if (this.isDirty("pinchZoom") || this.get("panX") || this.get("panY")) { this._setUpTouch(); } } _fitMap() { const projection = this.get("projection"); let w = this.innerWidth(); let h = this.innerHeight(); if (w > 0 && h > 0) { projection.fitSize([w, h], this._geometryColection); this.setPrivateRaw("mapScale", projection.scale()); const translate = projection.translate(); this.setRaw("translateX", translate[0]); this.setRaw("translateY", translate[1]); this._centerX = translate[0]; this._centerY = translate[1]; const geoPath = this.getPrivate("geoPath"); this._mapBounds = geoPath.bounds(this._geometryColection); this._geoCentroid = $mapUtils.getGeoCentroid(this._geometryColection); const bounds = $mapUtils.getGeoBounds(this._geometryColection); this._geoBounds = bounds; if (this._geometryColection.geometries.length > 0) { bounds.left = $math.round(this._geoBounds.left, 3); bounds.right = $math.round(this._geoBounds.right, 3); bounds.top = $math.round(this._geoBounds.top, 3); bounds.bottom = $math.round(this._geoBounds.bottom, 3); const prevGeoBounds = this._prevGeoBounds; if (prevGeoBounds && !$utils.sameBounds(bounds, prevGeoBounds)) { this._dispatchBounds = true; this._prevGeoBounds = bounds; } } this._mapFitted = true; } } /** * Returns geographical coordinates for calculated or manual center of the * map. */ homeGeoPoint() { let homeGeoPoint = this.get("homeGeoPoint"); if (!homeGeoPoint) { const geoPath = this.getPrivate("geoPath"); const bounds = geoPath.bounds(this._geometryColection); const left = bounds[0][0]; const top = bounds[0][1]; const right = bounds[1][0]; const bottom = bounds[1][1]; homeGeoPoint = this.invert({ x: left + (right - left) / 2, y: top + (bottom - top) / 2 }); } return homeGeoPoint; } /** * Repositions the map to the "home" zoom level and center coordinates. * * @see {@link https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Resetting_position_level} for more info * @param duration Animation duration in milliseconds */ goHome(duration) { this.zoomToGeoPoint(this.homeGeoPoint(), this.get("homeZoomLevel", 1), true, duration, this.get("homeRotationX"), this.get("homeRotationY")); } _updateChildren() { const projection = this.get("projection"); if (projection.invert) { let w = this.innerWidth(); let h = this.innerHeight(); if (w > 0 && h > 0) { this._centerLocation = projection.invert([this.innerWidth() / 2, this.innerHeight() / 2]); } } super._updateChildren(); } _afterChanged() { super._afterChanged(); if (this._dispatchBounds) { this._dispatchBounds = false; const type = "geoboundschanged"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this }); } } } _setUpTouch() { if (!this.chartContainer._display.cancelTouch) { this.chartContainer._display.cancelTouch = (this.get("pinchZoom") || this.get("panX") || this.get("panY")) ? true : false; } } /** * @ignore */ markDirtyGeometries() { this._dirtyGeometries = true; this.markDirty(); } /** * @ignore */ markDirtyProjection() { this.series.each((series) => { series.markDirtyProjection(); }); } _afterNew() { this._defaultThemes.push(MapChartDefaultTheme.new(this._root)); this._settings.themeTags = $utils.mergeTags(this._settings.themeTags, ["map"]); this.children.push(this.bulletsContainer); super._afterNew(); this._makeGeoPath(); this.chartContainer.children.push(this.seriesContainer); this.chartContainer.children.push(this.boxZoomSelection); if (this.get("translateX") == null) { this.set("translateX", this.width() / 2); } if (this.get("translateY") == null) { this.set("translateY", this.height() / 2); } // Setting trasnparent background so that full body of the plot container // is interactive this.chartContainer.set("interactive", true); this.chartContainer.set("interactiveChildren", false); this.chartContainer.set("background", Rectangle.new(this._root, { themeTags: ["map", "background"], fill: Color.fromHex(0x000000), fillOpacity: 0 })); this._disposers.push(this.chartContainer.events.on("pointerdown", (event) => { this._handleChartDown(event); })); this._disposers.push(this.chartContainer.events.on("globalpointerup", (event) => { this._handleChartUp(event); })); this._disposers.push(this.chartContainer.events.on("globalpointermove", (event) => { this._handleChartMove(event); })); this._disposers.push(this.chartContainer.events.on("dblclick", (event) => { this._handleDoubleClick(event); })); // Box zoom this._disposers.push(this.chartContainer.events.on("pointerdown", (event) => { this._handleBoxDown(event); })); this._disposers.push(this.chartContainer.events.on("globalpointermove", (event) => { this._handleBoxMove(event); })); this._disposers.push(this.chartContainer.events.on("globalpointerup", (event) => { this._handleBoxUp(event); })); let license = false; for (let i = 0; i < registry.licenses.length; i++) { if (registry.licenses[i].match(/^AM5M.{5,}/i)) { license = true; } } if (!license) { this._root._showBranding(); } else { this._root._licenseApplied(); } this._setUpTouch(); } _handleChartDown(event) { this._downZoomLevel = this.get("zoomLevel", 1); const downPoints = this.chartContainer._downPoints; let count = $object.keys(downPoints).length; if (count == 1) { // workaround to solve a problem when events are added to some children of chart container (rotation stops working) let downPoint = downPoints[1]; if (!downPoint) { downPoint = downPoints[0]; } if (downPoint && (downPoint.x == event.point.x && downPoint.y == event.point.y)) { count = 0; } } if (count > 0) { this._downTranslateX = this.get("translateX"); this._downTranslateY = this.get("translateY"); this._downRotationX = this.get("rotationX"); this._downRotationY = this.get("rotationY"); this._downRotationZ = this.get("rotationZ"); const downId = this.chartContainer._getDownPointId(); if (downId) { let movePoint = this._movePoints[downId]; if (movePoint) { this.chartContainer._downPoints[downId] = movePoint; } } } else if (count == 0) { let bg = this.chartContainer.get("background"); if (bg) { bg.events.enableType("click"); } if (this.get("panX") || this.get("panY")) { if (this._za) { this._za.stop(); } if (this._txa) { this._txa.stop(); } if (this._tya) { this._tya.stop(); } if (this._rxa) { this._rxa.stop(); } if (this._rya) { this._rya.stop(); } const downPoint = this.chartContainer._display.toLocal(event.point); this._downTranslateX = this.get("translateX"); this._downTranslateY = this.get("translateY"); this._downRotationX = this.get("rotationX"); this._downRotationY = this.get("rotationY"); this._downRotationZ = this.get("rotationZ"); let projection = this.get("projection"); if (projection.invert) { let l0 = projection.invert([downPoint.x, downPoint.y]); let l1 = projection.invert([downPoint.x + 1, downPoint.y + 1]); if (l0 && l1) { this._pLon = Math.abs(l1[0] - l0[0]); this._pLat = Math.abs(l1[1] - l0[1]); } } } } } /** * Converts screen coordinates (X and Y) within chart to latitude and * longitude. * * @param point Screen coordinates * @return Geographical coordinates */ invert(point) { let projection = this.get("projection"); if (projection.invert) { const ll = projection.invert([point.x, point.y]); if (ll) { return { longitude: ll[0], latitude: ll[1] }; } } return { longitude: 0, latitude: 0 }; } /** * Converts latitude/longitude to screen coordinates (X and Y). * * @param point Geographical coordinates * @param rotationX X rotation of a map if different from current * @param rotationY Y rotation of a map if different from current * * @return Screen coordinates */ convert(point, rotationX, rotationY) { let projection = this.get("projection"); let xy; if (!projection.rotate) { rotationX = undefined; rotationY = undefined; } if (rotationX != null || rotationY != null) { if (rotationX == null) { rotationX = 0; } if (rotationY == null) { rotationY = 0; } let rotation = projection.rotate(); projection.rotate([rotationX, rotationY, 0]); xy = projection([point.longitude, point.latitude]); projection.rotate(rotation); } else { xy = projection([point.longitude, point.latitude]); } if (xy) { return { x: xy[0], y: xy[1] }; } return { x: 0, y: 0 }; } _handleChartUp(_event) { this.chartContainer._downPoints = {}; } _handlePinch() { const chartContainer = this.chartContainer; let i = 0; let downPoints = []; let movePoints = []; $object.each(chartContainer._downPoints, (k, point) => { downPoints[i] = point; let movePoint = this._movePoints[k]; if (movePoint) { movePoints[i] = movePoint; } i++; }); if (downPoints.length > 1 && movePoints.length > 1) { const display = chartContainer._display; let downPoint0 = downPoints[0]; let downPoint1 = downPoints[1]; let movePoint0 = movePoints[0]; let movePoint1 = movePoints[1]; if (downPoint0 && downPoint1 && movePoint0 && movePoint1) { downPoint0 = display.toLocal(downPoint0); downPoint1 = display.toLocal(downPoint1); movePoint0 = display.toLocal(movePoint0); movePoint1 = display.toLocal(movePoint1); let initialDistance = Math.hypot(downPoint1.x - downPoint0.x, downPoint1.y - downPoint0.y); let currentDistance = Math.hypot(movePoint1.x - movePoint0.x, movePoint1.y - movePoint0.y); let level = currentDistance / initialDistance * this._downZoomLevel; level = $math.fitToRange(level, this.get("minZoomLevel", 1), this.get("maxZoomLevel", 32)); let moveCenter = { x: movePoint0.x + (movePoint1.x - movePoint0.x) / 2, y: movePoint0.y + (movePoint1.y - movePoint0.y) / 2 }; let downCenter = { x: downPoint0.x + (downPoint1.x - downPoint0.x) / 2, y: downPoint0.y + (downPoint1.y - downPoint0.y) / 2 }; let tx = this._downTranslateX || 0; let ty = this._downTranslateY || 0; let zoomLevel = this._downZoomLevel; let xx = moveCenter.x - (-tx + downCenter.x) / zoomLevel * level; let yy = moveCenter.y - (-ty + downCenter.y) / zoomLevel * level; this.set("zoomLevel", level); this.set("translateX", xx); this.set("translateY", yy); } } } _handleChartMove(event) { const chartContainer = this.chartContainer; let downPoint = chartContainer._getDownPoint(); const downPointId = chartContainer._getDownPointId(); const originalEvent = event.originalEvent; const pointerId = originalEvent.pointerId; if (this.get("pinchZoom")) { if (pointerId) { this._movePoints[pointerId] = event.point; if ($object.keys(chartContainer._downPoints).length > 1) { this._handlePinch(); return; } } } if (downPointId && pointerId && pointerId != downPointId) { return; } else { if (downPoint) { const panX = this.get("panX"); const panY = this.get("panY"); if (panX != "none" || panY != "none") { const display = chartContainer._display; let local = display.toLocal(event.point); downPoint = display.toLocal(downPoint); let x = this._downTranslateX; let y = this._downTranslateY; if (Math.hypot(downPoint.x - local.x, downPoint.y - local.y) > 5) { let bg = chartContainer.get("background"); if (bg) { bg.events.disableType("click"); } if ($type.isNumber(x) && $type.isNumber(y)) { let projection = this.get("projection"); const zoomLevel = this.get("zoomLevel", 1); const maxPanOut = this.get("maxPanOut", 0.4); const bounds = this._mapBounds; const w = this.width(); const h = this.height(); const ww = bounds[1][0] - bounds[0][0]; const hh = bounds[1][1] - bounds[0][1]; if (panX == "translateX") { x += local.x - downPoint.x; const cx = w / 2 - (w / 2 - this._centerX) * zoomLevel; x = Math.min(x, cx + ww * maxPanOut * zoomLevel); x = Math.max(x, cx - ww * maxPanOut * zoomLevel); } if (panY == "translateY") { y += local.y - downPoint.y; const cy = h / 2 - (h / 2 - this._centerY) * zoomLevel; y = Math.min(y, cy + hh * maxPanOut * zoomLevel); y = Math.max(y, cy - hh * maxPanOut * zoomLevel); } this.set("translateX", x); this.set("translateY", y); if (projection.invert) { let downLocation = projection.invert([downPoint.x, downPoint.y]); if (location && downLocation) { if (panX == "rotateX") { this.set("rotationX", this._downRotationX - (downPoint.x - local.x) * this._pLon); } if (panY == "rotateY") { this.set("rotationY", this._downRotationY + (downPoint.y - local.y) * this._pLat); } } } } } } } } } _handleWheelRotateY(delta, duration, easing) { this._rya = this.animate({ key: "rotationY", to: this.get("rotationY", 0) - delta, duration: duration, easing: easing }); } _handleWheelRotateX(delta, duration, easing) { this._rxa = this.animate({ key: "rotationX", to: this.get("rotationX", 0) - delta, duration: duration, easing: easing }); } _handleWheelZoom(delta, point) { let step = this.get("zoomStep", 2); let zoomLevel = this.get("zoomLevel", 1); let newZoomLevel = zoomLevel; if (delta > 0) { newZoomLevel = zoomLevel / step; } else if (delta < 0) { newZoomLevel = zoomLevel * step; } if (newZoomLevel != zoomLevel) { this.zoomToPoint(point, newZoomLevel); } } /** * Zoom the map to geographical bounds. * * @param geoBounds Bounds * @param duration Animation duration in milliseconds * @param rotationX X rotation of a map at the end of zoom * @param rotationY Y rotation of a map at the end of zoom */ zoomToGeoBounds(geoBounds, duration, rotationX, rotationY) { if (geoBounds.right < geoBounds.left) { geoBounds.right = 180; geoBounds.left = -180; } const geoPath = this.getPrivate("geoPath"); const mapBounds = geoPath.bounds(this._geometryColection); let p0 = this.convert({ longitude: geoBounds.left, latitude: geoBounds.top }, rotationX, rotationY); let p1 = this.convert({ longitude: geoBounds.right, latitude: geoBounds.bottom }, rotationX, rotationY); if (p0.y < mapBounds[0][1]) { p0.y = mapBounds[0][1]; } if (p1.y > mapBounds[1][1]) { p1.y = mapBounds[1][1]; } let zl = this.get("zoomLevel", 1); let bounds = { left: p0.x, right: p1.x, top: p0.y, bottom: p1.y }; let seriesContainer = this.seriesContainer; let zoomLevel = .9 * Math.min(seriesContainer.innerWidth() / (bounds.right - bounds.left) * zl, seriesContainer.innerHeight() / (bounds.bottom - bounds.top) * zl); let x = bounds.left + (bounds.right - bounds.left) / 2; let y = bounds.top + (bounds.bottom - bounds.top) / 2; let geoPoint = this.invert({ x, y }); if (rotationX != null || rotationY != null) { this.rotate(rotationX, rotationY); } return this.zoomToGeoPoint(geoPoint, zoomLevel, true, duration); } /** * Zooms the map to specific screen point. * * @param point Point * @param level Zoom level * @param center Center the map * @param duration Duration of the animation in milliseconds */ zoomToPoint(point, level, center, duration) { if (level) { level = $math.fitToRange(level, this.get("minZoomLevel", 1), this.get("maxZoomLevel", 32)); } if (!$type.isNumber(duration)) { duration = this.get("animationDuration", 0); } const easing = this.get("animationEasing"); const zoomLevel = this.get("zoomLevel", 1); if (this.get("centerMapOnZoomOut") && level == this.get("homeZoomLevel", 1)) { point = this.convert(this.homeGeoPoint(), this.get("homeRotationX"), this.get("homeRotationY")); center = true; } let x = point.x; let y = point.y; let tx = this.get("translateX", 0); let ty = this.get("translateY", 0); let cx = x; let cy = y; if (center) { cx = this.width() / 2; cy = this.height() / 2; } let xx = cx - ((x - tx) / zoomLevel * level); let yy = cy - ((y - ty) / zoomLevel * level); this._txa = this.animate({ key: "translateX", to: xx, duration: duration, easing: easing }); this._tya = this.animate({ key: "translateY", to: yy, duration: duration, easing: easing }); this._za = this.animate({ key: "zoomLevel", to: level, duration: duration, easing: easing }); if (zoomLevel != level) { this._root.readerAlert(this._t("Zoom level changed to %1", this._root.locale, $type.numberToString(level))); } return this._za; } /** * Zooms the map to specific geographical point. * * @param geoPoint Point * @param level Zoom level * @param center Center the map * @param duration Duration of the animation in milliseconds * @param rotationX X rotation of a map at the end of zoom * @param rotationY Y rotation of a map at the end of zoom * */ zoomToGeoPoint(geoPoint, level, center, duration, rotationX, rotationY) { let xy = this.convert(geoPoint, rotationX, rotationY); if (rotationX != null || rotationY != null) { this.rotate(rotationX, rotationY, duration); } if (xy) { return this.zoomToPoint(xy, level, center, duration); } } rotate(rotationX, rotationY, duration) { const projection = this.get("projection"); if (!projection.rotate) { } else { if (!$type.isNumber(duration)) { duration = this.get("animationDuration", 0); } const easing = this.get("animationEasing"); if (rotationX != null) { this.animate({ key: "rotationX", to: rotationX, duration: duration, easing: easing }); } if (rotationY != null) { this.animate({ key: "rotationY", to: rotationY, duration: duration, easing: easing }); } } } /** * Animates the map projection transition from the current projection to * the target projection over the specified duration. * * Since d3-geo does not expose raw projection functions on projection * instances, you must pass both the target `GeoProjection` and its * corresponding `GeoRawProjection`. On the first call you must also * pass `sourceRaw` so the method knows the current projection's raw * function. Subsequent calls reuse the previous target's raw * automatically. * * @param target Target projection (e.g. `geoOrthographic()`) * @param targetRaw Raw projection function (e.g. `geoOrthographicRaw`) * @param duration Duration in milliseconds (default: `animationDuration`) * @param easing Easing function (default: `animationEasing`) * @param sourceRaw Raw function of the current projection (needed on first call) * * @since 5.16.0 */ animateProjection(target, targetRaw, duration, easing, sourceRaw) { // Cancel any ongoing projection blend animation if (this._projectionBlendAnim) { this._projectionBlendAnim.stop(); this._projectionBlendAnim = undefined; this._projectionBlendData = undefined; } // Use provided sourceRaw, or fall back to stored one from previous call if (sourceRaw) { this._projectionRaw = sourceRaw; } const currentRaw = this._projectionRaw; // If we don't have a source raw, snap immediately if (!currentRaw) { this.set("projection", target); this._projectionRaw = targetRaw; return; } if (duration == null) { duration = this.get("animationDuration", 0); } if (!duration) { this.set("projection", target); this._projectionRaw = targetRaw; return; } if (!easing) { easing = this.get("animationEasing"); } // Stop ongoing zoom/pan/rotation animations if (this._za) this._za.stop(); if (this._txa) this._txa.stop(); if (this._tya) this._tya.stop(); if (this._rxa) this._rxa.stop(); if (this._rya) this._rya.stop(); const w = this.innerWidth(); const h = this.innerHeight(); // Capture current source state const sScale = this.getPrivate("mapScale"); const sTx = this.get("translateX", w / 2); const sTy = this.get("translateY", h / 2); // Compute fitted target state using a temporary projection const tempTarget = geoProjection(targetRaw); tempTarget.fitSize([w, h], this._geometryColection); const tScale = tempTarget.scale(); const tTranslate = tempTarget.translate(); // Create blended projection via geoProjectionMutator const mutate = geoProjectionMutator((t) => { return (lambda, phi) => { const p0 = currentRaw(lambda, phi); const p1 = targetRaw(lambda, phi); return [ (1 - t) * p0[0] + t * p1[0], (1 - t) * p0[1] + t * p1[1] ]; }; }); // Initialize at t=0 const blended = mutate(0); blended.scale(sScale * this.get("zoomLevel", 1)); blended.translate([sTx, sTy]); blended.precision(0.5); if (blended.rotate) { blended.rotate([ this.get("rotationX", 0), this.get("rotationY", 0), this.get("rotationZ", 0) ]); } // Install blended projection without triggering isDirty("projection") cascade this.setRaw("projection", blended); this._makeGeoPath(); // Store blend data for _prepareChildren to use this._projectionBlendData = { blended, mutate, sourceScale: sScale, targetScale: tScale, sourceTranslateX: sTx, sourceTranslateY: sTy, targetTranslateX: tTranslate[0], targetTranslateY: tTranslate[1], target, targetRaw }; // Animate the blend value 0→1 using the standard animation system this._projectionBlendAnim = this.animatePrivate({ key: "projectionBlend", from: 0, to: 1, duration, easing }); } /** * Zooms the map in. */ zoomIn() { return this.zoomToPoint({ x: this.width() / 2, y: this.height() / 2 }, this.get("zoomLevel", 1) * this.get("zoomStep", 2)); } /** * Zooms the map out. */ zoomOut() { return this.zoomToPoint({ x: this.width() / 2, y: this.height() / 2 }, this.get("zoomLevel", 1) / this.get("zoomStep", 2)); } _clearDirty() { super._clearDirty(); this._dirtyGeometries = false; this._mapFitted = false; } /** * Returns area of a mapPolygon in square pixels. */ getArea(dataItem) { const geoPath = this.getPrivate("geoPath"); const geometry = dataItem.get("geometry"); if (geometry) { return geoPath.area(geometry); } return 0; } } MapChart.className = "MapChart"; MapChart.classNames = SerialChart.classNames.concat([MapChart.className]); //# sourceMappingURL=MapChart.js.map