├── .gitattributes
├── .gitignore
├── README.md
├── custom_components
└── zigbee2mqtt_networkmap
│ ├── __init__.py
│ ├── manifest.json
│ ├── services.yaml
│ └── www
│ ├── map.html
│ ├── panzoom
│ └── panzoom.min.js
│ └── viz.js
│ ├── LICENSE.txt
│ ├── full.render.js
│ └── viz.js
├── info.md
└── map.gif
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask instance folder
57 | instance/
58 |
59 | # Scrapy stuff:
60 | .scrapy
61 |
62 | # Sphinx documentation
63 | docs/_build/
64 |
65 | # PyBuilder
66 | target/
67 |
68 | # IPython Notebook
69 | .ipynb_checkpoints
70 |
71 | # pyenv
72 | .python-version
73 |
74 | # celery beat schedule file
75 | celerybeat-schedule
76 |
77 | # dotenv
78 | .env
79 |
80 | # virtualenv
81 | venv/
82 | ENV/
83 |
84 | # Spyder project settings
85 | .spyderproject
86 |
87 | # Rope project settings
88 | .ropeproject
89 |
90 | # =========================
91 | # Operating System Files
92 | # =========================
93 |
94 | # OSX
95 | # =========================
96 |
97 | .DS_Store
98 | .AppleDouble
99 | .LSOverride
100 |
101 | # Thumbnails
102 | ._*
103 |
104 | # Files that might appear in the root of a volume
105 | .DocumentRevisions-V100
106 | .fseventsd
107 | .Spotlight-V100
108 | .TemporaryItems
109 | .Trashes
110 | .VolumeIcon.icns
111 |
112 | # Directories potentially created on remote AFP share
113 | .AppleDB
114 | .AppleDesktop
115 | Network Trash Folder
116 | Temporary Items
117 | .apdisk
118 |
119 | # Windows
120 | # =========================
121 |
122 | # Windows image file caches
123 | Thumbs.db
124 | ehthumbs.db
125 |
126 | # Folder config file
127 | Desktop.ini
128 |
129 | # Recycle Bin used on file shares
130 | $RECYCLE.BIN/
131 |
132 | # Windows Installer files
133 | *.cab
134 | *.msi
135 | *.msm
136 | *.msp
137 |
138 | # Windows shortcuts
139 | *.lnk
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | The frontend of Zigbee2mqtt offers the possibility to display the map.
4 |
5 |
6 | # ha_zigbee2mqtt_networkmap
7 | Custom Component for Homeassistant to show the [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt) Networkmap with [viz.js](https://github.com/mdaines/viz.js/).
8 |
9 | [Forum link with Screenshot](https://community.home-assistant.io/t/zigbee2mqtt-show-the-networkmap-in-hassio/89116)
10 |
11 | 
12 |
13 | **Important:** you have to clear the browsercache after each update
14 |
15 |
16 | **Instructions**
17 | 1. Download or clone [https://github.com/rgruebel/ha_zigbee2mqtt_networkmap](https://github.com/rgruebel/ha_zigbee2mqtt_networkmap)
18 | 2. Copy "custom_components/zigbee2mqtt_networkmap" and content to your "custom_components" folder.
19 | 3. Add the following to your configuration.yaml. It is possible to update the map directly via button. If you want to use this functionality you also have to activate the webhook component. Otherwise you have to use the service "zigbee2mqtt_networkmap.update"
20 |
21 | webhook:
22 |
23 | zigbee2mqtt_networkmap:
24 | #topic: your topic (optional, default zigbee2mqtt)
25 | panel_iframe:
26 | networkmap:
27 | title: 'Zigbee Map'
28 | url: '/local/community/zigbee2mqtt_networkmap/map.html'
29 | icon: 'mdi:graphql'
30 | You can set the graphviz engine via URL Parameter:
31 | map.html?engine=circo (Default: circo, [Supported Engines](https://github.com/mdaines/viz.js/wiki/Supported-Graphviz-Features))
32 |
33 | 4. Restart Homeassistant
34 | 5. Test if everything is working
35 |
36 |
37 |
--------------------------------------------------------------------------------
/custom_components/zigbee2mqtt_networkmap/__init__.py:
--------------------------------------------------------------------------------
1 | import homeassistant.loader as loader
2 | import os
3 | from distutils.dir_util import copy_tree
4 | from datetime import datetime
5 | from aiohttp import web
6 |
7 | # Updated by NG to handle waiting for the MQTT message received. Error checking and more.
8 | # Update Log
9 | # - Error checking
10 | # - Ajax/Timed checking for update mqtt message
11 | # - No need to reload page to get update, as it will come thru the ajax check request
12 |
13 | DOMAIN = 'zigbee2mqtt_networkmap'
14 |
15 | DEPENDENCIES = ['mqtt']
16 |
17 | CONF_TOPIC = 'topic'
18 | DEFAULT_TOPIC = 'zigbee2mqtt'
19 |
20 |
21 | async def async_setup(hass, config):
22 | fromDirectory = hass.config.path('custom_components', 'zigbee2mqtt_networkmap', 'www')
23 | toDirectory = hass.config.path('www', 'community', 'zigbee2mqtt_networkmap')
24 |
25 | copy_tree(fromDirectory, toDirectory)
26 |
27 | """Set up the zigbee2mqtt_networkmap component."""
28 | mqtt = hass.components.mqtt
29 | topic = config[DOMAIN].get(CONF_TOPIC, DEFAULT_TOPIC)
30 | entity_id = 'zigbee2mqtt_networkmap.map_last_update'
31 | tmpVar = type('', (), {})() #tmpVar.received_update and update_data
32 |
33 | async def handle_webhook_trigger_update(hass, webhook_id, request):
34 | """Handle trigger update webhook callback."""
35 | await update_service(None)
36 | return web.json_response({"success": "ok"})
37 |
38 | async def handle_webhook_check_update(hass, webhook_id, request):
39 | """Handle check update webhook callback."""
40 | return web.json_response({"success": "ok", "update_received": bool(tmpVar.received_update), "update_received_data": tmpVar.update_data, "last_update": tmpVar.last_update })
41 |
42 |
43 | # Register the Webhook for trigger update
44 | webhook_trigger_update_id = hass.components.webhook.async_generate_id()
45 | hass.components.webhook.async_register(
46 | DOMAIN, 'zigbee2mqtt_networkmap-webhook_trigger_update', webhook_trigger_update_id, handle_webhook_trigger_update)
47 |
48 | # Register the Webhook for trigger update
49 | webhook_check_update_id = hass.components.webhook.async_generate_id()
50 | hass.components.webhook.async_register(
51 | DOMAIN, 'zigbee2mqtt_networkmap-webhook_check_update', webhook_check_update_id, handle_webhook_check_update)
52 |
53 | f = open(hass.config.path('www', 'community', 'zigbee2mqtt_networkmap', 'settings.js'), "w")
54 | f.write("var webhook_trigger_update_id = '{}';\n".format(webhook_trigger_update_id))
55 | f.write("var webhook_check_update_id = '{}';".format(webhook_check_update_id))
56 | f.close()
57 |
58 |
59 | # Listener to be called when we receive a message.
60 | async def message_received(msg):
61 | """Handle new MQTT messages."""
62 | # Save Response as JS variable in source.js
63 | payload = msg.payload.replace('\n', ' ').replace(
64 | '\r', '').replace("'", r"\'")
65 | last_update = datetime.now()
66 | f = open(hass.config.path(
67 | 'www', 'community', 'zigbee2mqtt_networkmap', 'source.js'), "w")
68 | f.write("var last_update = new Date('" +
69 | last_update.strftime('%Y/%m/%d %H:%M:%S')+"');\nvar graph = \'"+payload+"\'")
70 | f.close()
71 | hass.states.async_set(entity_id, last_update)
72 | tmpVar.received_update = True
73 | tmpVar.update_data = payload
74 | tmpVar.last_update = last_update.strftime('%Y/%m/%d %H:%M:%S')
75 |
76 | # Subscribe our listener to the networkmap topic.
77 | await mqtt.async_subscribe(topic+'/bridge/networkmap/graphviz', message_received)
78 |
79 | # Set the initial state.
80 | hass.states.async_set(entity_id, None)
81 | tmpVar.received_update = False
82 | tmpVar.update_data = None
83 | tmpVar.last_update = None
84 |
85 | # Service to publish a message on MQTT.
86 | async def update_service(call):
87 | """Service to send a message."""
88 | tmpVar.received_update = False
89 | tmpVar.update_data = None
90 | tmpVar.last_update = None
91 | mqtt.async_publish(topic+'/bridge/networkmap/routes', 'graphviz')
92 |
93 | hass.services.async_register(DOMAIN, 'update', update_service)
94 | return True
95 |
--------------------------------------------------------------------------------
/custom_components/zigbee2mqtt_networkmap/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "zigbee2mqtt_networkmap",
3 | "name": "zigbee2mqtt Networkmap",
4 | "version": "0.0.1",
5 | "documentation": "https://github.com/rgruebel/ha_zigbee2mqtt_networkmap",
6 | "dependencies": ["mqtt"],
7 | "codeowners": [
8 | "@rgruebel"
9 | ],
10 | "requirements": []
11 | }
12 |
--------------------------------------------------------------------------------
/custom_components/zigbee2mqtt_networkmap/services.yaml:
--------------------------------------------------------------------------------
1 | update:
2 | description: Update the Networkmap.
3 |
--------------------------------------------------------------------------------
/custom_components/zigbee2mqtt_networkmap/www/map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
57 |
58 |
218 |
219 |
220 |
221 |
--------------------------------------------------------------------------------
/custom_components/zigbee2mqtt_networkmap/www/panzoom/panzoom.min.js:
--------------------------------------------------------------------------------
1 | (function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f() } else if (typeof define === "function" && define.amd) { define([], f) } else { var g; if (typeof window !== "undefined") { g = window } else if (typeof global !== "undefined") { g = global } else if (typeof self !== "undefined") { g = self } else { g = this } g.panzoom = f() } })(function () { var define, module, exports; return function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r }()({ 1: [function (require, module, exports) { "use strict"; var wheel = require("wheel"); var animate = require("amator"); var eventify = require("ngraph.events"); var kinetic = require("./lib/kinetic.js"); var preventTextSelection = require("./lib/textSelectionInterceptor.js")(); var Transform = require("./lib/transform.js"); var makeSvgController = require("./lib/svgController.js"); var makeDomController = require("./lib/domController.js"); var defaultZoomSpeed = .065; var defaultDoubleTapZoomSpeed = 1.75; var doubleTapSpeedInMS = 300; module.exports = createPanZoom; function createPanZoom(domElement, options) { options = options || {}; var panController = options.controller; if (!panController) { if (domElement instanceof SVGElement) { panController = makeSvgController(domElement) } if (domElement instanceof HTMLElement) { panController = makeDomController(domElement) } } if (!panController) { throw new Error("Cannot create panzoom for the current type of dom element") } var owner = panController.getOwner(); var storedCTMResult = { x: 0, y: 0 }; var isDirty = false; var transform = new Transform; if (panController.initTransform) { panController.initTransform(transform) } var filterKey = typeof options.filterKey === "function" ? options.filterKey : noop; var realPinch = typeof options.realPinch === "boolean" ? options.realPinch : false; var bounds = options.bounds; var maxZoom = typeof options.maxZoom === "number" ? options.maxZoom : Number.POSITIVE_INFINITY; var minZoom = typeof options.minZoom === "number" ? options.minZoom : 0; var boundsPadding = typeof options.boundsPadding === "number" ? options.boundsPadding : .05; var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === "number" ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; var beforeWheel = options.beforeWheel || noop; var speed = typeof options.zoomSpeed === "number" ? options.zoomSpeed : defaultZoomSpeed; validateBounds(bounds); if (options.autocenter) { autocenter() } var frameAnimation; var lastTouchEndTime = 0; var touchInProgress = false; var panstartFired = false; var mouseX; var mouseY; var pinchZoomLength; var smoothScroll; if ("smoothScroll" in options && !options.smoothScroll) { smoothScroll = rigidScroll() } else { smoothScroll = kinetic(getPoint, scroll, options.smoothScroll) } var moveByAnimation; var zoomToAnimation; var multitouch; var paused = false; listenForEvents(); var api = { dispose: dispose, moveBy: internalMoveBy, moveTo: moveTo, centerOn: centerOn, zoomTo: publicZoomTo, zoomAbs: zoomAbs, smoothZoom: smoothZoom, getTransform: getTransformModel, showRectangle: showRectangle, pause: pause, resume: resume, isPaused: isPaused }; eventify(api); return api; function pause() { releaseEvents(); paused = true } function resume() { if (paused) { listenForEvents(); paused = false } } function isPaused() { return paused } function showRectangle(rect) { var clientRect = owner.getBoundingClientRect(); var size = transformToScreen(clientRect.width, clientRect.height); var rectWidth = rect.right - rect.left; var rectHeight = rect.bottom - rect.top; if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) { throw new Error("Invalid rectangle") } var dw = size.x / rectWidth; var dh = size.y / rectHeight; var scale = Math.min(dw, dh); transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2; transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2; transform.scale = scale } function transformToScreen(x, y) { if (panController.getScreenCTM) { var parentCTM = panController.getScreenCTM(); var parentScaleX = parentCTM.a; var parentScaleY = parentCTM.d; var parentOffsetX = parentCTM.e; var parentOffsetY = parentCTM.f; storedCTMResult.x = x * parentScaleX - parentOffsetX; storedCTMResult.y = y * parentScaleY - parentOffsetY } else { storedCTMResult.x = x; storedCTMResult.y = y } return storedCTMResult } function autocenter() { var w; var h; var left = 0; var top = 0; var sceneBoundingBox = getBoundingBox(); if (sceneBoundingBox) { left = sceneBoundingBox.left; top = sceneBoundingBox.top; w = sceneBoundingBox.right - sceneBoundingBox.left; h = sceneBoundingBox.bottom - sceneBoundingBox.top } else { var ownerRect = owner.getBoundingClientRect(); w = ownerRect.width; h = ownerRect.height } var bbox = panController.getBBox(); if (bbox.width === 0 || bbox.height === 0) { return } var dh = h / bbox.height; var dw = w / bbox.width; var scale = Math.min(dw, dh); transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left; transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top; transform.scale = scale } function getTransformModel() { return transform } function getPoint() { return { x: transform.x, y: transform.y } } function moveTo(x, y) { transform.x = x; transform.y = y; keepTransformInsideBounds(); triggerEvent("pan"); makeDirty() } function moveBy(dx, dy) { moveTo(transform.x + dx, transform.y + dy) } function keepTransformInsideBounds() { var boundingBox = getBoundingBox(); if (!boundingBox) return; var adjusted = false; var clientRect = getClientRect(); var diff = boundingBox.left - clientRect.right; if (diff > 0) { transform.x += diff; adjusted = true } diff = boundingBox.right - clientRect.left; if (diff < 0) { transform.x += diff; adjusted = true } diff = boundingBox.top - clientRect.bottom; if (diff > 0) { transform.y += diff; adjusted = true } diff = boundingBox.bottom - clientRect.top; if (diff < 0) { transform.y += diff; adjusted = true } return adjusted } function getBoundingBox() { if (!bounds) return; if (typeof bounds === "boolean") { var ownerRect = owner.getBoundingClientRect(); var sceneWidth = ownerRect.width; var sceneHeight = ownerRect.height; return { left: sceneWidth * boundsPadding, top: sceneHeight * boundsPadding, right: sceneWidth * (1 - boundsPadding), bottom: sceneHeight * (1 - boundsPadding) } } return bounds } function getClientRect() { var bbox = panController.getBBox(); var leftTop = client(bbox.left, bbox.top); return { left: leftTop.x, top: leftTop.y, right: bbox.width * transform.scale + leftTop.x, bottom: bbox.height * transform.scale + leftTop.y } } function client(x, y) { return { x: x * transform.scale + transform.x, y: y * transform.scale + transform.y } } function makeDirty() { isDirty = true; frameAnimation = window.requestAnimationFrame(frame) } function zoomByRatio(clientX, clientY, ratio) { if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { throw new Error("zoom requires valid numbers") } var newScale = transform.scale * ratio; if (newScale < minZoom) { if (transform.scale === minZoom) return; ratio = minZoom / transform.scale } if (newScale > maxZoom) { if (transform.scale === maxZoom) return; ratio = maxZoom / transform.scale } var size = transformToScreen(clientX, clientY); transform.x = size.x - ratio * (size.x - transform.x); transform.y = size.y - ratio * (size.y - transform.y); var transformAdjusted = keepTransformInsideBounds(); if (!transformAdjusted) transform.scale *= ratio; triggerEvent("zoom"); makeDirty() } function zoomAbs(clientX, clientY, zoomLevel) { var ratio = zoomLevel / transform.scale; zoomByRatio(clientX, clientY, ratio) } function centerOn(ui) { var parent = ui.ownerSVGElement; if (!parent) throw new Error("ui element is required to be within the scene"); var clientRect = ui.getBoundingClientRect(); var cx = clientRect.left + clientRect.width / 2; var cy = clientRect.top + clientRect.height / 2; var container = parent.getBoundingClientRect(); var dx = container.width / 2 - cx; var dy = container.height / 2 - cy; internalMoveBy(dx, dy, true) } function internalMoveBy(dx, dy, smooth) { if (!smooth) { return moveBy(dx, dy) } if (moveByAnimation) moveByAnimation.cancel(); var from = { x: 0, y: 0 }; var to = { x: dx, y: dy }; var lastX = 0; var lastY = 0; moveByAnimation = animate(from, to, { step: function (v) { moveBy(v.x - lastX, v.y - lastY); lastX = v.x; lastY = v.y } }) } function scroll(x, y) { cancelZoomAnimation(); moveTo(x, y) } function dispose() { releaseEvents() } function listenForEvents() { owner.addEventListener("mousedown", onMouseDown); owner.addEventListener("dblclick", onDoubleClick); owner.addEventListener("touchstart", onTouch); owner.addEventListener("keydown", onKeyDown); wheel.addWheelListener(owner, onMouseWheel); makeDirty() } function releaseEvents() { wheel.removeWheelListener(owner, onMouseWheel); owner.removeEventListener("mousedown", onMouseDown); owner.removeEventListener("keydown", onKeyDown); owner.removeEventListener("dblclick", onDoubleClick); owner.removeEventListener("touchstart", onTouch); if (frameAnimation) { window.cancelAnimationFrame(frameAnimation); frameAnimation = 0 } smoothScroll.cancel(); releaseDocumentMouse(); releaseTouches(); triggerPanEnd() } function frame() { if (isDirty) applyTransform() } function applyTransform() { isDirty = false; panController.applyTransform(transform); triggerEvent("transform"); frameAnimation = 0 } function onKeyDown(e) { var x = 0, y = 0, z = 0; if (e.keyCode === 38) { y = 1 } else if (e.keyCode === 40) { y = -1 } else if (e.keyCode === 37) { x = 1 } else if (e.keyCode === 39) { x = -1 } else if (e.keyCode === 189 || e.keyCode === 109) { z = 1 } else if (e.keyCode === 187 || e.keyCode === 107) { z = -1 } if (filterKey(e, x, y, z)) { return } if (x || y) { e.preventDefault(); e.stopPropagation(); var clientRect = owner.getBoundingClientRect(); var offset = Math.min(clientRect.width, clientRect.height); var moveSpeedRatio = .05; var dx = offset * moveSpeedRatio * x; var dy = offset * moveSpeedRatio * y; internalMoveBy(dx, dy) } if (z) { var scaleMultiplier = getScaleMultiplier(z); var ownerRect = owner.getBoundingClientRect(); publicZoomTo(ownerRect.width / 2, ownerRect.height / 2, scaleMultiplier) } } function onTouch(e) { beforeTouch(e); if (e.touches.length === 1) { return handleSingleFingerTouch(e, e.touches[0]) } else if (e.touches.length === 2) { pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); multitouch = true; startTouchListenerIfNeeded() } } function beforeTouch(e) { if (options.onTouch && !options.onTouch(e)) { return } e.stopPropagation(); e.preventDefault() } function beforeDoubleClick(e) { if (options.onDoubleClick && !options.onDoubleClick(e)) { return } e.preventDefault(); e.stopPropagation() } function handleSingleFingerTouch(e) { var touch = e.touches[0]; var offset = getOffsetXY(touch); mouseX = offset.x; mouseY = offset.y; smoothScroll.cancel(); startTouchListenerIfNeeded() } function startTouchListenerIfNeeded() { if (!touchInProgress) { touchInProgress = true; document.addEventListener("touchmove", handleTouchMove); document.addEventListener("touchend", handleTouchEnd); document.addEventListener("touchcancel", handleTouchEnd) } } function handleTouchMove(e) { if (e.touches.length === 1) { e.stopPropagation(); var touch = e.touches[0]; var offset = getOffsetXY(touch); var dx = offset.x - mouseX; var dy = offset.y - mouseY; if (dx !== 0 && dy !== 0) { triggerPanStart() } mouseX = offset.x; mouseY = offset.y; var point = transformToScreen(dx, dy); internalMoveBy(point.x, point.y) } else if (e.touches.length === 2) { multitouch = true; var t1 = e.touches[0]; var t2 = e.touches[1]; var currentPinchLength = getPinchZoomLength(t1, t2); var scaleMultiplier = 1; if (realPinch) { scaleMultiplier = currentPinchLength / pinchZoomLength } else { var delta = 0; if (currentPinchLength < pinchZoomLength) { delta = 1 } else if (currentPinchLength > pinchZoomLength) { delta = -1 } scaleMultiplier = getScaleMultiplier(delta) } mouseX = (t1.clientX + t2.clientX) / 2; mouseY = (t1.clientY + t2.clientY) / 2; publicZoomTo(mouseX, mouseY, scaleMultiplier); pinchZoomLength = currentPinchLength; e.stopPropagation(); e.preventDefault() } } function handleTouchEnd(e) { if (e.touches.length > 0) { var offset = getOffsetXY(e.touches[0]); mouseX = offset.x; mouseY = offset.y } else { var now = new Date; if (now - lastTouchEndTime < doubleTapSpeedInMS) { smoothZoom(mouseX, mouseY, zoomDoubleClickSpeed) } lastTouchEndTime = now; touchInProgress = false; triggerPanEnd(); releaseTouches() } } function getPinchZoomLength(finger1, finger2) { return Math.sqrt((finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY)) } function onDoubleClick(e) { beforeDoubleClick(e); var offset = getOffsetXY(e); smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed) } function onMouseDown(e) { if (touchInProgress) { e.stopPropagation(); return false } var isLeftButton = e.button === 1 && window.event !== null || e.button === 0; if (!isLeftButton) return; smoothScroll.cancel(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); mouseX = point.x; mouseY = point.y; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); preventTextSelection.capture(e.target || e.srcElement); return false } function onMouseMove(e) { if (touchInProgress) return; triggerPanStart(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); var dx = point.x - mouseX; var dy = point.y - mouseY; mouseX = point.x; mouseY = point.y; internalMoveBy(dx, dy) } function onMouseUp() { preventTextSelection.release(); triggerPanEnd(); releaseDocumentMouse() } function releaseDocumentMouse() { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); panstartFired = false } function releaseTouches() { document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener("touchend", handleTouchEnd); document.removeEventListener("touchcancel", handleTouchEnd); panstartFired = false; multitouch = false } function onMouseWheel(e) { if (beforeWheel(e)) return; smoothScroll.cancel(); var scaleMultiplier = getScaleMultiplier(e.deltaY); if (scaleMultiplier !== 1) { var offset = getOffsetXY(e); publicZoomTo(offset.x, offset.y, scaleMultiplier); e.preventDefault() } } function getOffsetXY(e) { var offsetX, offsetY; var ownerRect = owner.getBoundingClientRect(); offsetX = e.clientX - ownerRect.left; offsetY = e.clientY - ownerRect.top; return { x: offsetX, y: offsetY } } function smoothZoom(clientX, clientY, scaleMultiplier) { var fromValue = transform.scale; var from = { scale: fromValue }; var to = { scale: scaleMultiplier * fromValue }; smoothScroll.cancel(); cancelZoomAnimation(); zoomToAnimation = animate(from, to, { step: function (v) { zoomAbs(clientX, clientY, v.scale) } }) } function publicZoomTo(clientX, clientY, scaleMultiplier) { smoothScroll.cancel(); cancelZoomAnimation(); return zoomByRatio(clientX, clientY, scaleMultiplier) } function cancelZoomAnimation() { if (zoomToAnimation) { zoomToAnimation.cancel(); zoomToAnimation = null } } function getScaleMultiplier(delta) { var scaleMultiplier = 1; if (delta > 0) { scaleMultiplier = 1 - speed } else if (delta < 0) { scaleMultiplier = 1 + speed } return scaleMultiplier } function triggerPanStart() { if (!panstartFired) { triggerEvent("panstart"); panstartFired = true; smoothScroll.start() } } function triggerPanEnd() { if (panstartFired) { if (!multitouch) smoothScroll.stop(); triggerEvent("panend") } } function triggerEvent(name) { api.fire(name, api) } } function noop() { } function validateBounds(bounds) { var boundsType = typeof bounds; if (boundsType === "undefined" || boundsType === "boolean") return; var validBounds = isNumber(bounds.left) && isNumber(bounds.top) && isNumber(bounds.bottom) && isNumber(bounds.right); if (!validBounds) throw new Error("Bounds object is not valid. It can be: " + "undefined, boolean (true|false) or an object {left, top, right, bottom}") } function isNumber(x) { return Number.isFinite(x) } function isNaN(value) { if (Number.isNaN) { return Number.isNaN(value) } return value !== value } function rigidScroll() { return { start: noop, stop: noop, cancel: noop } } function autoRun() { if (typeof document === "undefined") return; var scripts = document.getElementsByTagName("script"); if (!scripts) return; var panzoomScript; Array.from(scripts).forEach(function (x) { if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { panzoomScript = x } }); if (!panzoomScript) return; var query = panzoomScript.getAttribute("query"); if (!query) return; var globalName = panzoomScript.getAttribute("name") || "pz"; var started = Date.now(); tryAttach(); function tryAttach() { var el = document.querySelector(query); if (!el) { var now = Date.now(); var elapsed = now - started; if (elapsed < 2e3) { setTimeout(tryAttach, 100); return } console.error("Cannot find the panzoom element", globalName); return } var options = collectOptions(panzoomScript); console.log(options); window[globalName] = createPanZoom(el, options) } function collectOptions(script) { var attrs = script.attributes; var options = {}; for (var i = 0; i < attrs.length; ++i) { var attr = attrs[i]; var nameValue = getPanzoomAttributeNameValue(attr); if (nameValue) { options[nameValue.name] = nameValue.value } } return options } function getPanzoomAttributeNameValue(attr) { if (!attr.name) return; var isPanZoomAttribute = attr.name[0] === "p" && attr.name[1] === "z" && attr.name[2] === "-"; if (!isPanZoomAttribute) return; var name = attr.name.substr(3); var value = JSON.parse(attr.value); return { name: name, value: value } } } autoRun() }, { "./lib/domController.js": 2, "./lib/kinetic.js": 3, "./lib/svgController.js": 4, "./lib/textSelectionInterceptor.js": 5, "./lib/transform.js": 6, amator: 7, "ngraph.events": 9, wheel: 10 }], 2: [function (require, module, exports) { module.exports = makeDomController; function makeDomController(domElement) { var elementValid = domElement instanceof HTMLElement; if (!elementValid) { throw new Error("svg element is required for svg.panzoom to work") } var owner = domElement.parentElement; if (!owner) { throw new Error("Do not apply panzoom to the detached DOM element. ") } domElement.scrollTop = 0; owner.setAttribute("tabindex", 1); var api = { getBBox: getBBox, getOwner: getOwner, applyTransform: applyTransform }; return api; function getOwner() { return owner } function getBBox() { return { left: 0, top: 0, width: domElement.clientWidth, height: domElement.clientHeight } } function applyTransform(transform) { domElement.style.transformOrigin = "0 0 0"; domElement.style.transform = "matrix(" + transform.scale + ", 0, 0, " + transform.scale + ", " + transform.x + ", " + transform.y + ")" } } }, {}], 3: [function (require, module, exports) { module.exports = kinetic; function kinetic(getPoint, scroll, settings) { if (typeof settings !== "object") { settings = {} } var minVelocity = typeof settings.minVelocity === "number" ? settings.minVelocity : 5; var amplitude = typeof settings.amplitude === "number" ? settings.amplitude : .25; var lastPoint; var timestamp; var timeConstant = 342; var ticker; var vx, targetX, ax; var vy, targetY, ay; var raf; return { start: start, stop: stop, cancel: dispose }; function dispose() { window.clearInterval(ticker); window.cancelAnimationFrame(raf) } function start() { lastPoint = getPoint(); ax = ay = vx = vy = 0; timestamp = new Date; window.clearInterval(ticker); window.cancelAnimationFrame(raf); ticker = window.setInterval(track, 100) } function track() { var now = Date.now(); var elapsed = now - timestamp; timestamp = now; var currentPoint = getPoint(); var dx = currentPoint.x - lastPoint.x; var dy = currentPoint.y - lastPoint.y; lastPoint = currentPoint; var dt = 1e3 / (1 + elapsed); vx = .8 * dx * dt + .2 * vx; vy = .8 * dy * dt + .2 * vy } function stop() { window.clearInterval(ticker); window.cancelAnimationFrame(raf); var currentPoint = getPoint(); targetX = currentPoint.x; targetY = currentPoint.y; timestamp = Date.now(); if (vx < -minVelocity || vx > minVelocity) { ax = amplitude * vx; targetX += ax } if (vy < -minVelocity || vy > minVelocity) { ay = amplitude * vy; targetY += ay } raf = window.requestAnimationFrame(autoScroll) } function autoScroll() { var elapsed = Date.now() - timestamp; var moving = false; var dx = 0; var dy = 0; if (ax) { dx = -ax * Math.exp(-elapsed / timeConstant); if (dx > .5 || dx < -.5) moving = true; else dx = ax = 0 } if (ay) { dy = -ay * Math.exp(-elapsed / timeConstant); if (dy > .5 || dy < -.5) moving = true; else dy = ay = 0 } if (moving) { scroll(targetX + dx, targetY + dy); raf = window.requestAnimationFrame(autoScroll) } } } }, {}], 4: [function (require, module, exports) { module.exports = makeSvgController; function makeSvgController(svgElement) { var elementValid = svgElement instanceof SVGElement; if (!elementValid) { throw new Error("svg element is required for svg.panzoom to work") } var owner = svgElement.ownerSVGElement; if (!owner) { throw new Error("Do not apply panzoom to the root