├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── cytoscape-leaf.css ├── cytoscape-leaf.js ├── demo.html ├── gh-pages ├── cytoscape-leaf.css ├── cytoscape-leaf.js ├── demo.html └── index.html ├── package-lock.json ├── package.json ├── src ├── assign.js ├── core.js ├── cy-leaflet.js ├── defaults.js └── index.js ├── test └── example.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "amd": true, 7 | "es6": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "semi": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 30 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | # Label to use when marking an issue as stale 9 | staleLabel: stale 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale, because it has not had 13 | activity within the past 30 days. It will be closed if no further activity 14 | occurs within the next 30 days. If a feature request is important to you, 15 | please consider making a pull request. Thank you for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright (c) 2018-2021, The Cytoscape Consortium. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-leaf 2 | ================================================================================ 3 | 4 | [![DOI](https://zenodo.org/badge/405766955.svg)](https://zenodo.org/badge/latestdoi/405766955) 5 | 6 | ## Description 7 | 8 | A Cytoscape.js extension for integrating [Leaflet.js](https://leafletjs.com) ([demo](https://cytoscape.github.io/cytoscape.js-leaflet)). The Cytoscape graph is displayed overtop a map, and the node positions are set automatically based on geographic coordinates. 9 | 10 | ## Dependencies 11 | 12 | * Cytoscape.js 3.x, >= 3.2.0 13 | * Leaflet.js 1.x, >= 1.7.1 14 | * Leaflet providers 1.x, >= 1.12.0 15 | 16 | 17 | ## Usage instructions 18 | 19 | **Note that, aside from the ordinary JS imports, you must include `leaflet.css` and `cytoscape-leaf.css` in order for the map to display properly.** 20 | 21 | Download the library: 22 | * via npm: `npm install cytoscape-leaf`, 23 | * via direct download in the repository (probably from a tag). 24 | 25 | Import the library as appropriate for your project: 26 | 27 | ES import: 28 | 29 | ```js 30 | import cytoscape from 'cytoscape'; 31 | import leaflet from 'cytoscape-leaf'; 32 | 33 | cytoscape.use( leaflet ); 34 | ``` 35 | 36 | CommonJS require: 37 | 38 | ```js 39 | let cytoscape = require('cytoscape'); 40 | let leaflet = require('cytoscape-leaf'); 41 | 42 | cytoscape.use( leaflet ); // register extension 43 | ``` 44 | 45 | AMD: 46 | 47 | ```js 48 | require(['cytoscape', 'cytoscape-leaf'], function( cytoscape, leaflet ){ 49 | leaflet( cytoscape ); // register extension 50 | }); 51 | ``` 52 | 53 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 54 | 55 | ## API 56 | 57 | ### Node positions 58 | 59 | Because node positions should correspond to consistent geographic coordinates on the map, the `position` of each node is ignored in each element JSON object. Instead, the position is automatically set based on the `lat` and `lng` within the node's `data`, e.g.: 60 | 61 | ```js 62 | const someNodeJson = { 63 | position: {}, // ignored 64 | data: { id: 'foo', lat: 43.662402, lng: -79.388910 } 65 | }; 66 | ``` 67 | 68 | Node positions are automatically updated when the viewport is modified (i.e. via zoom and/or pan) to correspond to their specified geographic coordinates. 69 | 70 | If you want to use use custom fields for geographic coordinates, you can set `options.latitude` and `options.longitude` accordingly. 71 | 72 | ### Instance creation 73 | 74 | To create an instance of the extension, call `cy.leaflet(options)`. The following options are supported: 75 | 76 | ```js 77 | const options = { 78 | // the container in which the map should live, should be a sibling of the cytoscape container 79 | container: document.getElementById('cy-leaflet'), 80 | 81 | // the data field for latitude 82 | latitude: 'lat', 83 | 84 | // the data field for longitude 85 | longitude: 'lng' 86 | }; 87 | 88 | const leaf = cy.leaflet(options); 89 | ``` 90 | 91 | The Leaflet `map` instance can be accessed via `leaf.map`. The ordinary [Leaflet Map API](https://leafletjs.com/reference-1.7.1.html) may be used on the `map` instance. Additionally, the `L` static Leaflet API can be accessed via `leaf.L` -- though you may alternatively `import L` as normal. 92 | 93 | ### Interaction mode 94 | 95 | The graph map has an interaction mode. It can either be in pan mode or edit mode. 96 | 97 | When in pan mode, the user can zoom and pan about the map -- changing the viewport. In pan mode, graph elements are non-interactive. When the user manipulates the viewport in pan mode, the `leaflet-viewport` is applied to the elements. This can be used to create a peek feature: When the user is panning and zooming, the elements are faded out so that the user can see the street names underneath the elements. 98 | 99 | When in edit mode, the user is unable to zoom or pan -- the viewport is static. In edit mode, graph elements are interactive (e.g. edges can be clicked and nodes can be dragged). The mode is toggled for the user when he or she presses the control (CTRL) key. 100 | 101 | - `leaf.enablePanMode()` : Enables pan mode, disables edit mode 102 | - `leaf.enableEditMode()` : Enabled edit mode, disables pan mode 103 | 104 | ### Other 105 | 106 | - `leaf.fit()` : Fit the map to the nodes 107 | - `leaf.defaultTileLayer` : The default tile layer (which can be removed via `leaf.map.removeLayer(leaf.defaultTileLayer)`) 108 | - `leaf.destroy()` : Destroys the map 109 | 110 | ## Build targets 111 | 112 | * `npm run test` : Run Mocha tests in `./test` 113 | * `npm run build` : Build `./src/**` into `cytoscape-leaf.js` 114 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running) 115 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server 116 | * `npm run lint` : Run eslint on the source 117 | 118 | N.b. all builds use babel, so modern ES features can be used in the `src`. 119 | 120 | 121 | ## Publishing instructions 122 | 123 | This project is set up to automatically be published to npm and bower. To publish: 124 | 125 | 1. Build the extension : `npm run build:release` 126 | 1. Commit the build : `git commit -am "Build for release"` 127 | 1. Bump the version number and tag: `npm version major|minor|patch` 128 | 1. Push to origin: `git push && git push --tags` 129 | 1. Publish to npm: `npm publish .` 130 | 1. [Make a new release](https://github.com/cytoscape/cytoscape.js-leadlet/releases/new) for Zenodo. 131 | -------------------------------------------------------------------------------- /cytoscape-leaf.css: -------------------------------------------------------------------------------- 1 | .cy-leaflet-toggle { 2 | position: absolute; 3 | /* left: 0; */ 4 | /* top: 75px; */ 5 | left: 0; 6 | bottom: 0; 7 | margin: 10px; 8 | z-index: 999; 9 | width: 30px; 10 | background: #fff; 11 | border-radius: 4px; 12 | border: 2px solid rgba(0, 0, 0, 0.2); 13 | } 14 | 15 | .cy-leaflet-toggle-button { 16 | height: 33px; 17 | cursor: pointer; 18 | position: relative; 19 | z-index: 0; 20 | } 21 | 22 | .cy-leaflet-toggle-button + .cy-leaflet-toggle-button { 23 | border-top: 1px solid rgba(0, 0, 0, 0.2); 24 | } 25 | 26 | .cy-leaflet-toggle-button:hover, 27 | .cy-leaflet-toggle-button:active { 28 | background-color: rgb(242, 242, 242); 29 | } 30 | 31 | /* custom icon -- using font awesome, swap out your own icon here 32 | https://fontawesome.com/license/free 33 | */ 34 | .cy-leaflet-toggle-pan { 35 | background-size: 16px 16px; 36 | background-position: 50% 50%; 37 | background-repeat: no-repeat; 38 | background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20448%20512%22%3E%3Cpath%20fill%3D%22currentColor%22%20d%3D%22M372.57%20112.641v-10.825c0-43.612-40.52-76.691-83.039-65.546-25.629-49.5-94.09-47.45-117.982.747C130.269%2026.456%2089.144%2057.945%2089.144%20102v126.13c-19.953-7.427-43.308-5.068-62.083%208.871-29.355%2021.796-35.794%2063.333-14.55%2093.153L132.48%20498.569a32%2032%200%200%200%2026.062%2013.432h222.897c14.904%200%2027.835-10.289%2031.182-24.813l30.184-130.958A203.637%20203.637%200%200%200%20448%20310.564V179c0-40.62-35.523-71.992-75.43-66.359zm27.427%20197.922c0%2011.731-1.334%2023.469-3.965%2034.886L368.707%20464h-201.92L51.591%20302.303c-14.439-20.27%2015.023-42.776%2029.394-22.605l27.128%2038.079c8.995%2012.626%2029.031%206.287%2029.031-9.283V102c0-25.645%2036.571-24.81%2036.571.691V256c0%208.837%207.163%2016%2016%2016h6.856c8.837%200%2016-7.163%2016-16V67c0-25.663%2036.571-24.81%2036.571.691V256c0%208.837%207.163%2016%2016%2016h6.856c8.837%200%2016-7.163%2016-16V101.125c0-25.672%2036.57-24.81%2036.57.691V256c0%208.837%207.163%2016%2016%2016h6.857c8.837%200%2016-7.163%2016-16v-76.309c0-26.242%2036.57-25.64%2036.57-.691v131.563z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); 39 | } 40 | 41 | /* custom icon -- using font awesome, swap out your own icon here 42 | https://fontawesome.com/license/free 43 | */ 44 | .cy-leaflet-toggle-edit { 45 | background-size: 16px 16px; 46 | background-position: 55% 50%; 47 | background-repeat: no-repeat; 48 | background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20320%20512%22%3E%3Cpath%20fill%3D%22currentColor%22%20d%3D%22M302.189%20329.126H196.105l55.831%20135.993c3.889%209.428-.555%2019.999-9.444%2023.999l-49.165%2021.427c-9.165%204-19.443-.571-23.332-9.714l-53.053-129.136-86.664%2089.138C18.729%20472.71%200%20463.554%200%20447.977V18.299C0%201.899%2019.921-6.096%2030.277%205.443l284.412%20292.542c11.472%2011.179%203.007%2031.141-12.5%2031.141z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); 49 | } 50 | 51 | .cy-leaflet-toggle-active { 52 | box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.25); 53 | z-index: 1; 54 | background-color: #ddd; 55 | } -------------------------------------------------------------------------------- /cytoscape-leaf.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("leaflet")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["leaflet"], factory); 6 | else if(typeof exports === 'object') 7 | exports["cytoscapeLeaf"] = factory(require("leaflet")); 8 | else 9 | root["cytoscapeLeaf"] = factory(root["L"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_0__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // identity function for calling harmony imports with the correct context 47 | /******/ __webpack_require__.i = function(value) { return value; }; 48 | /******/ 49 | /******/ // define getter function for harmony exports 50 | /******/ __webpack_require__.d = function(exports, name, getter) { 51 | /******/ if(!__webpack_require__.o(exports, name)) { 52 | /******/ Object.defineProperty(exports, name, { 53 | /******/ configurable: false, 54 | /******/ enumerable: true, 55 | /******/ get: getter 56 | /******/ }); 57 | /******/ } 58 | /******/ }; 59 | /******/ 60 | /******/ // getDefaultExport function for compatibility with non-harmony modules 61 | /******/ __webpack_require__.n = function(module) { 62 | /******/ var getter = module && module.__esModule ? 63 | /******/ function getDefault() { return module['default']; } : 64 | /******/ function getModuleExports() { return module; }; 65 | /******/ __webpack_require__.d(getter, 'a', getter); 66 | /******/ return getter; 67 | /******/ }; 68 | /******/ 69 | /******/ // Object.prototype.hasOwnProperty.call 70 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 71 | /******/ 72 | /******/ // __webpack_public_path__ 73 | /******/ __webpack_require__.p = ""; 74 | /******/ 75 | /******/ // Load entry module and return exports 76 | /******/ return __webpack_require__(__webpack_require__.s = 5); 77 | /******/ }) 78 | /************************************************************************/ 79 | /******/ ([ 80 | /* 0 */ 81 | /***/ (function(module, exports) { 82 | 83 | module.exports = __WEBPACK_EXTERNAL_MODULE_0__; 84 | 85 | /***/ }), 86 | /* 1 */ 87 | /***/ (function(module, exports, __webpack_require__) { 88 | 89 | "use strict"; 90 | 91 | 92 | var CytoscapeLeaflet = __webpack_require__(3); 93 | 94 | var coreImpl = function coreImpl(options) { 95 | var cy = this; 96 | 97 | return new CytoscapeLeaflet(cy, options); 98 | }; 99 | 100 | module.exports = coreImpl; 101 | 102 | /***/ }), 103 | /* 2 */ 104 | /***/ (function(module, exports, __webpack_require__) { 105 | 106 | "use strict"; 107 | 108 | 109 | // Simple, internal Object.assign() polyfill for options objects etc. 110 | 111 | module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) { 112 | for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 113 | srcs[_key - 1] = arguments[_key]; 114 | } 115 | 116 | srcs.forEach(function (src) { 117 | if (src !== null && src !== undefined) { 118 | Object.keys(src).forEach(function (k) { 119 | return tgt[k] = src[k]; 120 | }); 121 | } 122 | }); 123 | 124 | return tgt; 125 | }; 126 | 127 | /***/ }), 128 | /* 3 */ 129 | /***/ (function(module, exports, __webpack_require__) { 130 | 131 | "use strict"; 132 | 133 | 134 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 135 | 136 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 137 | 138 | var assign = __webpack_require__(2); 139 | var defaults = __webpack_require__(4); 140 | 141 | __webpack_require__(6); 142 | var L = __webpack_require__(0); 143 | 144 | var CytoscapeLeaflet = function () { 145 | function CytoscapeLeaflet(cy, options) { 146 | _classCallCheck(this, CytoscapeLeaflet); 147 | 148 | this.cy = cy; 149 | this.options = assign({}, defaults, options); 150 | this.L = L; 151 | 152 | this.createMap(); 153 | this.createToggleControl(); 154 | this.configureCy(); 155 | this.addListeners(); 156 | this.onMapViewport(); // set initial node vp state 157 | this.enablePanMode(); // inital mode is pan 158 | } 159 | 160 | _createClass(CytoscapeLeaflet, [{ 161 | key: 'destroy', 162 | value: function destroy() { 163 | this.destroyMap(); 164 | this.destroyToggleControl(); 165 | this.resetCyConfig(); 166 | this.removeListeners(); 167 | } 168 | }, { 169 | key: 'getNodeLatLng', 170 | value: function getNodeLatLng(n) { 171 | var LAT = this.options.latitude; 172 | var LNG = this.options.longitude; 173 | 174 | var data = n.data(); 175 | var lat = data[LAT]; 176 | var lng = data[LNG]; 177 | 178 | return L.latLng(lat, lng); 179 | } 180 | }, { 181 | key: 'createMap', 182 | value: function createMap() { 183 | var cy = this.cy; 184 | var lCtr = this.options.container; 185 | 186 | var map = this.map = L.map(lCtr, { 187 | zoomControl: false, 188 | zoomSnap: 0 189 | }); 190 | 191 | // fallback init vp state 192 | map.setView([43.83155486662417, -79.37278747558595], 10); 193 | 194 | this.fit(); 195 | 196 | this.defaultTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png', { 197 | attribution: '© OpenStreetMap contributors © CARTO', 198 | subdomains: 'abcd', 199 | maxZoom: 19 200 | }).addTo(map); 201 | } 202 | }, { 203 | key: 'destroyMap', 204 | value: function destroyMap() { 205 | this.map.remove(); 206 | } 207 | }, { 208 | key: 'createToggleControl', 209 | value: function createToggleControl() { 210 | var container = document.createElement('div'); 211 | var panButton = document.createElement('div'); 212 | var editButton = document.createElement('div'); 213 | 214 | container.classList.add('cy-leaflet-toggle'); 215 | panButton.classList.add('cy-leaflet-toggle-button'); 216 | panButton.classList.add('cy-leaflet-toggle-pan'); 217 | editButton.classList.add('cy-leaflet-toggle-button'); 218 | editButton.classList.add('cy-leaflet-toggle-edit'); 219 | 220 | container.appendChild(panButton); 221 | container.appendChild(editButton); 222 | 223 | this.options.container.parentNode.appendChild(container); 224 | 225 | this.toggleContainer = container; 226 | this.panButton = panButton; 227 | this.editButton = editButton; 228 | } 229 | }, { 230 | key: 'destroyToggleControl', 231 | value: function destroyToggleControl() { 232 | this.toggleContainer.parentNode.removeChild(this.toggleContainer); 233 | } 234 | }, { 235 | key: 'configureCy', 236 | value: function configureCy() { 237 | this.orgZoomEnabled = this.cy.zoomingEnabled(); 238 | this.orgPanningEnabled = this.cy.panningEnabled(); 239 | 240 | this.cy.zoomingEnabled(false); 241 | this.cy.panningEnabled(false); 242 | 243 | this.cy.container().style.pointerEvents = 'none'; 244 | } 245 | }, { 246 | key: 'resetCyConfig', 247 | value: function resetCyConfig() { 248 | this.cy.zoomingEnabled(this.orgZoomEnabled); 249 | this.cy.panningEnabled(this.orgPanningEnabled); 250 | 251 | this.cy.container().style.pointerEvents = ''; 252 | } 253 | }, { 254 | key: 'addListeners', 255 | value: function addListeners() { 256 | var _this = this; 257 | 258 | this.onViewport = function () { 259 | var cy = _this.cy; 260 | 261 | cy.zoomingEnabled(true); 262 | cy.panningEnabled(true); 263 | 264 | cy.zoom(1); 265 | cy.pan({ x: 0, y: 0 }); 266 | 267 | cy.zoomingEnabled(false); 268 | cy.panningEnabled(false); 269 | 270 | cy.nodes().forEach(function (node) { 271 | return _this.setNodePosition(node); 272 | }); 273 | }; 274 | 275 | this.onEnablePan = function () { 276 | _this.enablePanMode(); 277 | }; 278 | 279 | this.onEnabledEdit = function () { 280 | _this.enableEditMode(); 281 | }; 282 | 283 | this.onDrag = function (e) { 284 | var node = e.target; 285 | 286 | _this.updateNodeGeo(node); 287 | }; 288 | 289 | this.updateNodePositionFromCyEvent = function (e) { 290 | var ele = e.target; 291 | 292 | if (ele.isNode()) { 293 | _this.setNodePosition(ele); 294 | } 295 | }; 296 | 297 | this.map.on('zoom', this.onViewport); 298 | this.map.on('move', this.onViewport); 299 | 300 | this.onViewportStart = function () { 301 | _this.cy.elements().addClass('leaflet-viewport'); 302 | }; 303 | 304 | this.onMoveTimeout = null; 305 | 306 | this.onViewportEnd = function () { 307 | clearTimeout(_this.onMoveTimeout); 308 | 309 | _this.onMoveTimeout = setTimeout(function () { 310 | _this.cy.elements().removeClass('leaflet-viewport'); 311 | }, 250); 312 | }; 313 | 314 | this.map.on('movestart', this.onViewportStart); 315 | this.map.on('moveend', this.onViewportEnd); 316 | this.map.on('zoomstart', this.onViewportStart); 317 | this.map.on('zoomend', this.onViewportEnd); 318 | 319 | this.panButton.addEventListener('click', this.onEnablePan); 320 | this.editButton.addEventListener('click', this.onEnabledEdit); 321 | 322 | this.cy.on('drag', this.onDrag); 323 | 324 | this.cy.on('add', this.updateNodePositionFromCyEvent); 325 | this.cy.on('data', this.updateNodePositionFromCyEvent); 326 | 327 | window.addEventListener('keyup', this.onToggleShortcut = function (event) { 328 | switch (event.keyCode) { 329 | case 17: 330 | // control 331 | //case 93: // meta/apple/command 332 | //case 16: // shift 333 | _this.toggleMode(); 334 | default: 335 | break; 336 | } 337 | }); 338 | 339 | this.cy.container().addEventListener('wheel', this.onWheel = function (event) { 340 | event.preventDefault(); 341 | }); 342 | } 343 | }, { 344 | key: 'removeListeners', 345 | value: function removeListeners() { 346 | this.map.off('zoom', this.onViewport); 347 | this.map.off('move', this.onViewport); 348 | 349 | this.panButton.removeEventListener('click', this.onEnablePan); 350 | this.editButton.removeEventListener('click', this.onEnabledEdit); 351 | 352 | this.cy.removeListener('drag', this.onDrag); 353 | 354 | this.cy.removeListener('add', this.updateNodePositionFromCyEvent); 355 | this.cy.removeListener('data', this.updateNodePositionFromCyEvent); 356 | 357 | window.removeEventListener('keyup', this.onToggleShortcut); 358 | 359 | this.cy.container().removeEventListener('wheel', this.onWheel); 360 | } 361 | }, { 362 | key: 'setNodePosition', 363 | value: function setNodePosition(node) { 364 | var latlng = this.getNodeLatLng(node); 365 | var pt = this.map.latLngToContainerPoint(latlng); 366 | var x = pt.x, 367 | y = pt.y; 368 | 369 | 370 | node.position({ x: x, y: y }); 371 | } 372 | }, { 373 | key: 'updateNodeGeo', 374 | value: function updateNodeGeo(node) { 375 | var _node$position = node.position(), 376 | x = _node$position.x, 377 | y = _node$position.y; 378 | 379 | var pt = L.point(x, y); 380 | var latlng = this.map.containerPointToLatLng(pt); 381 | var llObj = {}; 382 | 383 | var LAT = this.options.latitude; 384 | var LNG = this.options.longitude; 385 | 386 | llObj[LAT] = latlng.lat; 387 | llObj[LNG] = latlng.lng; 388 | 389 | node.data(llObj); 390 | } 391 | }, { 392 | key: 'onMapViewport', 393 | value: function onMapViewport() { 394 | var _this2 = this; 395 | 396 | var cy = this.cy; 397 | 398 | cy.zoomingEnabled(true); 399 | cy.panningEnabled(true); 400 | 401 | cy.zoom(1); 402 | cy.pan({ x: 0, y: 0 }); 403 | 404 | cy.zoomingEnabled(false); 405 | cy.panningEnabled(false); 406 | 407 | cy.nodes().forEach(function (node) { 408 | return _this2.setNodePosition(node); 409 | }); 410 | } 411 | }, { 412 | key: 'setZoomControlOpacity', 413 | value: function setZoomControlOpacity(opacity) { 414 | var zoomControl = document.querySelector('.leaflet-control-zoom'); 415 | if (zoomControl) { 416 | zoomControl.style.opacity = opacity; 417 | } 418 | } 419 | }, { 420 | key: 'enablePanMode', 421 | value: function enablePanMode() { 422 | this.panMode = true; 423 | this.editMode = false; 424 | 425 | this.panButton.classList.add('cy-leaflet-toggle-active'); 426 | this.editButton.classList.remove('cy-leaflet-toggle-active'); 427 | 428 | this.cy.container().style.pointerEvents = 'none'; 429 | 430 | this.setZoomControlOpacity(""); 431 | 432 | this.map.dragging.enable(); 433 | } 434 | }, { 435 | key: 'enableEditMode', 436 | value: function enableEditMode() { 437 | this.panMode = false; 438 | this.editMode = true; 439 | 440 | this.panButton.classList.remove('cy-leaflet-toggle-active'); 441 | this.editButton.classList.add('cy-leaflet-toggle-active'); 442 | 443 | this.cy.container().style.pointerEvents = ''; 444 | 445 | this.setZoomControlOpacity(0.5); 446 | 447 | this.map.dragging.disable(); 448 | } 449 | }, { 450 | key: 'toggleMode', 451 | value: function toggleMode() { 452 | if (this.panMode) { 453 | this.enableEditMode(); 454 | } else { 455 | this.enablePanMode(); 456 | } 457 | } 458 | }, { 459 | key: 'fit', 460 | value: function fit() { 461 | var _this3 = this; 462 | 463 | var cy = this.cy, 464 | map = this.map; 465 | 466 | var getGeo = function getGeo(n) { 467 | return _this3.getNodeLatLng(n); 468 | }; 469 | 470 | if (cy.nodes().nonempty()) { 471 | var bounds = L.latLngBounds(cy.nodes().map(getGeo)); 472 | 473 | map.fitBounds(bounds, { padding: [50, 50] }); 474 | } 475 | } 476 | }]); 477 | 478 | return CytoscapeLeaflet; 479 | }(); 480 | 481 | module.exports = CytoscapeLeaflet; 482 | 483 | /***/ }), 484 | /* 4 */ 485 | /***/ (function(module, exports, __webpack_require__) { 486 | 487 | "use strict"; 488 | 489 | 490 | module.exports = { 491 | container: undefined, // the container in which the map is put 492 | latitude: 'lat', 493 | longitude: 'lng' 494 | }; 495 | 496 | /***/ }), 497 | /* 5 */ 498 | /***/ (function(module, exports, __webpack_require__) { 499 | 500 | "use strict"; 501 | 502 | 503 | /* global cytoscape */ 504 | 505 | var coreImpl = __webpack_require__(1); 506 | 507 | // registers the extension on a cytoscape lib ref 508 | var register = function register(cytoscape) { 509 | if (!cytoscape) { 510 | return; 511 | } // can't register if cytoscape unspecified 512 | 513 | // register with cytoscape.js 514 | cytoscape('core', 'leaflet', coreImpl); 515 | }; 516 | 517 | if (typeof cytoscape !== 'undefined') { 518 | // expose to global cytoscape (i.e. window.cytoscape) 519 | register(cytoscape); 520 | } 521 | 522 | module.exports = register; 523 | 524 | /***/ }), 525 | /* 6 */ 526 | /***/ (function(module, exports, __webpack_require__) { 527 | 528 | var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (root, factory) { 529 | if (true) { 530 | // AMD. Register as an anonymous module. 531 | !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(0)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), 532 | __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? 533 | (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), 534 | __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 535 | } else if (typeof modules === 'object' && module.exports) { 536 | // define a Common JS module that relies on 'leaflet' 537 | module.exports = factory(require('leaflet')); 538 | } else { 539 | // Assume Leaflet is loaded into global object L already 540 | factory(L); 541 | } 542 | }(this, function (L) { 543 | 'use strict'; 544 | 545 | L.TileLayer.Provider = L.TileLayer.extend({ 546 | initialize: function (arg, options) { 547 | var providers = L.TileLayer.Provider.providers; 548 | 549 | var parts = arg.split('.'); 550 | 551 | var providerName = parts[0]; 552 | var variantName = parts[1]; 553 | 554 | if (!providers[providerName]) { 555 | throw 'No such provider (' + providerName + ')'; 556 | } 557 | 558 | var provider = { 559 | url: providers[providerName].url, 560 | options: providers[providerName].options 561 | }; 562 | 563 | // overwrite values in provider from variant. 564 | if (variantName && 'variants' in providers[providerName]) { 565 | if (!(variantName in providers[providerName].variants)) { 566 | throw 'No such variant of ' + providerName + ' (' + variantName + ')'; 567 | } 568 | var variant = providers[providerName].variants[variantName]; 569 | var variantOptions; 570 | if (typeof variant === 'string') { 571 | variantOptions = { 572 | variant: variant 573 | }; 574 | } else { 575 | variantOptions = variant.options; 576 | } 577 | provider = { 578 | url: variant.url || provider.url, 579 | options: L.Util.extend({}, provider.options, variantOptions) 580 | }; 581 | } 582 | 583 | // replace attribution placeholders with their values from toplevel provider attribution, 584 | // recursively 585 | var attributionReplacer = function (attr) { 586 | if (attr.indexOf('{attribution.') === -1) { 587 | return attr; 588 | } 589 | return attr.replace(/\{attribution.(\w*)\}/g, 590 | function (match, attributionName) { 591 | return attributionReplacer(providers[attributionName].options.attribution); 592 | } 593 | ); 594 | }; 595 | provider.options.attribution = attributionReplacer(provider.options.attribution); 596 | 597 | // Compute final options combining provider options with any user overrides 598 | var layerOpts = L.Util.extend({}, provider.options, options); 599 | L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts); 600 | } 601 | }); 602 | 603 | /** 604 | * Definition of providers. 605 | * see http://leafletjs.com/reference.html#tilelayer for options in the options map. 606 | */ 607 | 608 | L.TileLayer.Provider.providers = { 609 | OpenStreetMap: { 610 | url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 611 | options: { 612 | maxZoom: 19, 613 | attribution: 614 | '© OpenStreetMap contributors' 615 | }, 616 | variants: { 617 | Mapnik: {}, 618 | DE: { 619 | url: 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', 620 | options: { 621 | maxZoom: 18 622 | } 623 | }, 624 | CH: { 625 | url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png', 626 | options: { 627 | maxZoom: 18, 628 | bounds: [[45, 5], [48, 11]] 629 | } 630 | }, 631 | France: { 632 | url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', 633 | options: { 634 | maxZoom: 20, 635 | attribution: '© OpenStreetMap France | {attribution.OpenStreetMap}' 636 | } 637 | }, 638 | HOT: { 639 | url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', 640 | options: { 641 | attribution: 642 | '{attribution.OpenStreetMap}, ' + 643 | 'Tiles style by Humanitarian OpenStreetMap Team ' + 644 | 'hosted by OpenStreetMap France' 645 | } 646 | }, 647 | BZH: { 648 | url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png', 649 | options: { 650 | attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team', 651 | bounds: [[46.2, -5.5], [50, 0.7]] 652 | } 653 | } 654 | } 655 | }, 656 | OpenSeaMap: { 657 | url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 658 | options: { 659 | attribution: 'Map data: © OpenSeaMap contributors' 660 | } 661 | }, 662 | OpenPtMap: { 663 | url: 'http://openptmap.org/tiles/{z}/{x}/{y}.png', 664 | options: { 665 | maxZoom: 17, 666 | attribution: 'Map data: © OpenPtMap contributors' 667 | } 668 | }, 669 | OpenTopoMap: { 670 | url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', 671 | options: { 672 | maxZoom: 17, 673 | attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' 674 | } 675 | }, 676 | OpenRailwayMap: { 677 | url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', 678 | options: { 679 | maxZoom: 19, 680 | attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA)' 681 | } 682 | }, 683 | OpenFireMap: { 684 | url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png', 685 | options: { 686 | maxZoom: 19, 687 | attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA)' 688 | } 689 | }, 690 | SafeCast: { 691 | url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', 692 | options: { 693 | maxZoom: 16, 694 | attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA)' 695 | } 696 | }, 697 | Stadia: { 698 | url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', 699 | options: { 700 | maxZoom: 20, 701 | attribution: '© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors' 702 | }, 703 | variants: { 704 | AlidadeSmooth: { 705 | url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' 706 | }, 707 | AlidadeSmoothDark: { 708 | url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' 709 | }, 710 | OSMBright: { 711 | url: 'https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.png' 712 | }, 713 | Outdoors: { 714 | url: 'https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.png' 715 | } 716 | } 717 | }, 718 | Thunderforest: { 719 | url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}', 720 | options: { 721 | attribution: 722 | '© Thunderforest, {attribution.OpenStreetMap}', 723 | variant: 'cycle', 724 | apikey: '', 725 | maxZoom: 22 726 | }, 727 | variants: { 728 | OpenCycleMap: 'cycle', 729 | Transport: { 730 | options: { 731 | variant: 'transport' 732 | } 733 | }, 734 | TransportDark: { 735 | options: { 736 | variant: 'transport-dark' 737 | } 738 | }, 739 | SpinalMap: { 740 | options: { 741 | variant: 'spinal-map' 742 | } 743 | }, 744 | Landscape: 'landscape', 745 | Outdoors: 'outdoors', 746 | Pioneer: 'pioneer', 747 | MobileAtlas: 'mobile-atlas', 748 | Neighbourhood: 'neighbourhood' 749 | } 750 | }, 751 | CyclOSM: { 752 | url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 753 | options: { 754 | maxZoom: 20, 755 | attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}' 756 | } 757 | }, 758 | Hydda: { 759 | url: 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png', 760 | options: { 761 | maxZoom: 20, 762 | variant: 'full', 763 | attribution: 'Tiles courtesy of OpenStreetMap Sweden — Map data {attribution.OpenStreetMap}' 764 | }, 765 | variants: { 766 | Full: 'full', 767 | Base: 'base', 768 | RoadsAndLabels: 'roads_and_labels' 769 | } 770 | }, 771 | Jawg: { 772 | url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}', 773 | options: { 774 | attribution: 775 | '© JawgMaps ' + 776 | '{attribution.OpenStreetMap}', 777 | minZoom: 0, 778 | maxZoom: 22, 779 | subdomains: 'abcd', 780 | variant: 'jawg-terrain', 781 | // Get your own Jawg access token here : https://www.jawg.io/lab/ 782 | // NB : this is a demonstration key that comes with no guarantee 783 | accessToken: '', 784 | }, 785 | variants: { 786 | Streets: 'jawg-streets', 787 | Terrain: 'jawg-terrain', 788 | Sunny: 'jawg-sunny', 789 | Dark: 'jawg-dark', 790 | Light: 'jawg-light', 791 | Matrix: 'jawg-matrix' 792 | } 793 | }, 794 | MapBox: { 795 | url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}', 796 | options: { 797 | attribution: 798 | '© Mapbox ' + 799 | '{attribution.OpenStreetMap} ' + 800 | 'Improve this map', 801 | tileSize: 512, 802 | maxZoom: 18, 803 | zoomOffset: -1, 804 | id: 'mapbox/streets-v11', 805 | accessToken: '', 806 | } 807 | }, 808 | MapTiler: { 809 | url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}', 810 | options: { 811 | attribution: 812 | '© MapTiler © OpenStreetMap contributors', 813 | variant: 'streets', 814 | ext: 'png', 815 | key: '', 816 | tileSize: 512, 817 | zoomOffset: -1, 818 | minZoom: 0, 819 | maxZoom: 21 820 | }, 821 | variants: { 822 | Streets: 'streets', 823 | Basic: 'basic', 824 | Bright: 'bright', 825 | Pastel: 'pastel', 826 | Positron: 'positron', 827 | Hybrid: { 828 | options: { 829 | variant: 'hybrid', 830 | ext: 'jpg' 831 | } 832 | }, 833 | Toner: 'toner', 834 | Topo: 'topo', 835 | Voyager: 'voyager' 836 | } 837 | }, 838 | Stamen: { 839 | url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}', 840 | options: { 841 | attribution: 842 | 'Map tiles by Stamen Design, ' + 843 | 'CC BY 3.0 — ' + 844 | 'Map data {attribution.OpenStreetMap}', 845 | subdomains: 'abcd', 846 | minZoom: 0, 847 | maxZoom: 20, 848 | variant: 'toner', 849 | ext: 'png' 850 | }, 851 | variants: { 852 | Toner: 'toner', 853 | TonerBackground: 'toner-background', 854 | TonerHybrid: 'toner-hybrid', 855 | TonerLines: 'toner-lines', 856 | TonerLabels: 'toner-labels', 857 | TonerLite: 'toner-lite', 858 | Watercolor: { 859 | url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}', 860 | options: { 861 | variant: 'watercolor', 862 | ext: 'jpg', 863 | minZoom: 1, 864 | maxZoom: 16 865 | } 866 | }, 867 | Terrain: { 868 | options: { 869 | variant: 'terrain', 870 | minZoom: 0, 871 | maxZoom: 18 872 | } 873 | }, 874 | TerrainBackground: { 875 | options: { 876 | variant: 'terrain-background', 877 | minZoom: 0, 878 | maxZoom: 18 879 | } 880 | }, 881 | TerrainLabels: { 882 | options: { 883 | variant: 'terrain-labels', 884 | minZoom: 0, 885 | maxZoom: 18 886 | } 887 | }, 888 | TopOSMRelief: { 889 | url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}', 890 | options: { 891 | variant: 'toposm-color-relief', 892 | ext: 'jpg', 893 | bounds: [[22, -132], [51, -56]] 894 | } 895 | }, 896 | TopOSMFeatures: { 897 | options: { 898 | variant: 'toposm-features', 899 | bounds: [[22, -132], [51, -56]], 900 | opacity: 0.9 901 | } 902 | } 903 | } 904 | }, 905 | TomTom: { 906 | url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}', 907 | options: { 908 | variant: 'basic', 909 | maxZoom: 22, 910 | attribution: 911 | '© 1992 - ' + new Date().getFullYear() + ' TomTom. ', 912 | subdomains: 'abcd', 913 | style: 'main', 914 | ext: 'png', 915 | apikey: '', 916 | }, 917 | variants: { 918 | Basic: 'basic', 919 | Hybrid: 'hybrid', 920 | Labels: 'labels' 921 | } 922 | }, 923 | Esri: { 924 | url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}', 925 | options: { 926 | variant: 'World_Street_Map', 927 | attribution: 'Tiles © Esri' 928 | }, 929 | variants: { 930 | WorldStreetMap: { 931 | options: { 932 | attribution: 933 | '{attribution.Esri} — ' + 934 | 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' 935 | } 936 | }, 937 | DeLorme: { 938 | options: { 939 | variant: 'Specialty/DeLorme_World_Base_Map', 940 | minZoom: 1, 941 | maxZoom: 11, 942 | attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme' 943 | } 944 | }, 945 | WorldTopoMap: { 946 | options: { 947 | variant: 'World_Topo_Map', 948 | attribution: 949 | '{attribution.Esri} — ' + 950 | 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' 951 | } 952 | }, 953 | WorldImagery: { 954 | options: { 955 | variant: 'World_Imagery', 956 | attribution: 957 | '{attribution.Esri} — ' + 958 | 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' 959 | } 960 | }, 961 | WorldTerrain: { 962 | options: { 963 | variant: 'World_Terrain_Base', 964 | maxZoom: 13, 965 | attribution: 966 | '{attribution.Esri} — ' + 967 | 'Source: USGS, Esri, TANA, DeLorme, and NPS' 968 | } 969 | }, 970 | WorldShadedRelief: { 971 | options: { 972 | variant: 'World_Shaded_Relief', 973 | maxZoom: 13, 974 | attribution: '{attribution.Esri} — Source: Esri' 975 | } 976 | }, 977 | WorldPhysical: { 978 | options: { 979 | variant: 'World_Physical_Map', 980 | maxZoom: 8, 981 | attribution: '{attribution.Esri} — Source: US National Park Service' 982 | } 983 | }, 984 | OceanBasemap: { 985 | options: { 986 | variant: 'Ocean_Basemap', 987 | maxZoom: 13, 988 | attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri' 989 | } 990 | }, 991 | NatGeoWorldMap: { 992 | options: { 993 | variant: 'NatGeo_World_Map', 994 | maxZoom: 16, 995 | attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC' 996 | } 997 | }, 998 | WorldGrayCanvas: { 999 | options: { 1000 | variant: 'Canvas/World_Light_Gray_Base', 1001 | maxZoom: 16, 1002 | attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ' 1003 | } 1004 | } 1005 | } 1006 | }, 1007 | OpenWeatherMap: { 1008 | url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}', 1009 | options: { 1010 | maxZoom: 19, 1011 | attribution: 'Map data © OpenWeatherMap', 1012 | apiKey: '', 1013 | opacity: 0.5 1014 | }, 1015 | variants: { 1016 | Clouds: 'clouds', 1017 | CloudsClassic: 'clouds_cls', 1018 | Precipitation: 'precipitation', 1019 | PrecipitationClassic: 'precipitation_cls', 1020 | Rain: 'rain', 1021 | RainClassic: 'rain_cls', 1022 | Pressure: 'pressure', 1023 | PressureContour: 'pressure_cntr', 1024 | Wind: 'wind', 1025 | Temperature: 'temp', 1026 | Snow: 'snow' 1027 | } 1028 | }, 1029 | HERE: { 1030 | /* 1031 | * HERE maps, formerly Nokia maps. 1032 | * These basemaps are free, but you need an api id and app key. Please sign up at 1033 | * https://developer.here.com/plans 1034 | */ 1035 | url: 1036 | 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' + 1037 | '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + 1038 | 'app_id={app_id}&app_code={app_code}&lg={language}', 1039 | options: { 1040 | attribution: 1041 | 'Map © 1987-' + new Date().getFullYear() + ' HERE', 1042 | subdomains: '1234', 1043 | mapID: 'newest', 1044 | 'app_id': '', 1045 | 'app_code': '', 1046 | base: 'base', 1047 | variant: 'normal.day', 1048 | maxZoom: 20, 1049 | type: 'maptile', 1050 | language: 'eng', 1051 | format: 'png8', 1052 | size: '256' 1053 | }, 1054 | variants: { 1055 | normalDay: 'normal.day', 1056 | normalDayCustom: 'normal.day.custom', 1057 | normalDayGrey: 'normal.day.grey', 1058 | normalDayMobile: 'normal.day.mobile', 1059 | normalDayGreyMobile: 'normal.day.grey.mobile', 1060 | normalDayTransit: 'normal.day.transit', 1061 | normalDayTransitMobile: 'normal.day.transit.mobile', 1062 | normalDayTraffic: { 1063 | options: { 1064 | variant: 'normal.traffic.day', 1065 | base: 'traffic', 1066 | type: 'traffictile' 1067 | } 1068 | }, 1069 | normalNight: 'normal.night', 1070 | normalNightMobile: 'normal.night.mobile', 1071 | normalNightGrey: 'normal.night.grey', 1072 | normalNightGreyMobile: 'normal.night.grey.mobile', 1073 | normalNightTransit: 'normal.night.transit', 1074 | normalNightTransitMobile: 'normal.night.transit.mobile', 1075 | reducedDay: 'reduced.day', 1076 | reducedNight: 'reduced.night', 1077 | basicMap: { 1078 | options: { 1079 | type: 'basetile' 1080 | } 1081 | }, 1082 | mapLabels: { 1083 | options: { 1084 | type: 'labeltile', 1085 | format: 'png' 1086 | } 1087 | }, 1088 | trafficFlow: { 1089 | options: { 1090 | base: 'traffic', 1091 | type: 'flowtile' 1092 | } 1093 | }, 1094 | carnavDayGrey: 'carnav.day.grey', 1095 | hybridDay: { 1096 | options: { 1097 | base: 'aerial', 1098 | variant: 'hybrid.day' 1099 | } 1100 | }, 1101 | hybridDayMobile: { 1102 | options: { 1103 | base: 'aerial', 1104 | variant: 'hybrid.day.mobile' 1105 | } 1106 | }, 1107 | hybridDayTransit: { 1108 | options: { 1109 | base: 'aerial', 1110 | variant: 'hybrid.day.transit' 1111 | } 1112 | }, 1113 | hybridDayGrey: { 1114 | options: { 1115 | base: 'aerial', 1116 | variant: 'hybrid.grey.day' 1117 | } 1118 | }, 1119 | hybridDayTraffic: { 1120 | options: { 1121 | variant: 'hybrid.traffic.day', 1122 | base: 'traffic', 1123 | type: 'traffictile' 1124 | } 1125 | }, 1126 | pedestrianDay: 'pedestrian.day', 1127 | pedestrianNight: 'pedestrian.night', 1128 | satelliteDay: { 1129 | options: { 1130 | base: 'aerial', 1131 | variant: 'satellite.day' 1132 | } 1133 | }, 1134 | terrainDay: { 1135 | options: { 1136 | base: 'aerial', 1137 | variant: 'terrain.day' 1138 | } 1139 | }, 1140 | terrainDayMobile: { 1141 | options: { 1142 | base: 'aerial', 1143 | variant: 'terrain.day.mobile' 1144 | } 1145 | } 1146 | } 1147 | }, 1148 | HEREv3: { 1149 | /* 1150 | * HERE maps API Version 3. 1151 | * These basemaps are free, but you need an API key. Please sign up at 1152 | * https://developer.here.com/plans 1153 | * Version 3 deprecates the app_id and app_code access in favor of apiKey 1154 | * 1155 | * Supported access methods as of 2019/12/21: 1156 | * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services 1157 | */ 1158 | url: 1159 | 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' + 1160 | '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + 1161 | 'apiKey={apiKey}&lg={language}', 1162 | options: { 1163 | attribution: 1164 | 'Map © 1987-' + new Date().getFullYear() + ' HERE', 1165 | subdomains: '1234', 1166 | mapID: 'newest', 1167 | apiKey: '', 1168 | base: 'base', 1169 | variant: 'normal.day', 1170 | maxZoom: 20, 1171 | type: 'maptile', 1172 | language: 'eng', 1173 | format: 'png8', 1174 | size: '256' 1175 | }, 1176 | variants: { 1177 | normalDay: 'normal.day', 1178 | normalDayCustom: 'normal.day.custom', 1179 | normalDayGrey: 'normal.day.grey', 1180 | normalDayMobile: 'normal.day.mobile', 1181 | normalDayGreyMobile: 'normal.day.grey.mobile', 1182 | normalDayTransit: 'normal.day.transit', 1183 | normalDayTransitMobile: 'normal.day.transit.mobile', 1184 | normalNight: 'normal.night', 1185 | normalNightMobile: 'normal.night.mobile', 1186 | normalNightGrey: 'normal.night.grey', 1187 | normalNightGreyMobile: 'normal.night.grey.mobile', 1188 | normalNightTransit: 'normal.night.transit', 1189 | normalNightTransitMobile: 'normal.night.transit.mobile', 1190 | reducedDay: 'reduced.day', 1191 | reducedNight: 'reduced.night', 1192 | basicMap: { 1193 | options: { 1194 | type: 'basetile' 1195 | } 1196 | }, 1197 | mapLabels: { 1198 | options: { 1199 | type: 'labeltile', 1200 | format: 'png' 1201 | } 1202 | }, 1203 | trafficFlow: { 1204 | options: { 1205 | base: 'traffic', 1206 | type: 'flowtile' 1207 | } 1208 | }, 1209 | carnavDayGrey: 'carnav.day.grey', 1210 | hybridDay: { 1211 | options: { 1212 | base: 'aerial', 1213 | variant: 'hybrid.day' 1214 | } 1215 | }, 1216 | hybridDayMobile: { 1217 | options: { 1218 | base: 'aerial', 1219 | variant: 'hybrid.day.mobile' 1220 | } 1221 | }, 1222 | hybridDayTransit: { 1223 | options: { 1224 | base: 'aerial', 1225 | variant: 'hybrid.day.transit' 1226 | } 1227 | }, 1228 | hybridDayGrey: { 1229 | options: { 1230 | base: 'aerial', 1231 | variant: 'hybrid.grey.day' 1232 | } 1233 | }, 1234 | pedestrianDay: 'pedestrian.day', 1235 | pedestrianNight: 'pedestrian.night', 1236 | satelliteDay: { 1237 | options: { 1238 | base: 'aerial', 1239 | variant: 'satellite.day' 1240 | } 1241 | }, 1242 | terrainDay: { 1243 | options: { 1244 | base: 'aerial', 1245 | variant: 'terrain.day' 1246 | } 1247 | }, 1248 | terrainDayMobile: { 1249 | options: { 1250 | base: 'aerial', 1251 | variant: 'terrain.day.mobile' 1252 | } 1253 | } 1254 | } 1255 | }, 1256 | FreeMapSK: { 1257 | url: 'https://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg', 1258 | options: { 1259 | minZoom: 8, 1260 | maxZoom: 16, 1261 | subdomains: 'abcd', 1262 | bounds: [[47.204642, 15.996093], [49.830896, 22.576904]], 1263 | attribution: 1264 | '{attribution.OpenStreetMap}, vizualization CC-By-SA 2.0 Freemap.sk' 1265 | } 1266 | }, 1267 | MtbMap: { 1268 | url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', 1269 | options: { 1270 | attribution: 1271 | '{attribution.OpenStreetMap} & USGS' 1272 | } 1273 | }, 1274 | CartoDB: { 1275 | url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png', 1276 | options: { 1277 | attribution: '{attribution.OpenStreetMap} © CARTO', 1278 | subdomains: 'abcd', 1279 | maxZoom: 19, 1280 | variant: 'light_all' 1281 | }, 1282 | variants: { 1283 | Positron: 'light_all', 1284 | PositronNoLabels: 'light_nolabels', 1285 | PositronOnlyLabels: 'light_only_labels', 1286 | DarkMatter: 'dark_all', 1287 | DarkMatterNoLabels: 'dark_nolabels', 1288 | DarkMatterOnlyLabels: 'dark_only_labels', 1289 | Voyager: 'rastertiles/voyager', 1290 | VoyagerNoLabels: 'rastertiles/voyager_nolabels', 1291 | VoyagerOnlyLabels: 'rastertiles/voyager_only_labels', 1292 | VoyagerLabelsUnder: 'rastertiles/voyager_labels_under' 1293 | } 1294 | }, 1295 | HikeBike: { 1296 | url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png', 1297 | options: { 1298 | maxZoom: 19, 1299 | attribution: '{attribution.OpenStreetMap}', 1300 | variant: 'hikebike' 1301 | }, 1302 | variants: { 1303 | HikeBike: {}, 1304 | HillShading: { 1305 | options: { 1306 | maxZoom: 15, 1307 | variant: 'hillshading' 1308 | } 1309 | } 1310 | } 1311 | }, 1312 | BasemapAT: { 1313 | url: 'https://maps{s}.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}', 1314 | options: { 1315 | maxZoom: 19, 1316 | attribution: 'Datenquelle: basemap.at', 1317 | subdomains: ['', '1', '2', '3', '4'], 1318 | type: 'normal', 1319 | format: 'png', 1320 | bounds: [[46.358770, 8.782379], [49.037872, 17.189532]], 1321 | variant: 'geolandbasemap' 1322 | }, 1323 | variants: { 1324 | basemap: { 1325 | options: { 1326 | maxZoom: 20, // currently only in Vienna 1327 | variant: 'geolandbasemap' 1328 | } 1329 | }, 1330 | grau: 'bmapgrau', 1331 | overlay: 'bmapoverlay', 1332 | terrain: { 1333 | options: { 1334 | variant: 'bmapgelaende', 1335 | type: 'grau', 1336 | format: 'jpeg' 1337 | } 1338 | }, 1339 | surface: { 1340 | options: { 1341 | variant: 'bmapoberflaeche', 1342 | type: 'grau', 1343 | format: 'jpeg' 1344 | } 1345 | }, 1346 | highdpi: { 1347 | options: { 1348 | variant: 'bmaphidpi', 1349 | format: 'jpeg' 1350 | } 1351 | }, 1352 | orthofoto: { 1353 | options: { 1354 | maxZoom: 20, // currently only in Vienna 1355 | variant: 'bmaporthofoto30cm', 1356 | format: 'jpeg' 1357 | } 1358 | } 1359 | } 1360 | }, 1361 | nlmaps: { 1362 | url: 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png', 1363 | options: { 1364 | minZoom: 6, 1365 | maxZoom: 19, 1366 | bounds: [[50.5, 3.25], [54, 7.6]], 1367 | attribution: 'Kaartgegevens © Kadaster' 1368 | }, 1369 | variants: { 1370 | 'standaard': 'brtachtergrondkaart', 1371 | 'pastel': 'brtachtergrondkaartpastel', 1372 | 'grijs': 'brtachtergrondkaartgrijs', 1373 | 'luchtfoto': { 1374 | 'url': 'https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wmts/2018_ortho25/EPSG:3857/{z}/{x}/{y}.png', 1375 | } 1376 | } 1377 | }, 1378 | NASAGIBS: { 1379 | url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', 1380 | options: { 1381 | attribution: 1382 | 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' + 1383 | '(ESDIS) with funding provided by NASA/HQ.', 1384 | bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]], 1385 | minZoom: 1, 1386 | maxZoom: 9, 1387 | format: 'jpg', 1388 | time: '', 1389 | tilematrixset: 'GoogleMapsCompatible_Level' 1390 | }, 1391 | variants: { 1392 | ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor', 1393 | ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367', 1394 | ViirsEarthAtNight2012: { 1395 | options: { 1396 | variant: 'VIIRS_CityLights_2012', 1397 | maxZoom: 8 1398 | } 1399 | }, 1400 | ModisTerraLSTDay: { 1401 | options: { 1402 | variant: 'MODIS_Terra_Land_Surface_Temp_Day', 1403 | format: 'png', 1404 | maxZoom: 7, 1405 | opacity: 0.75 1406 | } 1407 | }, 1408 | ModisTerraSnowCover: { 1409 | options: { 1410 | variant: 'MODIS_Terra_Snow_Cover', 1411 | format: 'png', 1412 | maxZoom: 8, 1413 | opacity: 0.75 1414 | } 1415 | }, 1416 | ModisTerraAOD: { 1417 | options: { 1418 | variant: 'MODIS_Terra_Aerosol', 1419 | format: 'png', 1420 | maxZoom: 6, 1421 | opacity: 0.75 1422 | } 1423 | }, 1424 | ModisTerraChlorophyll: { 1425 | options: { 1426 | variant: 'MODIS_Terra_Chlorophyll_A', 1427 | format: 'png', 1428 | maxZoom: 7, 1429 | opacity: 0.75 1430 | } 1431 | } 1432 | } 1433 | }, 1434 | NLS: { 1435 | // NLS maps are copyright National library of Scotland. 1436 | // http://maps.nls.uk/projects/api/index.html 1437 | // Please contact NLS for anything other than non-commercial low volume usage 1438 | // 1439 | // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s 1440 | // z0-9 - 1:1m 1441 | // z10-11 - quarter inch (1:253440) 1442 | // z12-18 - one inch (1:63360) 1443 | url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', 1444 | options: { 1445 | attribution: 'National Library of Scotland Historic Maps', 1446 | bounds: [[49.6, -12], [61.7, 3]], 1447 | minZoom: 1, 1448 | maxZoom: 18, 1449 | subdomains: '0123', 1450 | } 1451 | }, 1452 | JusticeMap: { 1453 | // Justice Map (http://www.justicemap.org/) 1454 | // Visualize race and income data for your community, county and country. 1455 | // Includes tools for data journalists, bloggers and community activists. 1456 | url: 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png', 1457 | options: { 1458 | attribution: 'Justice Map', 1459 | // one of 'county', 'tract', 'block' 1460 | size: 'county', 1461 | // Bounds for USA, including Alaska and Hawaii 1462 | bounds: [[14, -180], [72, -56]] 1463 | }, 1464 | variants: { 1465 | income: 'income', 1466 | americanIndian: 'indian', 1467 | asian: 'asian', 1468 | black: 'black', 1469 | hispanic: 'hispanic', 1470 | multi: 'multi', 1471 | nonWhite: 'nonwhite', 1472 | white: 'white', 1473 | plurality: 'plural' 1474 | } 1475 | }, 1476 | GeoportailFrance: { 1477 | url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', 1478 | options: { 1479 | attribution: 'Geoportail France', 1480 | bounds: [[-75, -180], [81, 180]], 1481 | minZoom: 2, 1482 | maxZoom: 18, 1483 | // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/ 1484 | // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee 1485 | apikey: 'choisirgeoportail', 1486 | format: 'image/png', 1487 | style: 'normal', 1488 | variant: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' 1489 | }, 1490 | variants: { 1491 | plan: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2', 1492 | parcels: { 1493 | options: { 1494 | variant: 'CADASTRALPARCELS.PARCELLAIRE_EXPRESS', 1495 | style: 'PCI vecteur', 1496 | maxZoom: 20 1497 | } 1498 | }, 1499 | orthos: { 1500 | options: { 1501 | maxZoom: 19, 1502 | format: 'image/jpeg', 1503 | variant: 'ORTHOIMAGERY.ORTHOPHOTOS' 1504 | } 1505 | } 1506 | } 1507 | }, 1508 | OneMapSG: { 1509 | url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png', 1510 | options: { 1511 | variant: 'Default', 1512 | minZoom: 11, 1513 | maxZoom: 18, 1514 | bounds: [[1.56073, 104.11475], [1.16, 103.502]], 1515 | attribution: ' New OneMap | Map data © contributors, Singapore Land Authority' 1516 | }, 1517 | variants: { 1518 | Default: 'Default', 1519 | Night: 'Night', 1520 | Original: 'Original', 1521 | Grey: 'Grey', 1522 | LandLot: 'LandLot' 1523 | } 1524 | }, 1525 | USGS: { 1526 | url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', 1527 | options: { 1528 | maxZoom: 20, 1529 | attribution: 'Tiles courtesy of the U.S. Geological Survey' 1530 | }, 1531 | variants: { 1532 | USTopo: {}, 1533 | USImagery: { 1534 | url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}' 1535 | }, 1536 | USImageryTopo: { 1537 | url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}' 1538 | } 1539 | } 1540 | }, 1541 | WaymarkedTrails: { 1542 | url: 'https://tile.waymarkedtrails.org/{variant}/{z}/{x}/{y}.png', 1543 | options: { 1544 | maxZoom: 18, 1545 | attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © waymarkedtrails.org (CC-BY-SA)' 1546 | }, 1547 | variants: { 1548 | hiking: 'hiking', 1549 | cycling: 'cycling', 1550 | mtb: 'mtb', 1551 | slopes: 'slopes', 1552 | riding: 'riding', 1553 | skating: 'skating' 1554 | } 1555 | }, 1556 | OpenAIP: { 1557 | url: 'http://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.{ext}', 1558 | options: { 1559 | attribution: 'openAIP Data (CC-BY-NC-SA)', 1560 | ext: 'png', 1561 | minZoom: 4, 1562 | maxZoom: 14, 1563 | tms: true, 1564 | detectRetina: true, 1565 | subdomains: '12' 1566 | } 1567 | }, 1568 | OpenSnowMap: { 1569 | url: 'https://tiles.opensnowmap.org/{variant}/{z}/{x}/{y}.png', 1570 | options: { 1571 | minZoom: 9, 1572 | maxZoom: 18, 1573 | attribution: 'Map data: {attribution.OpenStreetMap} & ODbL, © www.opensnowmap.org CC-BY-SA' 1574 | }, 1575 | variants: { 1576 | pistes: 'pistes', 1577 | } 1578 | } 1579 | }; 1580 | 1581 | L.tileLayer.provider = function (provider, options) { 1582 | return new L.TileLayer.Provider(provider, options); 1583 | }; 1584 | 1585 | return L; 1586 | })); 1587 | 1588 | 1589 | /***/ }) 1590 | /******/ ]); 1591 | }); -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-leaflet.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 53 | 54 | 174 | 175 | 176 | 177 |

cytoscape-leaflet demo

178 |
179 |
180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /gh-pages/cytoscape-leaf.css: -------------------------------------------------------------------------------- 1 | ../cytoscape-leaf.css -------------------------------------------------------------------------------- /gh-pages/cytoscape-leaf.js: -------------------------------------------------------------------------------- 1 | ../cytoscape-leaf.js -------------------------------------------------------------------------------- /gh-pages/demo.html: -------------------------------------------------------------------------------- 1 | ../demo.html -------------------------------------------------------------------------------- /gh-pages/index.html: -------------------------------------------------------------------------------- 1 | ../demo.html -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-leaf", 3 | "version": "1.2.4", 4 | "description": "A Cytoscape.js extension for Leaflet.js", 5 | "main": "cytoscape-leaf.js", 6 | "author": "Cytoscape", 7 | "scripts": { 8 | "postpublish": "run-s gh-pages", 9 | "gh-pages": "gh-pages -d gh-pages", 10 | "copyright": "update license", 11 | "lint": "eslint src", 12 | "build": "cross-env NODE_ENV=production webpack", 13 | "build:min": "cross-env NODE_ENV=production MIN=true webpack", 14 | "build:release": "run-s build copyright", 15 | "watch": "webpack --progress --watch", 16 | "dev": "webpack-dev-server --open", 17 | "test": "mocha" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/cytoscape/cytoscape.js-leaflet.git" 22 | }, 23 | "keywords": [ 24 | "cytoscape", 25 | "cytoscape-extension" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/cytoscape/cytoscape.js-leaflet/issues" 30 | }, 31 | "homepage": "https://github.com/cytoscape/cytoscape.js-leaflet", 32 | "devDependencies": { 33 | "babel-core": "^6.24.1", 34 | "babel-loader": "^7.0.0", 35 | "babel-preset-env": "^1.5.1", 36 | "camelcase": "^4.1.0", 37 | "chai": "4.0.2", 38 | "cpy-cli": "^1.0.1", 39 | "cross-env": "^5.0.0", 40 | "eslint": "^4.18.2", 41 | "gh-pages": "^1.0.0", 42 | "mocha": "3.4.2", 43 | "npm-run-all": "^4.1.2", 44 | "rimraf": "^2.6.2", 45 | "update": "^0.7.4", 46 | "updater-license": "^1.0.0", 47 | "webpack": "^2.6.1", 48 | "webpack-dev-server": "^2.4.5" 49 | }, 50 | "peerDependencies": { 51 | "cytoscape": "^3.2.0" 52 | }, 53 | "dependencies": { 54 | "leaflet": "^1.7.1", 55 | "leaflet-providers": "^1.12.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assign.js: -------------------------------------------------------------------------------- 1 | // Simple, internal Object.assign() polyfill for options objects etc. 2 | 3 | module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt, ...srcs) { 4 | srcs.forEach(src => { 5 | if (src !== null && src !== undefined) { 6 | Object.keys(src).forEach(k => tgt[k] = src[k]); 7 | } 8 | }); 9 | 10 | return tgt; 11 | }; 12 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | const CytoscapeLeaflet = require('./cy-leaflet'); 2 | 3 | const coreImpl = function(options) { 4 | const cy = this; 5 | 6 | return new CytoscapeLeaflet(cy, options); 7 | }; 8 | 9 | module.exports = coreImpl; 10 | -------------------------------------------------------------------------------- /src/cy-leaflet.js: -------------------------------------------------------------------------------- 1 | const assign = require('./assign'); 2 | const defaults = require('./defaults'); 3 | 4 | require('leaflet-providers'); 5 | const L = require('leaflet'); 6 | 7 | class CytoscapeLeaflet { 8 | constructor(cy, options) { 9 | this.cy = cy; 10 | this.options = assign({}, defaults, options); 11 | this.L = L; 12 | 13 | this.createMap(); 14 | this.createToggleControl(); 15 | this.configureCy(); 16 | this.addListeners(); 17 | this.onMapViewport(); // set initial node vp state 18 | this.enablePanMode(); // inital mode is pan 19 | } 20 | 21 | destroy() { 22 | this.destroyMap(); 23 | this.destroyToggleControl(); 24 | this.resetCyConfig(); 25 | this.removeListeners(); 26 | } 27 | 28 | getNodeLatLng(n) { 29 | const LAT = this.options.latitude; 30 | const LNG = this.options.longitude; 31 | 32 | const data = n.data(); 33 | const lat = data[LAT]; 34 | const lng = data[LNG]; 35 | 36 | return L.latLng(lat, lng); 37 | } 38 | 39 | createMap() { 40 | const cy = this.cy; 41 | const lCtr = this.options.container; 42 | 43 | const map = this.map = L.map(lCtr, { 44 | zoomControl: false, 45 | zoomSnap: 0 46 | }); 47 | 48 | // fallback init vp state 49 | map.setView([43.83155486662417, -79.37278747558595], 10); 50 | 51 | this.fit(); 52 | 53 | this.defaultTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png', { 54 | attribution: '© OpenStreetMap contributors © CARTO', 55 | subdomains: 'abcd', 56 | maxZoom: 19 57 | }).addTo(map); 58 | } 59 | 60 | destroyMap() { 61 | this.map.remove(); 62 | } 63 | 64 | createToggleControl() { 65 | const container = document.createElement('div'); 66 | const panButton = document.createElement('div'); 67 | const editButton = document.createElement('div'); 68 | 69 | container.classList.add('cy-leaflet-toggle'); 70 | panButton.classList.add('cy-leaflet-toggle-button'); 71 | panButton.classList.add('cy-leaflet-toggle-pan'); 72 | editButton.classList.add('cy-leaflet-toggle-button'); 73 | editButton.classList.add('cy-leaflet-toggle-edit'); 74 | 75 | container.appendChild(panButton); 76 | container.appendChild(editButton); 77 | 78 | this.options.container.parentNode.appendChild(container); 79 | 80 | this.toggleContainer = container; 81 | this.panButton = panButton; 82 | this.editButton = editButton; 83 | } 84 | 85 | destroyToggleControl() { 86 | this.toggleContainer.parentNode.removeChild(this.toggleContainer); 87 | } 88 | 89 | configureCy() { 90 | this.orgZoomEnabled = this.cy.zoomingEnabled(); 91 | this.orgPanningEnabled = this.cy.panningEnabled(); 92 | 93 | this.cy.zoomingEnabled(false); 94 | this.cy.panningEnabled(false); 95 | 96 | this.cy.container().style.pointerEvents = 'none'; 97 | } 98 | 99 | resetCyConfig() { 100 | this.cy.zoomingEnabled(this.orgZoomEnabled); 101 | this.cy.panningEnabled(this.orgPanningEnabled); 102 | 103 | this.cy.container().style.pointerEvents = ''; 104 | } 105 | 106 | addListeners() { 107 | this.onViewport = () => { 108 | const cy = this.cy; 109 | 110 | cy.zoomingEnabled(true); 111 | cy.panningEnabled(true); 112 | 113 | cy.zoom(1); 114 | cy.pan({ x: 0, y: 0 }); 115 | 116 | cy.zoomingEnabled(false); 117 | cy.panningEnabled(false); 118 | 119 | cy.nodes().forEach(node => this.setNodePosition(node)); 120 | }; 121 | 122 | this.onEnablePan = () => { this.enablePanMode(); }; 123 | 124 | this.onEnabledEdit = () => { this.enableEditMode(); }; 125 | 126 | this.onDrag = (e) => { 127 | const node = e.target; 128 | 129 | this.updateNodeGeo(node); 130 | }; 131 | 132 | this.updateNodePositionFromCyEvent = e => { 133 | const ele = e.target; 134 | 135 | if (ele.isNode()) { 136 | this.setNodePosition(ele); 137 | } 138 | }; 139 | 140 | this.map.on('zoom', this.onViewport); 141 | this.map.on('move', this.onViewport); 142 | 143 | this.onViewportStart = () => { 144 | this.cy.elements().addClass('leaflet-viewport'); 145 | } 146 | 147 | this.onMoveTimeout = null; 148 | 149 | this.onViewportEnd = () => { 150 | clearTimeout(this.onMoveTimeout); 151 | 152 | this.onMoveTimeout = setTimeout(() => { 153 | this.cy.elements().removeClass('leaflet-viewport'); 154 | }, 250); 155 | } 156 | 157 | this.map.on('movestart', this.onViewportStart); 158 | this.map.on('moveend', this.onViewportEnd); 159 | this.map.on('zoomstart', this.onViewportStart); 160 | this.map.on('zoomend', this.onViewportEnd); 161 | 162 | this.panButton.addEventListener('click', this.onEnablePan); 163 | this.editButton.addEventListener('click', this.onEnabledEdit); 164 | 165 | this.cy.on('drag', this.onDrag); 166 | 167 | this.cy.on('add', this.updateNodePositionFromCyEvent); 168 | this.cy.on('data', this.updateNodePositionFromCyEvent); 169 | 170 | window.addEventListener('keyup', this.onToggleShortcut = (event) => { 171 | switch (event.keyCode) { 172 | case 17: // control 173 | //case 93: // meta/apple/command 174 | //case 16: // shift 175 | this.toggleMode(); 176 | default: 177 | break; 178 | } 179 | }); 180 | 181 | this.cy.container().addEventListener('wheel', this.onWheel = (event) => { 182 | event.preventDefault(); 183 | }); 184 | } 185 | 186 | removeListeners() { 187 | this.map.off('zoom', this.onViewport); 188 | this.map.off('move', this.onViewport); 189 | 190 | this.panButton.removeEventListener('click', this.onEnablePan); 191 | this.editButton.removeEventListener('click', this.onEnabledEdit); 192 | 193 | this.cy.removeListener('drag', this.onDrag); 194 | 195 | this.cy.removeListener('add', this.updateNodePositionFromCyEvent); 196 | this.cy.removeListener('data', this.updateNodePositionFromCyEvent); 197 | 198 | window.removeEventListener('keyup', this.onToggleShortcut); 199 | 200 | this.cy.container().removeEventListener('wheel', this.onWheel); 201 | } 202 | 203 | setNodePosition(node) { 204 | const latlng = this.getNodeLatLng(node); 205 | const pt = this.map.latLngToContainerPoint(latlng); 206 | const { x, y } = pt; 207 | 208 | node.position({ x, y }); 209 | } 210 | 211 | updateNodeGeo(node) { 212 | const { x, y } = node.position(); 213 | const pt = L.point(x, y); 214 | const latlng = this.map.containerPointToLatLng(pt); 215 | const llObj = {}; 216 | 217 | const LAT = this.options.latitude; 218 | const LNG = this.options.longitude; 219 | 220 | llObj[LAT] = latlng.lat; 221 | llObj[LNG] = latlng.lng; 222 | 223 | node.data(llObj); 224 | } 225 | 226 | onMapViewport() { 227 | const cy = this.cy; 228 | 229 | cy.zoomingEnabled(true); 230 | cy.panningEnabled(true); 231 | 232 | cy.zoom(1); 233 | cy.pan({ x: 0, y: 0 }); 234 | 235 | cy.zoomingEnabled(false); 236 | cy.panningEnabled(false); 237 | 238 | cy.nodes().forEach(node => this.setNodePosition(node)); 239 | } 240 | 241 | setZoomControlOpacity(opacity) { 242 | const zoomControl = document.querySelector('.leaflet-control-zoom'); 243 | if (zoomControl) { 244 | zoomControl.style.opacity = opacity; 245 | } 246 | } 247 | 248 | enablePanMode() { 249 | this.panMode = true; 250 | this.editMode = false; 251 | 252 | this.panButton.classList.add('cy-leaflet-toggle-active'); 253 | this.editButton.classList.remove('cy-leaflet-toggle-active'); 254 | 255 | this.cy.container().style.pointerEvents = 'none'; 256 | 257 | this.setZoomControlOpacity(""); 258 | 259 | this.map.dragging.enable(); 260 | } 261 | 262 | enableEditMode() { 263 | this.panMode = false; 264 | this.editMode = true; 265 | 266 | this.panButton.classList.remove('cy-leaflet-toggle-active'); 267 | this.editButton.classList.add('cy-leaflet-toggle-active'); 268 | 269 | this.cy.container().style.pointerEvents = ''; 270 | 271 | this.setZoomControlOpacity(0.5); 272 | 273 | this.map.dragging.disable(); 274 | } 275 | 276 | toggleMode() { 277 | if (this.panMode) { 278 | this.enableEditMode(); 279 | } else { 280 | this.enablePanMode(); 281 | } 282 | } 283 | 284 | fit() { 285 | const { cy, map } = this; 286 | const getGeo = n => this.getNodeLatLng(n); 287 | 288 | if (cy.nodes().nonempty()) { 289 | const bounds = L.latLngBounds(cy.nodes().map(getGeo)); 290 | 291 | map.fitBounds(bounds, { padding: [50, 50] }); 292 | } 293 | } 294 | } 295 | 296 | module.exports = CytoscapeLeaflet; 297 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | container: undefined, // the container in which the map is put 3 | latitude: 'lat', 4 | longitude: 'lng' 5 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* global cytoscape */ 2 | 3 | const coreImpl = require('./core'); 4 | 5 | // registers the extension on a cytoscape lib ref 6 | let register = function (cytoscape) { 7 | if (!cytoscape) { return; } // can't register if cytoscape unspecified 8 | 9 | // register with cytoscape.js 10 | cytoscape('core', 'leaflet', coreImpl); 11 | }; 12 | 13 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape) 14 | register(cytoscape); 15 | } 16 | 17 | module.exports = register; 18 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | describe('This', function(){ 4 | it('does that', function(){ 5 | expect( true ).to.be.true; 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const camelcase = require('camelcase'); 4 | const process = require('process'); 5 | const webpack = require('webpack'); 6 | const env = process.env; 7 | const NODE_ENV = env.NODE_ENV; 8 | const MIN = env.MIN; 9 | const PROD = NODE_ENV === 'production'; 10 | 11 | let config = { 12 | devtool: PROD ? false : 'inline-source-map', 13 | entry: './src/index.js', 14 | output: { 15 | path: path.join( __dirname ), 16 | filename: pkg.name + '.js', 17 | library: camelcase( pkg.name ), 18 | libraryTarget: 'umd' 19 | }, 20 | module: { 21 | rules: [ 22 | { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' } 23 | ] 24 | }, 25 | externals: { 26 | "leaflet" : { 27 | commonjs: "leaflet", 28 | commonjs2: "leaflet", 29 | amd: "leaflet", 30 | root: "L" 31 | } 32 | }, 33 | plugins: MIN ? [ 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false, 37 | drop_console: false, 38 | } 39 | }) 40 | ] : [] 41 | }; 42 | 43 | module.exports = config; 44 | --------------------------------------------------------------------------------