├── .eslintrc ├── .gitignore ├── .gitmodules ├── .tx └── config ├── CHANGELOG.md ├── Gruntfile.js ├── LICENCE ├── Makefile ├── README.rst ├── contrib ├── css │ └── storage.ui.default.css └── js │ ├── storage.ui.default.js │ └── storage.ui.foundation.js ├── package.json ├── src ├── css │ └── storage.css ├── img │ ├── 16-white.png │ ├── 16-white.svg │ ├── 16.png │ ├── 16.svg │ ├── 24-white.png │ ├── 24-white.svg │ ├── 24.png │ ├── 24.svg │ ├── edit-16.png │ ├── icon-bg.png │ ├── marker.png │ └── search.gif ├── js │ ├── leaflet.storage.controls.js │ ├── leaflet.storage.core.js │ ├── leaflet.storage.features.js │ ├── leaflet.storage.forms.js │ ├── leaflet.storage.icon.js │ ├── leaflet.storage.js │ ├── leaflet.storage.layer.js │ ├── leaflet.storage.popup.js │ ├── leaflet.storage.slideshow.js │ ├── leaflet.storage.tableeditor.js │ └── leaflet.storage.xhr.js └── locale │ ├── am_ET.json │ ├── bg.json │ ├── ca.json │ ├── cs_CZ.json │ ├── da.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fi.json │ ├── fr.json │ ├── it.json │ ├── ja.json │ ├── lt.json │ ├── nl.json │ ├── pl.json │ ├── pt.json │ ├── ru.json │ ├── sk_SK.json │ ├── uk_UA.json │ ├── vi.json │ ├── zh.json │ └── zh_TW.json └── test ├── .eslintrc ├── Controls.js ├── DataLayer.js ├── Feature.js ├── Map.js ├── Polygon.js ├── Polyline.js ├── TableEditor.js ├── Util.js ├── _pre.js └── index.html /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "quotes": [2, "single"], 7 | "no-underscore-dangle": 0, 8 | "curly": 0, 9 | "consistent-return": 0, 10 | "new-cap": 0, 11 | "strict": [2, "global"], 12 | "semi-spacing": 0 13 | }, 14 | "globals": {L: true} 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | *.lock 5 | *.pid 6 | *.DS_Store 7 | *.svn 8 | *.pdg 9 | *.orig 10 | *~ 11 | doc/_build/* 12 | node_modules/* 13 | reqs/* 14 | npm-debug.log 15 | *.bk 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/.gitmodules -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [umap.frontend] 5 | file_filter = src/locale/.json 6 | source_file = src/locale/en.json 7 | source_lang = en 8 | type = KEYVALUEJSON 9 | 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Leaflet.Storage changelog 2 | 3 | ## dev 4 | - allow colon in properties to be consumed in popupTemplate 5 | 6 | ## 0.7.5 7 | - upgrade osmtogeojson to 2.1.0 8 | - localize and proxy dataUrl parameter 9 | 10 | ## 0.7.3 11 | - add tooltip when drawing 12 | - import multiple files at a time 13 | - added Chinese (Taiwan) locale 14 | - fixed right-click on path vertex not working propertly when editing 15 | 16 | ## 0.7.1 17 | - upgrade Leaflet.Editable to 0.2.0 18 | - fixed some bugs after Leaflet.Editable switch 19 | 20 | ## 0.7.0 21 | - introduce panel popup mode 22 | - upgraded leaflet.loading to 0.1.10 23 | - make the cluster text color dynamic 24 | - fix missing icons for transorm to polygon/polyline actions 25 | - add a slideshow mode 26 | - make possible to set cluster color by hand 27 | - make possible to manage showLabel from layer and map 28 | - basic kml/gpx download support 29 | - MultiLineString are merged at import 30 | - catch setMaxBounds errors (when using useless bounds) 31 | - first version of a table editor 32 | - it's now possible to cancel every mouse action of a polygon 33 | (useful when using them as background) 34 | - simple custom popup templates 35 | - more control over map data attribution (custom inputs added) 36 | - basic HTTP optimistic concurrency control 37 | - add "empty" button in limit bounds fieldset 38 | - make possible to decide which properties the data browser will filter on 39 | - add "datalayers" query string parameter to override shown datalayers on map load 40 | - add edit fieldset for changing marker latlng by hand 41 | - moved from Leaflet.Draw to Leaflet.Editable 42 | 43 | ## 0.6.x 44 | - add TMS option to custom tilelayer 45 | - allow to define default properties at map level 46 | - support iframe in text formatting 47 | - fix bug where polygon export were adding a point 48 | - make that only visible elements are downloaded 49 | - iframe export helper 50 | - add Leaflet.label (for marker only atm) 51 | - GeoRSS support 52 | - heatmap support, thanks to https://github.com/Leaflet/Leaflet.heat 53 | - added optional caption bar 54 | - added new "large" popup template 55 | - added a button to empty a layer without deleting it 56 | - added a button to clone a datalayer 57 | - added dataUrl and dataFormat on map creation page 58 | - basic support for GeometryCollection import 59 | - removed submodules and switched to grunt for assets management 60 | 61 | ## 0.5.x 62 | - datalayers are now sent to backend as geojson 63 | - there is now a global "save" button, and also a "cancel changes" 64 | - added a contextmenu, thanks to https://github.com/aratcliffe/Leaflet.contextmenu 65 | - added a loader, thanks to https://github.com/ebrelsford/Leaflet.loading 66 | - import are processed client side, thanks to https://github.com/mapbox/csv2geojson 67 | and https://github.com/mapbox/togeojson 68 | - download is handled client side 69 | - option "outlink" as been added, to open external URL on polygon click 70 | - edit shortcuts has been added (Ctrl-E to toggle edit status, Ctrl-S to save, etc.) 71 | - links in popup now open in a now window 72 | - possibility to add custom icon symbols 73 | - new option to clusterize markers, thanks to https://github.com/Leaflet/Leaflet.markercluster 74 | - remote data option added to datalayer: this will fetch data from a given URL 75 | instead of from the local database 76 | - popup window can now display a table with all features properties 77 | - support of OSM XML format, thanks to https://github.com/tyrasd/osmtogeojson 78 | - added a measure control, thanks to https://github.com/makinacorpus/Leaflet.MeasureControl 79 | - added Transifex config 80 | - simple help boxes 81 | - it's now possible to set background layer with manual settings 82 | - add an edit button in the data browser (when in edit mode) 83 | - add icon URL formatting with feature properties 84 | - add "Transform to Polygon/Polyline" action 85 | - new link on contextmenu to open external routing service from clicked point 86 | - fix bug where features were duplicated when datalayer was deleted then reverted 87 | - add layer action to databrowser 88 | - add optional default CSS 89 | - allow to close panel by ctrl-Enter when editing in textarea 90 | - add management for map max bounds 91 | - add Ctrl-Z for canceling changes 92 | 93 | ## 0.4.x 94 | - add a data browser 95 | - add a popup footer with navigation between features 96 | - some work on IE compat 97 | - new tilelayer visual switcher 98 | - Spanish translation, thanks to @ikks 99 | 100 | ## 0.3.x 101 | 102 | - add a setting to display map caption on map load (cf #50) 103 | - add nl translation 104 | - update to Leaflet 0.6-dev and Leaflet.Draw 0.2 105 | 106 | 107 | ## 0.2.0 108 | 109 | - handle auth from popup 110 | - add a control for map settings management 111 | - move to Leaflet 0.5 112 | - move to Leaflet.draw 0.1.6 113 | - default tooltip has now a fixed position 114 | - make just drown polys editable 115 | - handle path styling option (https://github.com/yohanboniface/Leaflet.Storage/issues/26) 116 | - add an UI to manage icon style and picto (https://github.com/yohanboniface/django-leaflet-storage/issues/22) 117 | - icon style and picto are now manageable also on Markers (https://github.com/yohanboniface/django-leaflet-storage/issues/21) 118 | - add Leaflet.EditInOSM plugin in options 119 | - add a scale control (optional) 120 | - add an optional minimap (with Leaflet.MiniMap plugin) 121 | 122 | ## 0.1.0 123 | 124 | - first packaged version 125 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | 9 | copy: { 10 | main: { 11 | files: [ 12 | {expand: true, cwd: 'node_modules/leaflet/dist/', src: ['**'], dest: 'reqs/leaflet/'}, 13 | {expand: true, cwd: 'node_modules/leaflet-editable/src/', src: ['*.js'], dest: 'reqs/editable/'}, 14 | {expand: true, cwd: 'node_modules/leaflet.path.drag/src/', src: ['*.js'], dest: 'reqs/editable/'}, 15 | {expand: true, cwd: 'node_modules/leaflet-hash/', src: ['*.js'], dest: 'reqs/hash/'}, 16 | {expand: true, cwd: 'node_modules/leaflet-i18n/', src: ['*.js'], dest: 'reqs/i18n/'}, 17 | {expand: true, cwd: 'node_modules/leaflet-editinosm/', src: ['*.js', '*.css'], dest: 'reqs/editinosm/'}, 18 | {expand: true, cwd: 'node_modules/leaflet-minimap/src/', src: ['**'], dest: 'reqs/minimap/'}, 19 | {expand: true, cwd: 'node_modules/leaflet-loading/src/', src: ['**'], dest: 'reqs/loading/'}, 20 | {expand: true, cwd: 'node_modules/leaflet.markercluster/dist/', src: ['**'], dest: 'reqs/markercluster/'}, 21 | {expand: true, cwd: 'node_modules/leaflet-contextmenu/dist/', src: ['**'], dest: 'reqs/contextmenu/'}, 22 | {expand: true, cwd: 'node_modules/leaflet.heat/dist/', src: ['**'], dest: 'reqs/heat/'}, 23 | {expand: true, cwd: 'node_modules/leaflet-fullscreen/dist/', src: ['**'], dest: 'reqs/fullscreen/'}, 24 | {expand: true, cwd: 'node_modules/leaflet-toolbar/dist/', src: ['**'], dest: 'reqs/toolbar/'}, 25 | {expand: true, cwd: 'node_modules/leaflet-formbuilder/', src: ['*.js'], dest: 'reqs/formbuilder/'}, 26 | {expand: true, cwd: 'node_modules/leaflet-measurable/', src: ['*.js', '*.css'], dest: 'reqs/measurable/'}, 27 | {expand: true, cwd: 'node_modules/leaflet.photon/', src: ['*.js'], dest: 'reqs/photon/'}, 28 | {expand: true, cwd: 'node_modules/csv2geojson/', src: ['*.js'], dest: 'reqs/csv2geojson/'}, 29 | {expand: true, cwd: 'node_modules/togeojson/', src: ['*.js'], dest: 'reqs/togeojson/'}, 30 | {expand: true, cwd: 'node_modules/osmtogeojson/', src: ['osmtogeojson.js'], dest: 'reqs/osmtogeojson/'}, 31 | {expand: true, cwd: 'node_modules/georsstogeojson/', src: ['GeoRSSToGeoJSON.js'], dest: 'reqs/georsstogeojson/'}, 32 | {expand: true, cwd: 'node_modules/togpx/', src: ['togpx.js'], dest: 'reqs/togpx/'}, 33 | {expand: true, cwd: 'node_modules/tokml/', src: ['tokml.js'], dest: 'reqs/tokml/'} 34 | ] 35 | } 36 | } 37 | 38 | }); 39 | 40 | grunt.loadNpmTasks('grunt-contrib-copy'); 41 | 42 | // Default task(s). 43 | grunt.registerTask('default', ['copy']); 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2013 Yohan Boniface 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | install: 3 | npm install 4 | vendors: 5 | grunt 6 | testfx: 7 | firefox test/index.html 8 | test: node_modules 9 | @./node_modules/mocha-phantomjs/bin/mocha-phantomjs --view 1024x768 test/index.html 10 | i18n: 11 | node node_modules/leaflet-i18n/bin/i18n.js --dir_path=src/js/ --dir_path=reqs/measurable/ --locale_dir_path=src/locale/ --locale_codes=en --mode=json --clean --default_values 12 | tx_push: 13 | tx push -s 14 | tx_pull: 15 | tx pull 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Archived: see https://github.com/umap-project/umap 2 | 3 | =============== 4 | Leaflet-Storage 5 | =============== 6 | 7 | Manage map and features with Leaflet and expose them for backend storage with an API. 8 | 9 | ------------------- 10 | Feedback and issues 11 | ------------------- 12 | 13 | Please use uMap issues: https://github.com/umap-project/umap/issues 14 | 15 | 16 | ---------------- 17 | Backend agnostic 18 | ---------------- 19 | 20 | Leaflet.Storage is backend agnostic: it only knows about a convention API. 21 | 22 | Known backends: 23 | 24 | - `django-leaflet-storage `_ 25 | 26 | 27 | ================ 28 | Functional tests 29 | ================ 30 | 31 | Functional tests are implemented with `mocha `_, 32 | `chai `_ and `sinon `_. 33 | 34 | To launch them:: 35 | 36 | cd Leaflet.Storage/ 37 | make test 38 | 39 | ================ 40 | Show me an image 41 | ================ 42 | 43 | .. image:: http://i.imgur.com/vOllwf6.png 44 | -------------------------------------------------------------------------------- /contrib/css/storage.ui.default.css: -------------------------------------------------------------------------------- 1 | div, ul, li, a, section, nav, 2 | h1, h2, h3, h4, h5, h6, label, 3 | hr, input, textarea { 4 | -moz-box-sizing: border-box; 5 | -webkit-box-sizing: border-box; 6 | box-sizing: border-box; 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | /* *********** */ 12 | /* forms */ 13 | /* *********** */ 14 | input[type="text"], input[type="password"], input[type="date"], 15 | input[type="datetime"], input[type="email"], input[type="number"], 16 | input[type="search"], input[type="tel"], input[type="time"], 17 | input[type="url"], textarea { 18 | background-color: white; 19 | border: 1px solid #CCCCCC; 20 | border-radius: 2px 2px 2px 2px; 21 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; 22 | color: rgba(0, 0, 0, 0.75); 23 | display: block; 24 | font-family: inherit; 25 | font-size: 14px; 26 | height: 32px; 27 | margin: 0 0 14px; 28 | padding: 7px; 29 | width: 100%; 30 | } 31 | input[type="range"] { 32 | margin-top: 10px; 33 | margin-bottom: 5px; 34 | width: 100%; 35 | } 36 | input[type="checkbox"] { 37 | margin: 0 5px; 38 | vertical-align: middle; 39 | } 40 | textarea { 41 | height: inherit; 42 | padding: 7px; 43 | } 44 | select { 45 | width: 100%; 46 | height: 28px; 47 | line-height: 28px; 48 | color: #efefef; 49 | border: 1px solid #222; 50 | background-color: #393F3F; 51 | margin-top: 5px; 52 | } 53 | select[multiple="multiple"] { 54 | height: auto; 55 | } 56 | .button, input[type="submit"] { 57 | display: block; 58 | margin-bottom: 14px; 59 | text-align: center; 60 | border-radius: 2px; 61 | font-weight: normal; 62 | cursor: pointer; 63 | padding: 7px; 64 | width: 100%; 65 | min-height: 32px; 66 | line-height: 32px; 67 | border: none; 68 | text-decoration: none; 69 | } 70 | .dark .button { 71 | background-color: #2a2e30; 72 | color: #eeeeec; 73 | border: 1px solid #1b1f20; 74 | } 75 | .dark .button:hover, .dark input[type="submit"]:hover { 76 | background-color: #2e3436; 77 | } 78 | .help-text, .helptext { 79 | display: block; 80 | padding: 7px 7px; 81 | margin-bottom: 14px; 82 | background: #393F3F; 83 | color: #ddd; 84 | font-size: 10px; 85 | border-radius: 0 2px; 86 | } 87 | input + .help-text { 88 | margin-top: -14px; 89 | } 90 | .formbox { 91 | min-height: 36px; 92 | line-height: 28px; 93 | margin-bottom: 14px; 94 | } 95 | .formbox.with-switch { 96 | padding-top: 2px; 97 | } 98 | .formbox select { 99 | width: calc(100% - 14px); 100 | } 101 | label { 102 | display: block; 103 | font-size: 12px; 104 | line-height: 21px; 105 | width: 100%; 106 | } 107 | input[type="checkbox"] + label { 108 | display: inline; 109 | padding: 0 14px; 110 | } 111 | select + .error, 112 | input + .error { 113 | display: block; 114 | padding: 7px 7px; 115 | margin-top: -14px; 116 | margin-bottom: 14px; 117 | background: #ddd; 118 | color: #fff; 119 | background-color: #cc0000; 120 | font-size: 11px; 121 | border-radius: 0 2px; 122 | } 123 | input[type="file"] + .error { 124 | margin-top: 0; 125 | } 126 | .fieldset { 127 | border: 1px solid #222; 128 | margin-bottom: 5px; 129 | border-top-left-radius: 4px; 130 | border-top-right-radius: 4px; 131 | } 132 | .fieldset .fields { 133 | visibility: hidden; 134 | opacity: 0; 135 | transition: visibility 0s, opacity 0.5s linear; 136 | height: 0; 137 | overflow: hidden; 138 | } 139 | .fieldset.toggle.on .fields { 140 | visibility: visible; 141 | opacity: 1; 142 | height: initial; 143 | padding: 10px; 144 | } 145 | .fieldset.toggle .legend { 146 | text-align: center; 147 | display: block; 148 | cursor: pointer; 149 | background-color: #232729; 150 | height: 30px; 151 | line-height: 30px; 152 | color: #fff; 153 | margin: 0; 154 | font-family: fira_sanslight; 155 | font-size: 1.2em; 156 | padding: 0 5px; 157 | } 158 | /* Switch */ 159 | input.switch:empty { 160 | display: none; 161 | } 162 | input.switch:empty ~ label { 163 | white-space: nowrap; 164 | position: relative; 165 | float: left; 166 | line-height: 2em; 167 | height: 2em; 168 | text-indent: 6em; 169 | margin: 0.2em 0; 170 | cursor: pointer; 171 | -webkit-user-select: none; 172 | -moz-user-select: none; 173 | -ms-user-select: none; 174 | user-select: none; 175 | text-shadow: 0 1px rgba(0, 0, 0, 0.1); 176 | width: 80px; 177 | } 178 | input.switch:empty ~ label:before, 179 | input.switch:empty ~ label:after { 180 | position: absolute; 181 | display: block; 182 | top: 0; 183 | bottom: 0; 184 | left: 0; 185 | content: ' '; 186 | width: 6em; 187 | -webkit-transition: all 100ms ease-in; 188 | transition: all 100ms ease-in; 189 | color: #c9c9c7; 190 | font-weight: bold; 191 | background-color: #ededed; 192 | } 193 | .dark input.switch:empty ~ label:before, 194 | .dark input.switch:empty ~ label:after { 195 | background-color: #272c2e; 196 | } 197 | input.switch:empty ~ label:after { 198 | width: 3em; 199 | margin-left: 0.1em; 200 | background-color: #ededed; 201 | content: "OFF"; 202 | text-indent: 3.5em; 203 | border: 1px solid #374E75; 204 | font-weight: bold; 205 | } 206 | .dark input.switch:empty ~ label:after { 207 | border: 1px solid #202425; 208 | background-color: #2c3233; 209 | } 210 | input.switch:checked:empty ~ label:after { 211 | content: ' '; 212 | } 213 | .dark input.switch:checked ~ label:before, 214 | input.switch:checked ~ label:before { 215 | background-color: #215d9c; 216 | content: "ON"; 217 | text-indent: 0.7em; 218 | text-align: left; 219 | font-weight: bold; 220 | } 221 | input.switch:checked ~ label:after { 222 | margin-left: 3em; 223 | } 224 | .button-bar { 225 | margin-top: 5px; 226 | } 227 | .storage-multiplechoice input[type='radio'] { 228 | display: none; 229 | } 230 | .storage-multiplechoice label { 231 | border: 1px solid #374E75; 232 | cursor: pointer; 233 | background-color: #c9c9c7; 234 | height: 30px; 235 | line-height: 30px; 236 | text-align: center; 237 | width: calc(100% / 3); 238 | display: inline-block; 239 | } 240 | .storage-multiplechoice.by4 label { 241 | width: calc(100% / 4); 242 | } 243 | .dark .storage-multiplechoice label { 244 | border: 1px solid black; 245 | background-color: #2c3233; 246 | } 247 | .storage-multiplechoice input[type='radio']:checked + label { 248 | background-color: #215d9c; 249 | box-shadow: inset 0 0 6px 0px #2c3233; 250 | color: #ededed; 251 | } 252 | .inheritable .header, 253 | .inheritable { 254 | clear: both; 255 | overflow: hidden; 256 | } 257 | .inheritable .header { 258 | margin-bottom: 5px; 259 | } 260 | .inheritable .header label { 261 | padding-top: 6px; 262 | } 263 | .inheritable + .inheritable { 264 | border-top: 1px solid #222; 265 | padding-top: 5px; 266 | margin-top: 5px; 267 | } 268 | .inheritable .define, 269 | .inheritable .undefine { 270 | float: right; 271 | width: initial; 272 | min-height: 18px; 273 | line-height: 18px; 274 | margin-bottom: 0; 275 | } 276 | .inheritable .quick-actions { 277 | float: right; 278 | } 279 | .inheritable .quick-actions .formbox { 280 | margin-bottom: 0; 281 | } 282 | .inheritable .quick-actions input { 283 | width: 100px; 284 | margin-right: 5px; 285 | } 286 | .inheritable .define, 287 | .inheritable.undefined .undefine, 288 | .inheritable.undefined .show-on-defined { 289 | display: none; 290 | } 291 | .inheritable.undefined .define { 292 | display: block; 293 | } 294 | i.info { 295 | background-repeat: no-repeat; 296 | background-image: url("../../src/img/16.png"); 297 | background-position: -170px -50px; 298 | display: inline-block; 299 | margin-left: 5px; 300 | vertical-align: middle; 301 | width: 16px; 302 | height: 18px; 303 | } 304 | .dark i.info { 305 | background-image: url("../../src/img/16-white.png"); 306 | } 307 | .with-transition { 308 | /*transition: top .7s, right .7s, left .7s, width .7s, visibility .7s;*/ 309 | transition: all .7s; 310 | } 311 | 312 | 313 | 314 | /* *********** */ 315 | /* Panel */ 316 | /* *********** */ 317 | .leaflet-ui-container { 318 | overflow-x: hidden; 319 | } 320 | #storage-ui-container { 321 | width: 400px; 322 | position: fixed; 323 | top: 0; 324 | bottom: 0; 325 | right: -400px; 326 | padding: 0 20px 40px 20px; 327 | border-left: 1px solid #ddd; 328 | overflow-x: auto; 329 | z-index: 1010; 330 | background-color: #fff; 331 | opacity: 0.98; 332 | cursor: initial; 333 | } 334 | #storage-ui-container.dark { 335 | border-left: 1px solid #222; 336 | background-color: #323737; 337 | color: #efefef; 338 | } 339 | #storage-ui-container.fullwidth { 340 | width: 100%; 341 | z-index: 10000; 342 | padding-left: 0; 343 | padding-right: 0; 344 | transition: all .7s; 345 | } 346 | .storage-edit-enabled #storage-ui-container { 347 | top: 46px; 348 | } 349 | .storage-caption-bar-enabled #storage-ui-container { 350 | bottom: 46px; 351 | } 352 | .storage-ui #storage-ui-container { 353 | right: 0; 354 | } 355 | .leaflet-top, 356 | .leaflet-right { 357 | transition: all .7s; 358 | } 359 | .storage-ui .leaflet-right { 360 | right: 400px; 361 | } 362 | #storage-ui-container, 363 | #storage-alert-container, 364 | #storage-tooltip-container { 365 | -moz-box-sizing:border-box; 366 | -webkit-box-sizing:border-box; 367 | box-sizing: border-box; 368 | } 369 | #storage-ui-container .storage-popup-content img { 370 | /* See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847 */ 371 | max-width: 99% !important; 372 | } 373 | #storage-ui-container .storage-popup-content { 374 | max-height: inherit; 375 | } 376 | #storage-ui-container .body { 377 | clear: both; 378 | height: calc(100% - 46px); /* Minus size of toolbox */ 379 | } 380 | #storage-ui-container .toolbox { 381 | padding: 5px 0; 382 | overflow: hidden; 383 | } 384 | #storage-ui-container .toolbox li { 385 | color: #2e3436; 386 | line-height: 32px; 387 | cursor: pointer; 388 | float: right; 389 | display: inline; 390 | padding: 0 7px; 391 | border: 1px solid #b6b6b3; 392 | border-radius: 2px; 393 | } 394 | #storage-ui-container.dark .toolbox li { 395 | color: #d3dfeb; 396 | border: 1px solid #202425; 397 | } 398 | #storage-ui-container .toolbox li:hover { 399 | color: #2e3436; 400 | background-color: #d4d4d2; 401 | } 402 | #storage-ui-container.dark .toolbox li:hover { 403 | color: #eeeeec; 404 | background-color: #353c3e; 405 | } 406 | #storage-ui-container .toolbox li + li { 407 | margin-right: 5px; 408 | margin-left: 5px; 409 | } 410 | .dark input, .dark textarea { 411 | background-color: #232729; 412 | border-color: #1b1f20; 413 | /*box-shadow: inset 0 0 0 1px #215d9c;*/ 414 | color: #efefef; 415 | } 416 | 417 | /* *********** */ 418 | /* Alerts */ 419 | /* *********** */ 420 | #storage-alert-container { 421 | min-height: 46px; 422 | line-height: 46px; 423 | padding-left: 10px; 424 | width: calc(100% - 500px); 425 | position: absolute; 426 | top: -46px; 427 | left: 250px; /* Keep save/cancel button accessible. */ 428 | right: 250px; 429 | box-shadow: 0 1px 7px #999999; 430 | visibility: hidden; 431 | background: none repeat scroll 0 0 rgba(20, 22, 23, 0.8); 432 | font-weight: bold; 433 | color: #fff; 434 | font-size: 0.8em; 435 | z-index: 1002; 436 | border-radius: 2px; 437 | } 438 | #storage-alert-container.error { 439 | background-color: #c60f13; 440 | } 441 | .storage-alert #storage-alert-container { 442 | visibility: visible; 443 | top: 23px; 444 | } 445 | .storage-alert .storage-action { 446 | margin-left: 10px; 447 | background-color: #fff; 448 | color: #999; 449 | padding: 5px; 450 | border-radius: 4px; 451 | } 452 | .storage-alert .storage-action:hover { 453 | color: #000; 454 | } 455 | .storage-alert .error .storage-action { 456 | background-color: #666; 457 | color: #eee; 458 | } 459 | .storage-alert .error .storage-action:hover { 460 | color: #fff; 461 | } 462 | 463 | /* *********** */ 464 | /* Tooltip */ 465 | /* *********** */ 466 | #storage-tooltip-container { 467 | line-height: 20px; 468 | padding: 5px 10px; 469 | width: auto; 470 | position: absolute; 471 | box-shadow: 0 1px 7px #999999; 472 | display: none; 473 | background-color: rgba(40, 40, 40, 0.8); 474 | color: #eeeeec; 475 | font-size: 0.8em; 476 | border-radius: 2px; 477 | z-index: 1004; 478 | font-weight: normal; 479 | max-width: 300px; 480 | } 481 | .storage-tooltip #storage-tooltip-container { 482 | display: block; 483 | } 484 | #storage-tooltip-container.tooltip-top:after { 485 | top: 100%; 486 | left: calc(50% - 11px); 487 | border: solid transparent; 488 | content: " "; 489 | height: 0; 490 | width: 0; 491 | position: absolute; 492 | pointer-events: none; 493 | border-top-color: rgba(30, 30, 30, 0.8); 494 | border-width: 11px; 495 | margin-left: calc(-50% + 21px); 496 | } 497 | #storage-tooltip-container.tooltip.tooltip-left:after { 498 | left: 100%; 499 | top: 50%; 500 | border: solid transparent; 501 | content: " "; 502 | height: 0; 503 | width: 0; 504 | position: absolute; 505 | pointer-events: none; 506 | border-color: rgba(136, 183, 213, 0); 507 | border-left-color: #333; 508 | border-width: 11px; 509 | margin-top: -10px; 510 | } 511 | 512 | 513 | 514 | /* *********** */ 515 | /* Close link */ 516 | /* *********** */ 517 | .storage-close-icon { 518 | background-repeat: no-repeat; 519 | background-image: url("../../src/img/16.png"); 520 | background-position: -52px -9px; 521 | display: inline; 522 | padding: 0 10px; 523 | vertical-align: middle; 524 | } 525 | .dark .storage-close-icon { 526 | background-image: url("../../src/img/16-white.png"); 527 | } 528 | .dark .storage-close-link { 529 | border: 1px solid #202425; 530 | color: #eeeeec; 531 | padding: 0 7px; 532 | line-height: 32px; 533 | background-color: #323737; 534 | } 535 | .dark .storage-close-link:hover { 536 | background-color: #2e3436; 537 | } 538 | .storage-alert .storage-close-link { 539 | color: #fff; 540 | float: right; 541 | padding-right: 10px; 542 | } 543 | .storage-alert .storage-close-icon { 544 | background-position: -128px -90px; 545 | } 546 | 547 | 548 | /* *********** */ 549 | /* Mobile */ 550 | /* *********** */ 551 | @media all and (orientation:portrait) { 552 | .storage-ui #storage-ui-container { 553 | height: 50%; 554 | max-height: 400px; 555 | width: 100%; 556 | top: inherit!important; 557 | bottom: 0; 558 | right: 0; 559 | left: 0; 560 | } 561 | .storage-ui .leaflet-right { 562 | right: 0; 563 | } 564 | #storage-alert-container { 565 | width: 100%; 566 | left: 0; 567 | right: 0; 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /contrib/js/storage.ui.default.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Modals 3 | */ 4 | L.S.UI = L.Evented.extend({ 5 | 6 | ALERTS: Array(), 7 | ALERT_ID: null, 8 | TOOLTIP_ID: null, 9 | 10 | initialize: function (parent) { 11 | this.parent = parent; 12 | this.container = L.DomUtil.create('div', 'leaflet-ui-container', this.parent); 13 | L.DomEvent.disableClickPropagation(this.container); 14 | L.DomEvent.on(this.container, 'contextmenu', L.DomEvent.stopPropagation); // Do not activate our custom context menu. 15 | L.DomEvent.on(this.container, 'mousewheel', L.DomEvent.stopPropagation); 16 | L.DomEvent.on(this.container, 'MozMousePixelScroll', L.DomEvent.stopPropagation); 17 | this._panel = L.DomUtil.create('div', '', this.container); 18 | this._panel.id = 'storage-ui-container'; 19 | this._alert = L.DomUtil.create('div', 'with-transition', this.container); 20 | this._alert.id = 'storage-alert-container'; 21 | this._tooltip = L.DomUtil.create('div', '', this.container); 22 | this._tooltip.id = 'storage-tooltip-container'; 23 | }, 24 | 25 | resetPanelClassName: function () { 26 | this._panel.className = 'with-transition'; 27 | }, 28 | 29 | openPanel: function (e) { 30 | this.fire('panel:open'); 31 | // We reset all because we can't know which class has been added 32 | // by previous ui processes... 33 | this.resetPanelClassName(); 34 | this._panel.innerHTML = ''; 35 | var actionsContainer = L.DomUtil.create('ul', 'toolbox', this._panel); 36 | var body = L.DomUtil.create('div', 'body', this._panel); 37 | if (e.data.html.nodeType && e.data.html.nodeType === 1) body.appendChild(e.data.html); 38 | else body.innerHTML = e.data.html; 39 | var closeLink = L.DomUtil.create('li', 'storage-close-link', actionsContainer); 40 | L.DomUtil.add('i', 'storage-close-icon', closeLink); 41 | var label = L.DomUtil.create('span', '', closeLink); 42 | label.title = label.innerHTML = L._('Close'); 43 | if (e.actions) { 44 | for (var i = 0; i < e.actions.length; i++) { 45 | actionsContainer.appendChild(e.actions[i]); 46 | } 47 | } 48 | if (e.className) L.DomUtil.addClass(this._panel, e.className); 49 | if (L.DomUtil.hasClass(this.parent, 'storage-ui')) { 50 | // Already open. 51 | this.fire('panel:ready'); 52 | } else { 53 | L.DomEvent.once(this._panel, 'transitionend', function (e) { 54 | this.fire('panel:ready'); 55 | }, this); 56 | L.DomUtil.addClass(this.parent, 'storage-ui'); 57 | } 58 | L.DomEvent.on(closeLink, 'click', this.closePanel, this); 59 | }, 60 | 61 | closePanel: function () { 62 | this.resetPanelClassName(); 63 | L.DomUtil.removeClass(this.parent, 'storage-ui'); 64 | this.fire('panel:closed'); 65 | }, 66 | 67 | alert: function (e) { 68 | if (L.DomUtil.hasClass(this.parent, 'storage-alert')) this.ALERTS.push(e); 69 | else this.popAlert(e); 70 | }, 71 | 72 | popAlert: function (e) { 73 | var self = this; 74 | if(!e) { 75 | if (this.ALERTS.length) e = this.ALERTS.pop(); 76 | else return; 77 | } 78 | var timeoutID, 79 | level_class = e.level && e.level == 'info'? 'info': 'error'; 80 | this._alert.innerHTML = ''; 81 | L.DomUtil.addClass(this.parent, 'storage-alert'); 82 | L.DomUtil.addClass(this._alert, level_class); 83 | var close = function () { 84 | if (timeoutID !== this.ALERT_ID) { return;} // Another alert has been forced 85 | this._alert.innerHTML = ''; 86 | L.DomUtil.removeClass(this.parent, 'storage-alert'); 87 | L.DomUtil.removeClass(this._alert, level_class); 88 | if (timeoutID) window.clearTimeout(timeoutID); 89 | this.popAlert(); 90 | }; 91 | var closeLink = L.DomUtil.create('a', 'storage-close-link', this._alert); 92 | closeLink.href = '#'; 93 | L.DomUtil.add('i', 'storage-close-icon', closeLink); 94 | var label = L.DomUtil.create('span', '', closeLink); 95 | label.title = label.innerHTML = L._('Close'); 96 | L.DomEvent.on(closeLink, 'click', L.DomEvent.stop) 97 | .on(closeLink, 'click', close, this); 98 | L.DomUtil.add('div', '', this._alert, e.content); 99 | if (e.actions) { 100 | var action, el; 101 | for (var i = 0; i < e.actions.length; i++) { 102 | action = e.actions[i]; 103 | el = L.DomUtil.element('a', {'className': 'storage-action'}, this._alert); 104 | el.href = '#'; 105 | el.innerHTML = action.label; 106 | L.DomEvent.on(el, 'click', L.DomEvent.stop) 107 | .on(el, 'click', close, this); 108 | if (action.callback) L.DomEvent.on(el, 'click', action.callback, action.callbackContext || this.map); 109 | } 110 | } 111 | self.ALERT_ID = timeoutID = window.setTimeout(L.bind(close, this), e.duration || 3000); 112 | }, 113 | 114 | tooltip: function (e) { 115 | this.TOOLTIP_ID = Math.random(); 116 | var id = this.TOOLTIP_ID; 117 | L.DomUtil.addClass(this.parent, 'storage-tooltip'); 118 | if (e.anchor && e.position === 'top') this.anchorTooltipTop(e.anchor); 119 | else if (e.anchor && e.position === 'left') this.anchorTooltipLeft(e.anchor); 120 | else this.anchorTooltipAbsolute(); 121 | this._tooltip.innerHTML = e.content; 122 | function closeIt () { this.closeTooltip(id); } 123 | if (e.anchor) L.DomEvent.once(e.anchor, 'mouseout', closeIt, this); 124 | if (e.duration !== Infinity) window.setTimeout(L.bind(closeIt, this), e.duration || 3000); 125 | }, 126 | 127 | anchorTooltipAbsolute: function () { 128 | this._tooltip.className = ''; 129 | var left = this.parent.offsetLeft + (this.parent.clientWidth / 2) - (this._tooltip.clientWidth / 2), 130 | top = this.parent.offsetTop + 75; 131 | this.setTooltipPosition({top: top, left: left}); 132 | }, 133 | 134 | anchorTooltipTop: function (el) { 135 | this._tooltip.className = 'tooltip-top'; 136 | var coords = this.getPosition(el); 137 | this.setTooltipPosition({left: coords.left - 10, bottom: this.getDocHeight() - coords.top + 11}); 138 | }, 139 | 140 | anchorTooltipLeft: function (el) { 141 | this._tooltip.className = 'tooltip-left'; 142 | var coords = this.getPosition(el); 143 | this.setTooltipPosition({top: coords.top, right: document.documentElement.offsetWidth - coords.left + 11}); 144 | }, 145 | 146 | closeTooltip: function (id) { 147 | if (id && id !== this.TOOLTIP_ID) return; 148 | this._tooltip.innerHTML = ''; 149 | L.DomUtil.removeClass(this.parent, 'storage-tooltip'); 150 | }, 151 | 152 | getPosition: function (el) { 153 | return el.getBoundingClientRect(); 154 | }, 155 | 156 | setTooltipPosition: function (coords) { 157 | if (coords.left) this._tooltip.style.left = coords.left + 'px'; 158 | else this._tooltip.style.left = 'initial'; 159 | if (coords.right) this._tooltip.style.right = coords.right + 'px'; 160 | else this._tooltip.style.right = 'initial'; 161 | if (coords.top) this._tooltip.style.top = coords.top + 'px'; 162 | else this._tooltip.style.top = 'initial'; 163 | if (coords.bottom) this._tooltip.style.bottom = coords.bottom + 'px'; 164 | else this._tooltip.style.bottom = 'initial'; 165 | }, 166 | 167 | getDocHeight: function () { 168 | var D = document; 169 | return Math.max( 170 | D.body.scrollHeight, D.documentElement.scrollHeight, 171 | D.body.offsetHeight, D.documentElement.offsetHeight, 172 | D.body.clientHeight, D.documentElement.clientHeight 173 | ); 174 | }, 175 | 176 | }); 177 | -------------------------------------------------------------------------------- /contrib/js/storage.ui.foundation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Optionaly include this file if you are using Foundation framework. 3 | * You need a
at the bottom of your 4 | * HTML page. 5 | */ 6 | $(document).foundationAlerts(); 7 | /* 8 | * Modals 9 | */ 10 | L.Storage.on('ui:start', function (e) { 11 | var $div = $('#storage-ui-container'); 12 | // reset class 13 | $div.attr("class", ""); 14 | $div.addClass("reveal-modal"); 15 | if (e.cssClass) { 16 | $div.addClass(e.cssClass); 17 | } 18 | // in case a modal is already opened with same id, unbind 19 | $div.unbind('.reveal'); 20 | return $div.empty().html(e.data.html).append('×').reveal(); 21 | }); 22 | L.Storage.on('ui:end', function (e) { 23 | var $div = $('#storage-ui-container'); 24 | if ($div) { 25 | $div.trigger('reveal:close'); 26 | } 27 | }); 28 | $('a.reveal').click(function(e) { 29 | // Generic reveal from ajax call 30 | e.preventDefault(); 31 | var $this = $(this); 32 | var options = {}; 33 | if ($this.data('listenForm')) { 34 | options.listen_form = { 35 | id: $this.data('listenForm') 36 | }; 37 | } 38 | L.Storage.Xhr.get($this.attr('href'), options); 39 | }); 40 | /* 41 | * Alerts 42 | */ 43 | L.Storage.on('ui:alert', function (e) { 44 | var level_class = e.level && e.level == "info"? "success": "alert"; 45 | $div = $('
').addClass('alert-box global').addClass(level_class).html(e.content); 46 | $div.append('×'); 47 | $("body").prepend($div); 48 | }); 49 | /* 50 | * Login/logout buttons 51 | */ 52 | $(document).ready(function(e){ 53 | $('a.login_button').click(function (e) { 54 | e.preventDefault(); 55 | var $this = $(this); 56 | L.Storage.Xhr.login({ 57 | "login_required": $this.attr('href'), 58 | "redirect": "/" 59 | }); 60 | }); 61 | }); 62 | $(document).ready(function(e){ 63 | $('a.logout_button').click(function (e) { 64 | e.preventDefault(); 65 | var $this = $(this); 66 | L.Storage.Xhr.logout($this.attr('href')); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-storage", 3 | "version": "0.8.2", 4 | "description": "Manage map and features with Leaflet and expose them for backend storage through an API.", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "devDependencies": { 9 | "chai": "^3.3.0", 10 | "grunt": "^0.4.4", 11 | "grunt-cli": "^1.2.0", 12 | "grunt-contrib-concat": "^0.5.1", 13 | "grunt-contrib-copy": "^0.5.0", 14 | "happen": "~0.1.3", 15 | "mocha": "^2.3.3", 16 | "mocha-phantomjs": "^4.0.1", 17 | "optimist": "~0.4.0", 18 | "phantomjs": "^1.9.18", 19 | "sinon": "^1.10.3", 20 | "uglify-js": "~2.2.3" 21 | }, 22 | "scripts": { 23 | "test": "firefox test/index.html", 24 | "build": "grunt" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/umap-project/Leaflet.Storage.git" 29 | }, 30 | "keywords": [ 31 | "leaflet" 32 | ], 33 | "author": "Yohan Boniface", 34 | "license": "WTFPL", 35 | "bugs": { 36 | "url": "https://github.com/umap-project/Leaflet.Storage/issues" 37 | }, 38 | "homepage": "http://wiki.openstreetmap.org/wiki/UMap", 39 | "dependencies": { 40 | "csv2geojson": "5.0.2", 41 | "georsstogeojson": "^0.1.0", 42 | "leaflet": "1.3.1", 43 | "leaflet-contextmenu": "^1.4.0", 44 | "leaflet-editable": "^1.1.0", 45 | "leaflet-editinosm": "0.2.3", 46 | "leaflet-formbuilder": "0.2.3", 47 | "leaflet-fullscreen": "1.0.2", 48 | "leaflet-hash": "0.2.1", 49 | "leaflet-i18n": "0.3.1", 50 | "leaflet-loading": "0.1.24", 51 | "leaflet-measurable": "0.0.5", 52 | "leaflet-minimap": "^3.6.1", 53 | "leaflet-toolbar": "umap-project/Leaflet.toolbar", 54 | "leaflet.heat": "0.2.0", 55 | "leaflet.markercluster": "^1.3.0", 56 | "leaflet.path.drag": "0.0.6", 57 | "leaflet.photon": "0.7.3", 58 | "osmtogeojson": "^3.0.0-beta.3", 59 | "togeojson": "0.16.0", 60 | "togpx": "^0.5.4", 61 | "tokml": "0.4.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/img/16-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/16-white.png -------------------------------------------------------------------------------- /src/img/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/16.png -------------------------------------------------------------------------------- /src/img/24-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/24-white.png -------------------------------------------------------------------------------- /src/img/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/24.png -------------------------------------------------------------------------------- /src/img/edit-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/edit-16.png -------------------------------------------------------------------------------- /src/img/icon-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/icon-bg.png -------------------------------------------------------------------------------- /src/img/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/marker.png -------------------------------------------------------------------------------- /src/img/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umap-project/Leaflet.Storage/b85718be8dd8d256860e5e602094bff88f74e909/src/img/search.gif -------------------------------------------------------------------------------- /src/js/leaflet.storage.icon.js: -------------------------------------------------------------------------------- 1 | L.Storage.Icon = L.DivIcon.extend({ 2 | initialize: function(map, options) { 3 | this.map = map; 4 | var default_options = { 5 | iconSize: null, // Made in css 6 | iconUrl: this.map.getDefaultOption('iconUrl'), 7 | feature: null 8 | }; 9 | options = L.Util.extend({}, default_options, options); 10 | L.Icon.prototype.initialize.call(this, options); 11 | this.feature = this.options.feature; 12 | if (this.feature && this.feature.isReadOnly()) { 13 | this.options.className += ' readonly'; 14 | } 15 | }, 16 | 17 | _getIconUrl: function (name) { 18 | var url; 19 | if(this.feature && this.feature._getIconUrl(name)) url = this.feature._getIconUrl(name); 20 | else url = this.options[name + 'Url']; 21 | return this.formatUrl(url, this.feature); 22 | }, 23 | 24 | _getColor: function () { 25 | var color; 26 | if(this.feature) color = this.feature.getOption('color'); 27 | else if (this.options.color) color = this.options.color; 28 | else color = this.map.getDefaultOption('color'); 29 | return color; 30 | }, 31 | 32 | formatUrl: function (url, feature) { 33 | return L.Util.greedyTemplate(url || '', feature ? feature.properties : {}); 34 | } 35 | 36 | }); 37 | 38 | L.Storage.Icon.Default = L.Storage.Icon.extend({ 39 | default_options: { 40 | iconAnchor: new L.Point(16, 40), 41 | popupAnchor: new L.Point(0, -40), 42 | tooltipAnchor: new L.Point(16, -24), 43 | className: 'storage-div-icon' 44 | }, 45 | 46 | initialize: function(map, options) { 47 | options = L.Util.extend({}, this.default_options, options); 48 | L.Storage.Icon.prototype.initialize.call(this, map, options); 49 | }, 50 | 51 | _setColor: function() { 52 | var color = this._getColor(); 53 | this.elements.container.style.backgroundColor = color; 54 | this.elements.arrow.style.borderTopColor = color; 55 | }, 56 | 57 | createIcon: function() { 58 | this.elements = {}; 59 | this.elements.main = L.DomUtil.create('div'); 60 | this.elements.container = L.DomUtil.create('div', 'icon_container', this.elements.main); 61 | this.elements.arrow = L.DomUtil.create('div', 'icon_arrow', this.elements.main); 62 | this.elements.img = L.DomUtil.create('img', null, this.elements.container); 63 | var src = this._getIconUrl('icon'); 64 | if (src) this.elements.img.src = src; 65 | this._setColor(); 66 | this._setIconStyles(this.elements.main, 'icon'); 67 | return this.elements.main; 68 | } 69 | 70 | }); 71 | 72 | L.Storage.Icon.Circle = L.Storage.Icon.extend({ 73 | initialize: function(map, options) { 74 | var default_options = { 75 | iconAnchor: new L.Point(6, 6), 76 | popupAnchor: new L.Point(0, -6), 77 | tooltipAnchor: new L.Point(6, 0), 78 | className: 'storage-circle-icon' 79 | }; 80 | options = L.Util.extend({}, default_options, options); 81 | L.Storage.Icon.prototype.initialize.call(this, map, options); 82 | }, 83 | 84 | _setColor: function() { 85 | this.elements.main.style.backgroundColor = this._getColor(); 86 | }, 87 | 88 | createIcon: function() { 89 | this.elements = {}; 90 | this.elements.main = L.DomUtil.create('div'); 91 | this.elements.main.innerHTML = ' '; 92 | this._setColor(); 93 | this._setIconStyles(this.elements.main, 'icon'); 94 | return this.elements.main; 95 | } 96 | 97 | }); 98 | 99 | L.Storage.Icon.Drop = L.Storage.Icon.Default.extend({ 100 | default_options: { 101 | iconAnchor: new L.Point(16, 42), 102 | popupAnchor: new L.Point(0, -42), 103 | tooltipAnchor: new L.Point(16, -24), 104 | className: 'storage-drop-icon' 105 | } 106 | }); 107 | 108 | L.Storage.Icon.Ball = L.Storage.Icon.Default.extend({ 109 | default_options: { 110 | iconAnchor: new L.Point(8, 30), 111 | popupAnchor: new L.Point(0, -28), 112 | tooltipAnchor: new L.Point(8, -23), 113 | className: 'storage-ball-icon' 114 | }, 115 | 116 | createIcon: function() { 117 | this.elements = {}; 118 | this.elements.main = L.DomUtil.create('div'); 119 | this.elements.container = L.DomUtil.create('div', 'icon_container', this.elements.main); 120 | this.elements.arrow = L.DomUtil.create('div', 'icon_arrow', this.elements.main); 121 | this._setColor(); 122 | this._setIconStyles(this.elements.main, 'icon'); 123 | return this.elements.main; 124 | }, 125 | 126 | _setColor: function() { 127 | var color = this._getColor('color'), 128 | background; 129 | if (L.Browser.ielt9) { 130 | background = color; 131 | } 132 | else if (L.Browser.webkit) { 133 | background = '-webkit-gradient( radial, 6 38%, 0, 6 38%, 8, from(white), to(' + color + ') )'; 134 | } 135 | else { 136 | background = 'radial-gradient(circle at 6px 38% , white -4px, ' + color + ' 8px) repeat scroll 0 0 transparent'; 137 | } 138 | this.elements.container.style.background = background; 139 | } 140 | 141 | }); 142 | 143 | var _CACHE_COLOR = {}; 144 | L.Storage.Icon.Cluster = L.DivIcon.extend({ 145 | options: { 146 | iconSize: [40, 40] 147 | }, 148 | 149 | initialize: function (datalayer, cluster) { 150 | this.datalayer = datalayer; 151 | this.cluster = cluster; 152 | }, 153 | 154 | createIcon: function () { 155 | var container = L.DomUtil.create('div', 'leaflet-marker-icon marker-cluster'), 156 | div = L.DomUtil.create('div', '', container), 157 | span = L.DomUtil.create('span', '', div), 158 | backgroundColor = this.datalayer.getColor(), 159 | color; 160 | span.innerHTML = this.cluster.getChildCount(); 161 | div.style.backgroundColor = backgroundColor; 162 | if (this.datalayer.options.cluster && this.datalayer.options.cluster.textColor) { 163 | color = this.datalayer.options.cluster.textColor; 164 | } 165 | if (!color) { 166 | if (typeof _CACHE_COLOR[backgroundColor] === 'undefined') { 167 | color = L.DomUtil.TextColorFromBackgroundColor(div); 168 | _CACHE_COLOR[backgroundColor] = color; 169 | } else { 170 | color = _CACHE_COLOR[backgroundColor]; 171 | } 172 | } 173 | div.style.color = color; 174 | return container; 175 | } 176 | 177 | }); 178 | -------------------------------------------------------------------------------- /src/js/leaflet.storage.popup.js: -------------------------------------------------------------------------------- 1 | L.S.Popup = L.Popup.extend({ 2 | 3 | options: { 4 | parseTemplate: true 5 | }, 6 | 7 | initialize: function (feature) { 8 | this.feature = feature; 9 | this.container = L.DomUtil.create('div', 'storage-popup'); 10 | this.format(); 11 | L.Popup.prototype.initialize.call(this, {}, feature); 12 | this.setContent(this.container); 13 | }, 14 | 15 | hasFooter: function () { 16 | return this.feature.hasPopupFooter(); 17 | }, 18 | 19 | renderTitle: function () {}, 20 | 21 | renderBody: function () { 22 | var template = this.feature.getOption('popupContentTemplate'), 23 | container = L.DomUtil.create('div', ''), 24 | content, properties, center; 25 | if (this.options.parseTemplate) { 26 | // Include context properties 27 | properties = this.feature.map.getGeoContext(); 28 | center = this.feature.getCenter(); 29 | properties.lat = center.lat; 30 | properties.lon = center.lng; 31 | properties.lng = center.lng; 32 | if (typeof this.feature.getMeasure !== 'undefined') { 33 | properties.measure = this.feature.getMeasure(); 34 | } 35 | properties = L.extend(properties, this.feature.properties); 36 | // Resolve properties inside description 37 | properties.description = L.Util.greedyTemplate(this.feature.properties.description || '', properties); 38 | content = L.Util.greedyTemplate(template, properties); 39 | } 40 | content = L.Util.toHTML(content); 41 | container.innerHTML = content; 42 | var els = container.querySelectorAll('img,iframe'); 43 | for (var i = 0; i < els.length; i++) { 44 | this.onElementLoaded(els[i]); 45 | } 46 | if (!els.length && container.textContent.replace('\n', '') === '') { 47 | container.innerHTML = ''; 48 | L.DomUtil.add('h3', '', container, this.feature.getDisplayName()); 49 | } 50 | return container; 51 | }, 52 | 53 | renderFooter: function () { 54 | if (this.hasFooter()) { 55 | var footer = L.DomUtil.create('ul', 'storage-popup-footer', this.container), 56 | previousLi = L.DomUtil.create('li', 'previous', footer), 57 | zoomLi = L.DomUtil.create('li', 'zoom', footer), 58 | nextLi = L.DomUtil.create('li', 'next', footer), 59 | next = this.feature.getNext(), 60 | prev = this.feature.getPrevious(); 61 | if (next) { 62 | nextLi.title = L._('Go to «{feature}»', {feature: next.properties.name || L._('next')}); 63 | } 64 | if (prev) { 65 | previousLi.title = L._('Go to «{feature}»', {feature: prev.properties.name || L._('previous')}); 66 | } 67 | zoomLi.title = L._('Zoom to this feature'); 68 | L.DomEvent.on(nextLi, 'click', function () { 69 | if (next) next.bringToCenter({zoomTo: next.getOption('zoomTo'), callback: next.view}); 70 | }); 71 | L.DomEvent.on(previousLi, 'click', function () { 72 | if (prev) prev.bringToCenter({zoomTo: prev.getOption('zoomTo'), callback: prev.view}); 73 | }); 74 | L.DomEvent.on(zoomLi, 'click', function () { 75 | this.bringToCenter({zoomTo: this.getOption('zoomTo')}); 76 | }, this.feature); 77 | } 78 | }, 79 | 80 | format: function () { 81 | var title = this.renderTitle(); 82 | if (title) this.container.appendChild(title); 83 | var body = this.renderBody(); 84 | if (body) L.DomUtil.add('div', 'storage-popup-content', this.container, body); 85 | this.renderFooter(); 86 | }, 87 | 88 | onElementLoaded: function (el) { 89 | L.DomEvent.on(el, 'load', function () { 90 | this._updateLayout(); 91 | this._updatePosition(); 92 | this._adjustPan(); 93 | }, this); 94 | } 95 | 96 | }); 97 | 98 | L.S.Popup.Large = L.S.Popup.extend({ 99 | options: { 100 | maxWidth: 500, 101 | className: 'storage-popup-large' 102 | } 103 | }); 104 | 105 | L.S.Popup.BaseWithTitle = L.S.Popup.extend({ 106 | 107 | renderTitle: function () { 108 | var title; 109 | if (this.feature.getDisplayName()) { 110 | title = L.DomUtil.create('h3', 'popup-title'); 111 | title.innerHTML = L.Util.escapeHTML(this.feature.getDisplayName()); 112 | } 113 | return title; 114 | } 115 | 116 | }); 117 | 118 | L.S.Popup.Table = L.S.Popup.BaseWithTitle.extend({ 119 | 120 | formatRow: function (key, value) { 121 | if (value.indexOf('http') === 0) { 122 | value = '' + value + ''; 123 | } 124 | return value; 125 | }, 126 | 127 | addRow: function (container, key, value) { 128 | var tr = L.DomUtil.create('tr', '', container); 129 | L.DomUtil.add('th', '', tr, key); 130 | L.DomUtil.add('td', '', tr, this.formatRow(key, value)); 131 | }, 132 | 133 | renderBody: function () { 134 | var table = L.DomUtil.create('table'); 135 | 136 | for (var key in this.feature.properties) { 137 | if (typeof this.feature.properties[key] === 'object' || key === 'name') continue; 138 | // TODO, manage links (url, mailto, wikipedia...) 139 | this.addRow(table, key, L.Util.escapeHTML(this.feature.properties[key]).trim()); 140 | } 141 | return table; 142 | } 143 | 144 | }); 145 | 146 | L.S.Popup.table = L.S.Popup.Table; // backward compatibility 147 | 148 | L.S.Popup.GeoRSSImage = L.S.Popup.BaseWithTitle.extend({ 149 | 150 | options: { 151 | minWidth: 300, 152 | maxWidth: 500, 153 | className: 'storage-popup-large storage-georss-image' 154 | }, 155 | 156 | renderBody: function () { 157 | var container = L.DomUtil.create('a'); 158 | container.href = this.feature.properties.link; 159 | container.target = '_blank'; 160 | if (this.feature.properties.img) { 161 | var img = L.DomUtil.create('img', '', container); 162 | img.src = this.feature.properties.img; 163 | // Sadly, we are unable to override this from JS the clean way 164 | // See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847 165 | img.style.maxWidth = this.options.maxWidth + 'px'; 166 | img.style.maxHeight = this.options.maxWidth + 'px'; 167 | this.onElementLoaded(img); 168 | } 169 | return container; 170 | } 171 | 172 | }); 173 | 174 | L.S.Popup.GeoRSSLink = L.S.Popup.extend({ 175 | 176 | options: { 177 | className: 'storage-georss-link' 178 | }, 179 | 180 | renderBody: function () { 181 | var title = this.renderTitle(this), 182 | a = L.DomUtil.add('a'); 183 | a.href = this.feature.properties.link; 184 | a.target = '_blank'; 185 | a.appendChild(title); 186 | return a; 187 | } 188 | }); 189 | 190 | L.S.Popup.SimplePanel = L.S.Popup.extend({ 191 | 192 | options: { 193 | zoomAnimation: false 194 | }, 195 | 196 | allButton: function () { 197 | var button = L.DomUtil.create('li', ''); 198 | L.DomUtil.create('i', 'storage-icon-16 storage-list', button); 199 | var label = L.DomUtil.create('span', '', button); 200 | label.innerHTML = label.title = L._('See all'); 201 | L.DomEvent.on(button, 'click', this.feature.map.openBrowser, this.feature.map); 202 | return button; 203 | }, 204 | 205 | update: function () { 206 | this.feature.map.ui.openPanel({data: {html: this._content}, actions: [this.allButton()]}); 207 | }, 208 | 209 | onRemove: function (map) { 210 | map.ui.closePanel(); 211 | L.S.Popup.prototype.onRemove.call(this, map); 212 | }, 213 | 214 | _initLayout: function () {this._container = L.DomUtil.create('span');}, 215 | _updateLayout: function () {}, 216 | _updatePosition: function () {}, 217 | _adjustPan: function () {} 218 | }); 219 | -------------------------------------------------------------------------------- /src/js/leaflet.storage.slideshow.js: -------------------------------------------------------------------------------- 1 | L.S.Slideshow = L.Class.extend({ 2 | 3 | statics: { 4 | CLASSNAME: 'storage-slideshow-active' 5 | }, 6 | 7 | options: { 8 | delay: 5000, 9 | autoplay: false 10 | }, 11 | 12 | initialize: function (map, options) { 13 | this.setOptions(options); 14 | this.map = map; 15 | this._id = null; 16 | var current = null, // current feature 17 | self = this; 18 | try { 19 | Object.defineProperty(this, 'current', { 20 | get: function () { 21 | if (!current) { 22 | var datalayer = this.defaultDatalayer(); 23 | if (datalayer) current = datalayer.getFeatureByIndex(0); 24 | } 25 | return current; 26 | }, 27 | set: function (feature) { 28 | current = feature; 29 | } 30 | }); 31 | } 32 | catch (e) { 33 | // Certainly IE8, which has a limited version of defineProperty 34 | } 35 | try { 36 | Object.defineProperty(this, 'next', { 37 | get: function () { 38 | if (!current) { 39 | return self.current; 40 | } 41 | return current.getNext(); 42 | } 43 | }); 44 | } 45 | catch (e) { 46 | // Certainly IE8, which has a limited version of defineProperty 47 | } 48 | if (this.options.autoplay) { 49 | this.map.onceDatalayersLoaded(function () { 50 | this.play(); 51 | }, this); 52 | } 53 | this.map.on('edit:enabled', function () { 54 | this.stop(); 55 | }, this); 56 | }, 57 | 58 | setOptions: function (options) { 59 | L.setOptions(this, options); 60 | this.timeSpinner(); 61 | }, 62 | 63 | defaultDatalayer: function () { 64 | return this.map.findDataLayer(function (d) { return d.allowBrowse(); }); 65 | }, 66 | 67 | timeSpinner: function () { 68 | var time = parseInt(this.options.delay, 10); 69 | if (!time) return; 70 | var css = 'rotation ' + time / 1000 + 's infinite linear', 71 | spinners = document.querySelectorAll('.storage-slideshow-toolbox .play .spinner'); 72 | for (var i = 0; i < spinners.length; i++) { 73 | spinners[i].style.animation = css; 74 | spinners[i].style['-webkit-animation'] = css; 75 | spinners[i].style['-moz-animation'] = css; 76 | spinners[i].style['-o-animation'] = css; 77 | } 78 | }, 79 | 80 | resetSpinners: function () { 81 | // Make that animnation is coordinated with user actions 82 | var spinners = document.querySelectorAll('.storage-slideshow-toolbox .play .spinner'), 83 | el, newOne; 84 | for (var i = 0; i < spinners.length; i++) { 85 | el = spinners[i]; 86 | newOne = el.cloneNode(true); 87 | el.parentNode.replaceChild(newOne, el); 88 | } 89 | }, 90 | 91 | play: function () { 92 | if (this._id) return; 93 | if (this.map.editEnabled) return; 94 | L.DomUtil.addClass(document.body, L.S.Slideshow.CLASSNAME); 95 | this._id = window.setInterval(L.bind(this.loop, this), this.options.delay); 96 | this.resetSpinners(); 97 | this.loop(); 98 | }, 99 | 100 | loop: function () { 101 | this.current = this.next; 102 | this.step(); 103 | }, 104 | 105 | pause: function () { 106 | if (this._id) { 107 | L.DomUtil.removeClass(document.body, L.S.Slideshow.CLASSNAME); 108 | window.clearInterval(this._id); 109 | this._id = null; 110 | } 111 | }, 112 | 113 | stop: function () { 114 | this.pause(); 115 | this.current = null; 116 | }, 117 | 118 | forward: function () { 119 | this.pause(); 120 | this.current = this.next; 121 | this.step(); 122 | }, 123 | 124 | backward: function () { 125 | this.pause(); 126 | if (this.current) this.current = this.current.getPrevious(); 127 | this.step(); 128 | }, 129 | 130 | step: function () { 131 | if(!this.current) return this.stop(); 132 | this.current.zoomTo({easing: this.options.easing}); 133 | this.current.view(); 134 | }, 135 | 136 | renderToolbox: function (container) { 137 | var box = L.DomUtil.create('ul', 'storage-slideshow-toolbox'), 138 | play = L.DomUtil.create('li', 'play', box), 139 | stop = L.DomUtil.create('li', 'stop', box), 140 | prev = L.DomUtil.create('li', 'prev', box), 141 | next = L.DomUtil.create('li', 'next', box); 142 | L.DomUtil.create('div', 'spinner', play); 143 | play.title = L._('Start slideshow'); 144 | stop.title = L._('Stop slideshow'); 145 | next.title = L._('Zoom to the next'); 146 | prev.title = L._('Zoom to the previous'); 147 | var toggle = function () { 148 | if (this._id) this.pause(); 149 | else this.play(); 150 | }; 151 | L.DomEvent.on(play, 'click', L.DomEvent.stop) 152 | .on(play, 'click', toggle, this); 153 | L.DomEvent.on(stop, 'click', L.DomEvent.stop) 154 | .on(stop, 'click', this.stop, this); 155 | L.DomEvent.on(prev, 'click', L.DomEvent.stop) 156 | .on(prev, 'click', this.backward, this); 157 | L.DomEvent.on(next, 'click', L.DomEvent.stop) 158 | .on(next, 'click', this.forward, this); 159 | container.appendChild(box); 160 | this.timeSpinner(); 161 | return box; 162 | } 163 | 164 | }); 165 | -------------------------------------------------------------------------------- /src/js/leaflet.storage.tableeditor.js: -------------------------------------------------------------------------------- 1 | L.S.TableEditor = L.Class.extend({ 2 | 3 | initialize: function (datalayer) { 4 | this.datalayer = datalayer; 5 | this.table = L.DomUtil.create('div', 'table'); 6 | this.header = L.DomUtil.create('div', 'thead', this.table); 7 | this.body = L.DomUtil.create('div', 'tbody', this.table); 8 | this.resetProperties(); 9 | }, 10 | 11 | renderHeaders: function () { 12 | this.header.innerHTML = ''; 13 | for (var i = 0; i < this.properties.length; i++) { 14 | this.renderHeader(this.properties[i]); 15 | } 16 | }, 17 | 18 | renderHeader: function (property) { 19 | var container = L.DomUtil.create('div', 'tcell', this.header), 20 | title = L.DomUtil.add('span', '', container, property), 21 | del = L.DomUtil.create('i', 'storage-delete', container), 22 | rename = L.DomUtil.create('i', 'storage-edit', container); 23 | del.title = L._('Delete this property on all the features'); 24 | rename.title = L._('Rename this property on all the features'); 25 | var doDelete = function () { 26 | if (confirm(L._('Are you sure you want to delete this property on all the features?'))) { 27 | this.datalayer.eachLayer(function (feature) { 28 | feature.deleteProperty(property); 29 | }); 30 | this.datalayer.deindexProperty(property); 31 | this.resetProperties(); 32 | this.edit(); 33 | } 34 | }; 35 | var doRename = function () { 36 | var newName = prompt(L._('Please enter the new name of this property'), property); 37 | if (!newName) return; 38 | this.datalayer.eachLayer(function (feature) { 39 | feature.renameProperty(property, newName); 40 | }); 41 | this.datalayer.deindexProperty(property); 42 | this.datalayer.indexProperty(newName); 43 | this.resetProperties(); 44 | this.edit(); 45 | }; 46 | L.DomEvent.on(del, 'click', doDelete, this); 47 | L.DomEvent.on(rename, 'click', doRename, this); 48 | }, 49 | 50 | renderRow: function (feature) { 51 | var builder = new L.S.FormBuilder(feature, this.field_properties, 52 | { 53 | id: 'storage-feature-properties_' + L.stamp(feature), 54 | className: 'trow', 55 | callback: feature.resetTooltip 56 | } 57 | ); 58 | this.body.appendChild(builder.build()); 59 | }, 60 | 61 | compileProperties: function () { 62 | if (this.properties.length === 0) this.properties = ['name']; 63 | // description is a forced textarea, don't edit it in a text input, or you lose cariage returns 64 | if (this.properties.indexOf('description') !== -1) this.properties.splice(this.properties.indexOf('description'), 1); 65 | this.properties.sort(); 66 | this.field_properties = []; 67 | for (var i = 0; i < this.properties.length; i++) { 68 | this.field_properties.push(['properties.' + this.properties[i], {wrapper: 'div', wrapperClass: 'tcell'}]); 69 | } 70 | }, 71 | 72 | resetProperties: function () { 73 | this.properties = this.datalayer._propertiesIndex; 74 | }, 75 | 76 | edit: function () { 77 | var id = 'tableeditor:edit'; 78 | this.datalayer.map.fire('dataloading', {id: id}); 79 | this.compileProperties(); 80 | this.renderHeaders(); 81 | this.body.innerHTML = ''; 82 | this.datalayer.eachLayer(this.renderRow, this); 83 | var addButton = L.DomUtil.create('li', 'add-property'); 84 | L.DomUtil.create('i', 'storage-icon-16 storage-add', addButton); 85 | var label = L.DomUtil.create('span', '', addButton); 86 | label.innerHTML = label.title = L._('Add a new property'); 87 | var addProperty = function () { 88 | var newName = prompt(L._('Please enter the name of the property')); 89 | if (!newName) return; 90 | this.datalayer.indexProperty(newName); 91 | this.edit(); 92 | }; 93 | L.DomEvent.on(addButton, 'click', addProperty, this); 94 | var className = (this.properties.length > 2) ? 'storage-table-editor fullwidth dark' : 'storage-table-editor dark'; 95 | this.datalayer.map.ui.openPanel({data: {html: this.table}, className: className, actions: [addButton]}); 96 | this.datalayer.map.fire('dataload', {id: id}); 97 | } 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /src/js/leaflet.storage.xhr.js: -------------------------------------------------------------------------------- 1 | L.Storage.Xhr = L.Evented.extend({ 2 | 3 | initialize: function (ui) { 4 | this.ui = ui; 5 | }, 6 | 7 | _wrapper: function () { 8 | var wrapper; 9 | if (window.XMLHttpRequest === undefined) { 10 | wrapper = function() { 11 | try { 12 | return new window.ActiveXObject('Microsoft.XMLHTTP.6.0'); 13 | } 14 | catch (e1) { 15 | try { 16 | return new window.ActiveXObject('Microsoft.XMLHTTP.3.0'); 17 | } 18 | catch (e2) { 19 | throw new Error('XMLHttpRequest is not supported'); 20 | } 21 | } 22 | }; 23 | } 24 | else { 25 | wrapper = window.XMLHttpRequest; 26 | } 27 | return new wrapper(); 28 | }, 29 | 30 | _ajax: function (settings) { 31 | var xhr = this._wrapper(), id = Math.random(), self = this; 32 | this.fire('dataloading', {id: id}); 33 | var loaded = function () {self.fire('dataload', {id: id});}; 34 | 35 | try { 36 | xhr.open(settings.verb, settings.uri, true); 37 | } catch (err) { 38 | // Unknown protocol? 39 | this.ui.alert({content: L._('Error while fetching {url}', {url: settings.uri}), level: 'error'}); 40 | loaded(); 41 | return 42 | } 43 | 44 | if (settings.uri.indexOf('http') !== 0 || settings.uri.indexOf(window.location.origin) === 0) { 45 | // "X-" mode headers cause the request to be in preflight mode, 46 | // we don"t want that by default for CORS requests 47 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 48 | } 49 | if (settings.headers) { 50 | for (var name in settings.headers) { 51 | xhr.setRequestHeader(name, settings.headers[name]); 52 | } 53 | } 54 | 55 | 56 | xhr.onreadystatechange = function() { 57 | if (xhr.readyState === 4) { 58 | if (xhr.status == 200) { 59 | settings.callback.call(settings.context || xhr, xhr.responseText, xhr); 60 | } 61 | else if (xhr.status === 403) { 62 | self.ui.alert({content: L._('Action not allowed :('), level: 'error'}); 63 | } 64 | else if (xhr.status === 412) { 65 | var msg = L._('Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.'); 66 | var actions = [ 67 | { 68 | label: L._('Save anyway'), 69 | callback: function () { 70 | delete settings.headers['If-Match']; 71 | self._ajax(settings); 72 | }, 73 | callbackContext: self 74 | }, 75 | { 76 | label: L._('Cancel') 77 | } 78 | ]; 79 | self.ui.alert({content: msg, level: 'error', duration: 100000, actions: actions}); 80 | } 81 | else { 82 | if (xhr.status !== 0) { // 0 === request cut by user 83 | self.ui.alert({'content': L._('Problem in the response'), 'level': 'error'}); 84 | } 85 | } 86 | loaded(); 87 | } 88 | }; 89 | 90 | try { 91 | xhr.send(settings.data); 92 | } catch (e) { 93 | // Pass 94 | loaded(); 95 | console.error('Bad Request', e); 96 | } 97 | }, 98 | 99 | // supports only JSON as response data type 100 | _json: function (verb, uri, options) { 101 | var args = arguments, 102 | self = this; 103 | var default_options = { 104 | 'async': true, 105 | 'callback': null, 106 | 'responseType': 'text', 107 | 'data': null, 108 | 'listen_form': null // optional form to listen in default callback 109 | }; 110 | var settings = L.Util.extend({}, default_options, options); 111 | 112 | if (verb === 'POST') { 113 | // find a way not to make this django specific 114 | var token = document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*\=\s*([^;]*).*$)|^.*$/, '$1'); 115 | if (token) { 116 | settings.headers = settings.headers || {}; 117 | settings.headers['X-CSRFToken'] = token; 118 | } 119 | } 120 | 121 | var callback = function(responseText, response) { 122 | var data; 123 | try { 124 | data = JSON.parse(responseText); 125 | } 126 | catch (err) { 127 | console.log(err); 128 | self.ui.alert({content: L._('Problem in the response format'), level: 'error'}); 129 | return; 130 | } 131 | if (data.errors) { 132 | console.log(data.errors); 133 | self.ui.alert({content: L._('An error occured'), level: 'error'}); 134 | } else if (data.login_required) { 135 | // login_required should be an URL for the login form 136 | if (settings.login_callback) settings.login_callback(data); 137 | else self.login(data, args); 138 | } 139 | else { 140 | if (settings.callback) L.bind(settings.callback, settings.context || this)(data, response); 141 | else self.default_callback(data, settings, response); 142 | } 143 | }; 144 | 145 | this._ajax({ 146 | verb: verb, 147 | uri: uri, 148 | data: settings.data, 149 | callback: callback, 150 | headers: settings.headers, 151 | listener: settings.listener 152 | }); 153 | }, 154 | 155 | get: function(uri, options) { 156 | this._json('GET', uri, options); 157 | }, 158 | 159 | post: function(uri, options) { 160 | this._json('POST', uri, options); 161 | }, 162 | 163 | submit_form: function(form_id, options) { 164 | if(typeof options === 'undefined') options = {}; 165 | var form = L.DomUtil.get(form_id); 166 | var formData = new FormData(form); 167 | if(options.extraFormData) formData.append(options.extraFormData); 168 | options.data = formData; 169 | this.post(form.action, options); 170 | return false; 171 | }, 172 | 173 | listen_form: function (form_id, options) { 174 | var form = L.DomUtil.get(form_id), self = this; 175 | if (!form) return; 176 | L.DomEvent 177 | .on(form, 'submit', L.DomEvent.stopPropagation) 178 | .on(form, 'submit', L.DomEvent.preventDefault) 179 | .on(form, 'submit', function () { 180 | self.submit_form(form_id, options); 181 | }); 182 | }, 183 | 184 | listen_link: function (link_id, options) { 185 | var link = L.DomUtil.get(link_id), self = this; 186 | if (link) { 187 | L.DomEvent 188 | .on(link, 'click', L.DomEvent.stop) 189 | .on(link, 'click', function () { 190 | if (options.confirm && !confirm(options.confirm)) { return;} 191 | self.get(link.href, options); 192 | }); 193 | } 194 | }, 195 | 196 | default_callback: function (data, options) { 197 | // default callback, to avoid boilerplate 198 | if (data.redirect) { 199 | var newPath = data.redirect; 200 | if (window.location.pathname == newPath) window.location.reload(); // Keep the hash, so the current view 201 | else window.location = newPath; 202 | } 203 | else if (data.info) { 204 | this.ui.alert({content: data.info, level: 'info'}); 205 | this.ui.closePanel(); 206 | } 207 | else if (data.error) { 208 | this.ui.alert({content: data.error, level: 'error'}); 209 | } 210 | else if (data.html) { 211 | var ui_options = {'data': data}, 212 | listen_options; 213 | if (options.className) ui_options.className = options.className; 214 | this.ui.openPanel(ui_options); 215 | // To low boilerplate, if there is a form, listen it 216 | if (options.listen_form) { 217 | // Listen form again 218 | listen_options = L.Util.extend({}, options, options.listen_form.options); 219 | this.listen_form(options.listen_form.id, listen_options); 220 | } 221 | if (options.listen_link) { 222 | for (var i=0, l=options.listen_link.length; iLeaflet and Django, glued by uMap project.": "Powered by Leaflet and Django, glued by uMap project.", 183 | "Zoom level for automatic zooms": "自動ズーム時のズームレベル", 184 | "Do you want to display the «more» control?": "«more»操作パネルを表示しますか?", 185 | "Auto": "自動", 186 | "Default: name": "デフォルト: 名称", 187 | "Property to use for sorting features": "地物並び替え時のプロパティ", 188 | "Slideshow": "スライドショー", 189 | "Start slideshow": "スライドショーを開始", 190 | "Stop slideshow": "スライドショーを停止", 191 | "Text color for the cluster label": "クラスタラベルのテキスト色", 192 | "Zoom to the next": "次にズーム", 193 | "Zoom to the previous": "前にズーム", 194 | "Add a new property": "プロパティ追加", 195 | "Are you sure you want to delete this property on all the features?": "すべての地物からこのプロパティを削除します。よろしいですか?", 196 | "Close": "閉じる", 197 | "Delete this property on all the features": "すべての地物からこのプロパティを削除", 198 | "Edit properties in a table": "表形式でプロパティを編集", 199 | "Please enter the name of the property": "プロパティの名称を入力してください", 200 | "Please enter the new name of this property": "このプロパティに新しい名称を付与してください", 201 | "Rename this property on all the features": "すべての地物に対してこのプロパティ名を変更", 202 | "If false, the polygon will act as a part of the underlying map.": "無効にした場合、ポリゴンは背景地図の一部として動作します。", 203 | "Iframe with custom height (in px): {{{http://iframe.url.com|height}}}": "Iframeの縦幅を指定(ピクセル): {{{http://iframe.url.com|height}}}", 204 | "Iframe: {{{http://iframe.url.com}}}": "Iframe: {{{http://iframe.url.com}}}", 205 | "See all": "すべて表示", 206 | "Dynamic properties": "動的なプロパティ", 207 | "Use placeholders with feature properties between brackets, eg. {name}, they will be dynamically replaced by the corresponding values.": "波括弧で地物プロパティをくくると(例: {name})、対応する値に動的に置き換えられます。", 208 | "Long credits": "正式なクレジット", 209 | "No licence has been set": "ライセンス指定がありません", 210 | "Popup content template": "ポップアップコンテンツのテンプレート", 211 | "Short credits": "短縮表示板クレジット", 212 | "Will be displayed in the bottom right corner of the map": "地図の右下に表示されます", 213 | "Will be visible in the caption of the map": "地図の脚注として表示されます", 214 | "Map has been saved!": "地図の保存完了", 215 | "Save anyway": "保存を再実行", 216 | "Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.": "おおおっと! 他の誰かがデータを編集したようです。あなたの編集内容をもう一度保存することもできますが、その場合、他の誰かが行った編集は削除されます。", 217 | "Comma separated list of properties to use when filtering features": "地物をフィルタする際に利用する、カンマで区切ったプロパティのリスト", 218 | "Keep current visible layers": "現在表示しているレイヤを保持", 219 | "Coordinates": "位置情報", 220 | "Latitude": "緯度", 221 | "Longitude": "経度", 222 | "Continue line (Ctrl-click)": "ラインを延長(Ctrl-クリック)", 223 | "Start a hole here": "この部分に穴を作成", 224 | "Click last point to finish shape": "クリックでシェイプの作成を終了", 225 | "Click to add a marker": "クリックでマーカー追加", 226 | "Click to continue drawing": "クリックで描画を継続", 227 | "Click to start drawing a line": "クリックでラインの描画開始", 228 | "Click to start drawing a polygon": "クリックでポリゴンの描画開始", 229 | "Import in a new layer": "新規レイヤをインポート", 230 | "Layer": "レイヤ", 231 | "Please choose a format": "フォーマットを指定してください", 232 | "Imports all umap data, including layers and settings.": "umapのデータをすべてインポート(レイヤ、設定を含む)", 233 | "Invalid umap data": "不正なumapデータ", 234 | "Invalid umap data in {filename}": "{filename} に不正なumapデータ", 235 | "Add a line to the current multi": "現在のマルチにラインを追加", 236 | "Add a polygon to the current multi": "現在のマルチにポリゴンを追加", 237 | "Click to edit": "クリックで編集", 238 | "Continue line": "ラインを延長", 239 | "Delete this shape": "このシェイプを削除", 240 | "Make main shape": "メインシェイプを作成", 241 | "Merge lines": "ラインを結合", 242 | "Remove shape from the multi": "マルチからシェイプを削除", 243 | "Transfer shape to edited feature": "シェイプを編集済み地物へ変換", 244 | "next": "次へ", 245 | "previous": "前へ", 246 | "Measure distances": "距離を計測", 247 | "NM": "NM", 248 | "kilometers": "キロメートル", 249 | "km": "(km)", 250 | "mi": "(マイル)", 251 | "miles": "マイル", 252 | "nautical miles": "海里", 253 | "{area} acres": "{area} エイカー", 254 | "{area} ha": "{area} ヘクタール", 255 | "{area} m²": "{area} m²", 256 | "{area} mi²": "{area} 平方マイル", 257 | "{area} yd²": "{area} 平方ヤード", 258 | "{distance} NM": "{distance} NM", 259 | "{distance} km": "{distance} km", 260 | "{distance} m": "{distance} m", 261 | "{distance} miles": "{distance} マイル", 262 | "{distance} yd": "{distance} ヤード", 263 | "Are you sure you want to restore this version?": "本当にこのバージョンを復元してよいですか?", 264 | "Extract shape to separate feature": "シェイプを複数の地物に変換", 265 | "Layer properties": "レイヤープロパティ", 266 | "Restore this version": "このバージョンを復元する", 267 | "Versions": "バージョン", 268 | "You have unsaved changes.": "編集が保存されていません", 269 | "A comma separated list of numbers that defines the stroke dash pattern. Ex.: \"5, 10, 15\".": "カンマで数字を区切り、ストロークの破線パターンを指定。 例: \"5, 10, 15\"", 270 | "Advanced transition": "拡張トランジション", 271 | "Allow interactions": "インタラクションを許可", 272 | "Autostart when map is loaded": "マップ読み込み時に自動で開始", 273 | "Default interaction options": "標準のポップアップオプション", 274 | "Default shape properties": "標準のシェイプ表示プロパティ", 275 | "Default zoom level": "標準ズームレベル", 276 | "Define link to open in a new window on polygon click.": "ポリゴンクリック時にリンクを新しいウィンドウで開く", 277 | "Delete this vertex (Alt-click)": "このポイントを削除(Altを押しながらクリック)", 278 | "Display the control to open OpenStreetMap editor": "OpenStreetMapエディタを起動するパネルを表示", 279 | "Display the data layers control": "データレイヤ操作パネルを表示", 280 | "Display the embed control": "サイトへのマップ埋め込みパネルを表示", 281 | "Display the fullscreen control": "フルスクリーンパネルを表示", 282 | "Display the locate control": "現在地パネルを表示", 283 | "Display the measure control": "縮尺パネルを表示", 284 | "Display the search control": "検索パネルを表示", 285 | "Display the tile layers control": "タイルレイヤパネルを表示", 286 | "Display the zoom control": "ズーム変更パネルを表示", 287 | "Exit Fullscreen": "フルスクリーン表示を解除", 288 | "Fetch data each time map view changes.": "地図表示が変更されたらデータを再取得する", 289 | "Filter keys": "フィルタに使うキー", 290 | "Icon shape": "アイコン形状", 291 | "Icon symbol": "アイコンシンボル", 292 | "Interaction options": "ポップアップオプション", 293 | "Label key": "ラベル表示するキー", 294 | "Link to…": "リンク", 295 | "Must be a valid CSS value (eg.: DarkBlue or #123456)": "CSSで有効な値を指定してください (例: DarkBlue, #123456など)", 296 | "No results": "検索結果なし", 297 | "Popup style": "ポップアップスタイル", 298 | "Replace layer content": "レイヤ内容を差し替える", 299 | "Save this location as new feature": "この場所を新しい地物として保存", 300 | "Search location": "地名で検索", 301 | "Set URL": "URLを設定", 302 | "Shape properties": "シェイプ表示プロパティ", 303 | "Simplify": "簡略化", 304 | "Sort key": "並び替えに使うキー", 305 | "The name of the property to use as feature label (ex.: \"nom\")": "地物のラベルとして用いるプロパティ名を入力する", 306 | "Toggle edit mode (shift-click)": "編集モードを切り替える(シフトキーを押しながらクリック)", 307 | "View Fullscreen": "フルスクリーン表示", 308 | "Whether to display or not polygons paths.": "ポリゴンの外周線を表示するかどうか", 309 | "Whether to fill polygons with color.": "ポリゴンを塗りつぶすかどうか", 310 | "Zoom to this place": "この場所にズーム", 311 | "always": "常時", 312 | "clear": "クリア", 313 | "define": "指定", 314 | "hidden": "隠す", 315 | "never": "表示しない", 316 | "Automatic": "自動", 317 | "Clone this feature": "この地物を複製", 318 | "Display label": "ラベルを表示", 319 | "Drag to reorder": "ドラッグして並べ替える", 320 | "Iframe with custom height and width (in px): {{{http://iframe.url.com|height*width}}}": "Iframeの縦幅および横幅を指定(ピクセル): {{{http://iframe.url.com|height*width}}}", 321 | "Labels are clickable": "ラベルをクリックしてポップアップを表示", 322 | "Label direction": "ラベルの位置", 323 | "Manage layers": "レイヤ管理", 324 | "On the bottom": "下寄せ", 325 | "On the left": "左寄せ", 326 | "On the right": "右寄せ", 327 | "On the top": "上寄せ", 328 | "Only display label on mouse hover": "カーソルを乗せたときにラベルを表示", 329 | "Open link in…": "リンクの開き方", 330 | "Unable to detect format of file {filename}": "ファイル形式を認識できません {filename}", 331 | "collapsed": "ボタン", 332 | "expanded": "リスト", 333 | "iframe": "iframe", 334 | "new window": "新規ウィンドウ", 335 | "parent window": "親ウィンドウ", 336 | "{count} errors during import: {message}": "インポートで {count} 個のエラー: {message}", 337 | "Are you sure you want to delete this layer?": "本当にこのレイヤを削除してよいですか?", 338 | "Delete layer": "レイヤを削除", 339 | "Error while fetching {url}": "{url} の取得エラー", 340 | "Home": "ホーム", 341 | "Delete all layers": "すべてのレイヤを削除", 342 | "Full map data": "Full map data", 343 | "Smart transitions": "Smart transitions", 344 | "Activate slideshow mode": "Activate slideshow mode", 345 | "Data is browsable": "Data is browsable", 346 | "Delay between two transitions when in play mode": "Delay between two transitions when in play mode", 347 | "Set it to false to hide this layer from the slideshow, the data browser, the popup navigation…": "Set it to false to hide this layer from the slideshow, the data browser, the popup navigation…", 348 | "{delay} seconds": "{delay} seconds", 349 | "Display measure": "Display measure" 350 | } -------------------------------------------------------------------------------- /src/locale/zh_TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "About": "關於", 3 | "Action not allowed :(": "行為不被允許:(", 4 | "Add a layer": "新增圖層", 5 | "Add symbol": "新增圖示", 6 | "Advanced actions": "進階動作", 7 | "Advanced properties": "進階屬性", 8 | "Allow scroll wheel zoom?": "允許捲動放大?", 9 | "An error occured": "發生錯誤", 10 | "Are you sure you want to cancel your changes?": "您確定要取消您所做的變更?", 11 | "Are you sure you want to clone this map and all its datalayers?": "您確定要複製此地圖及所有資料圖層?", 12 | "Are you sure you want to delete the feature?": "您確定要刪除該圖徵?", 13 | "Are you sure you want to delete this map?": "您確定要刪除此地圖?", 14 | "Ball": "球", 15 | "Bring feature to center": "將圖徵置中", 16 | "Browse data": "瀏覽資料", 17 | "Cancel": "取消", 18 | "Cancel edits": "取消編輯", 19 | "Center map on your location": "將您的位置設為地圖中心", 20 | "Change map background": "更改地圖背景", 21 | "Change symbol": "更改圖示", 22 | "Change tilelayers": "改變地圖磚圖層", 23 | "Choose the format of the data to import": "選擇匯入的資料格式", 24 | "Choose the layer of the feature": "選擇圖徵的圖層", 25 | "Choose the layer to import in": "選擇匯入圖層", 26 | "Circle": "圓圈", 27 | "Clone this map": "複製此地圖", 28 | "Default": "預設", 29 | "Delete": "刪除", 30 | "Delete this feature": "刪除此圖徵", 31 | "Disable editing": "停用編輯功能", 32 | "Display on load": "載入時顯示", 33 | "Do you want to display a minimap?": "您想要顯示小型地圖嗎?", 34 | "Do you want to display popup footer?": "您是否要顯示註腳彈出?", 35 | "Do you want to display the scale control?": "您是否要顯示尺標?", 36 | "Download data": "下載資料", 37 | "Draw a line": "描繪線條", 38 | "Draw a marker": "描繪標記", 39 | "Draw a polygon": "描繪多邊形", 40 | "Draw a polyline": "描繪折線", 41 | "Drop": "中止", 42 | "Dynamic": "動態", 43 | "Edit": "編輯", 44 | "Edit feature's layer": "編輯圖徵的圖層", 45 | "Edit map properties": "編輯地圖屬性", 46 | "Edit map settings": "編輯地圖設定值", 47 | "Edit this feature": "編輯此圖徵", 48 | "Embed and share this map": "將地圖內嵌並分享", 49 | "Enable editing": "啟用編輯功能", 50 | "Format": "格式", 51 | "From zoom": "由縮放大小", 52 | "Go to «{feature}»": "轉至 «{feature}»", 53 | "Hide controls": "隱藏控制列", 54 | "How much to simplify the polyline on each zoom level (more = better performance and smoother look, less = more accurate)": "在不同縮放比例下,多邊形的精簡程度 (精簡越多有較好的效率、多邊形越平滑,精簡越少圖形越精確)", 55 | "Import": "匯入", 56 | "Import data": "匯入資料", 57 | "Inherit": "繼承", 58 | "Licence": "授權", 59 | "Map background credits": "地圖背景取自", 60 | "Map user content has been published under licence": "使用者地圖資訊內容已經以以下授權發佈", 61 | "More controls": "更多控制項目", 62 | "Optional. Same as color if not set.": "可選,若您未選取顏色,則採用預設值", 63 | "Optionnal.": "選填", 64 | "Paste here your data": "在此貼入資料", 65 | "Please be sure the licence is compliant with your use.": "請再次確認所選的授權方式符合您的需求", 66 | "Problem in the response": "回應出現錯誤", 67 | "Problem in the response format": "回應的格式出現錯誤", 68 | "Provide an URL here": "提供 URL網址", 69 | "Remote data": "遠端資料", 70 | "Save": "儲存", 71 | "Save current edits": "儲存近期的變更", 72 | "Save this center and zoom": "保存地圖中心點位置與縮放大小", 73 | "Show/hide layer": "顯示/隱藏圖層", 74 | "Start editing": "開始編輯", 75 | "Stop editing": "停止編輯", 76 | "The zoom and center have been setted.": "已完成置中及切換功能設定", 77 | "To zoom": "至縮放大小", 78 | "Untitled layer": "未命名圖層", 79 | "Untitled map": "未命名地圖", 80 | "Update permissions and editors": "更新可編輯權限及編輯者", 81 | "Url": "網址", 82 | "User content credits": "使用者內容清單", 83 | "Where do we go from here?": "我們要去哪裡?", 84 | "Zoom in": "放大", 85 | "Zoom out": "縮小", 86 | "Zoom to layer extent": "切換至圖層範圍", 87 | "Zoom to this feature": "縮放至圖徵範圍", 88 | "color": "色彩", 89 | "dash array": "虛線排列", 90 | "description": "描述", 91 | "fill": "填入", 92 | "fill color": "填入色彩", 93 | "fill opacity": "填入不透明度", 94 | "inherit": "繼承", 95 | "licence": "授權", 96 | "name": "名稱", 97 | "no": "否", 98 | "opacity": "不透明度", 99 | "stroke": "筆畫粗細", 100 | "weight": "寬度", 101 | "yes": "是", 102 | "Editing": "編輯", 103 | "Embed the map": "嵌入地圖", 104 | "Short URL": "短網址", 105 | "# one hash for main heading": "單個 # 代表主標題", 106 | "## two hashes for second heading": "兩個 # 代表次標題", 107 | "### three hashes for third heading": "三個 # 代表第三標題", 108 | "**double star for bold**": "** 重複兩次星號代表粗體 **", 109 | "*simple star for italic*": "*單個星號代表斜體*", 110 | "--- for an horizontal rule": "-- 代表水平線", 111 | "All properties are imported.": "所有物件皆已匯入", 112 | "Comma, tab or semi-colon separated values. SRS WGS84 is implied. Only Point geometries are imported. The import will look at the column headers for any mention of «lat» and «lon» at the begining of the header, case insensitive. All other column are imported as properties.": "使用逗號、定位鍵或是分號分隔的地理數據。預設座標系為 SRS WGS84。只會匯入地理座標點。匯入時抓取 «lat» 與 «lon» 開頭的欄位資料,不分大小寫。其他欄位則歸入屬性資料。", 113 | "Custom background": "自訂背景", 114 | "Help": "幫助", 115 | "Image: {{http://image.url.com}}": "圖像: {{http://image.url.com}}", 116 | "Link with text: [[http://example.com|text of the link]]": "帶有超連結的文字: [[http://example.com|text of the link]]", 117 | "Properties imported:": "屬性匯入完成", 118 | "Simple link: [[http://example.com]]": "簡單連結: [[http://example.com]]", 119 | "Supported scheme": "支援的模板", 120 | "Supported variables that will be dynamically replaced": "支援的物件將直接動態轉換", 121 | "Text formatting": "文字格式", 122 | "attribution": "表彰", 123 | "display name": "顯示名稱", 124 | "max zoom": "放到最大", 125 | "min zoom": "縮至最小", 126 | "Skipping unkown geometry.type: {type}": "略過未知的地理資料格式 geometry.type: {type}", 127 | "Please save the map before": "請先儲存您的地圖", 128 | "You can use feature properties as variables: ex.: with \"http://myserver.org/images/{name}.png\", the {name} variable will be replaced by the \"name\" value of each markers.": "您可以圖徵屬性作為變數使用:例如 \"http://myserver.org/images/{name}.png\", 這裡 {name} 是個變數,會以圖標的 \"name\" 值來取代。", 129 | "Transform to polygon": "轉換為多邊形", 130 | "Transform to lines": "轉換為線條", 131 | "Choose the data format": "選擇資料格式", 132 | "Error in the tilelayer URL": "地圖磚圖層 URL 錯誤", 133 | "Directions from here": "從此處開始導航", 134 | "Choose a preset": "選擇一種預設值", 135 | "Limit bounds": "限制範圍", 136 | "Use current bounds": "使用目前範圍", 137 | "max East": "最東方", 138 | "max North": "最北方", 139 | "max South": "最南方", 140 | "max West": "最西方", 141 | "TMS format": "TMS 格式", 142 | "Credits": "工作人員名單", 143 | "Only visible features will be downloaded.": "只有可見的圖徵會被下載", 144 | "Open this map extent in a map editor to provide more accurate data to OpenStreetMap": "在地圖編輯器中打開此地圖,提供更多準確資料給 OpenStreetMap", 145 | "Default properties": "預設屬性", 146 | "User interface options": "使用者界面選項", 147 | "Image with custom width (in px): {{http://image.url.com|width}}": "自訂圖像的寬度(像素): {{http://image.url.com|width}}", 148 | "Current view instead of default map view?": "將目前視點設為預設視點?", 149 | "Iframe export options": "Iframe 匯出選項", 150 | "Include full screen link?": "是否包含全螢幕的連結?", 151 | "See full screen": "觀看全螢幕", 152 | "height": "高度", 153 | "width": "寬度", 154 | "Clustered": "群集後", 155 | "Clustering radius": "群集分析半徑", 156 | "GeoRSS (only link)": "GeoRSS (只有連結)", 157 | "GeoRSS (title + image)": "GeoRSS (標題與圖片)", 158 | "Heatmap": "熱點圖", 159 | "Heatmap radius": "熱點圖半徑", 160 | "Name and description": "名稱與說明", 161 | "Override clustering radius (default 80)": "覆蓋群集分析半徑 (預設80)", 162 | "Override heatmap radius (default 25)": "覆蓋指定熱圖 heatmap 半徑 (預設 25)", 163 | "Proxy request": "使用代理請求", 164 | "Table": "表格", 165 | "To use if remote server doesn't allow cross domain (slower)": "如果遠端伺服器不允許跨網域存取時使用 (效率較差)", 166 | "Type of layer": "圖層類型", 167 | "Filter…": "篩選器", 168 | "Heatmap intensity property": "熱點圖強度屬性", 169 | "Optional intensity property for heatmap": "選用的熱圖 heatmap 強度屬性", 170 | "Caption": "標題", 171 | "Data browser": "資料檢視器", 172 | "Do you want to display a caption bar?": "您是否要顯示標題列?", 173 | "Do you want to display a panel on load?": "您是否要顯示", 174 | "None": "以上皆非", 175 | "by": "由", 176 | "Name and description (large)": "名稱和敘述(大型)", 177 | "Empty": "空白", 178 | "Split line": "分隔線", 179 | "Clone": "複製", 180 | "Clone of {name}": "複製 {name}", 181 | "Side panel": "側邊框", 182 | "Powered by Leaflet and Django, glued by uMap project.": "使用 LeafletDjango 技術﹐由 uMap 計畫 整合。", 183 | "Zoom level for automatic zooms": "自動縮放的比例大小", 184 | "Do you want to display the «more» control?": "您是否要顯示 《更多》?", 185 | "Auto": "自動", 186 | "Default: name": "預設: name", 187 | "Property to use for sorting features": "排序圖徵所使用的屬性", 188 | "Slideshow": "投影片", 189 | "Start slideshow": "開啟投影片", 190 | "Stop slideshow": "中止投影片", 191 | "Text color for the cluster label": "叢集標籤的文字顏色", 192 | "Zoom to the next": "切換至下一頁", 193 | "Zoom to the previous": "切換至前一頁", 194 | "Add a new property": "新增屬性", 195 | "Are you sure you want to delete this property on all the features?": "您確定要刪除所有圖徵中的此項屬性?", 196 | "Close": "關閉", 197 | "Delete this property on all the features": "從所有圖徵中刪除此屬性", 198 | "Edit properties in a table": "在表格中編輯屬性", 199 | "Please enter the name of the property": "請輸入物件名稱", 200 | "Please enter the new name of this property": "請輸入新的物件名稱", 201 | "Rename this property on all the features": "在所有特徵中重新命名該物件", 202 | "If false, the polygon will act as a part of the underlying map.": "選擇「否」時,多邊形物件會被當成為底圖的一部分。", 203 | "Iframe with custom height (in px): {{{http://iframe.url.com|height}}}": "自訂 iframe 高度 (以 px 為單位): {{{http://iframe.url.com|height}}}", 204 | "Iframe: {{{http://iframe.url.com}}}": "Iframe: {{{http://iframe.url.com}}}", 205 | "See all": "觀看完整內容", 206 | "Dynamic properties": "動態屬性", 207 | "Use placeholders with feature properties between brackets, eg. {name}, they will be dynamically replaced by the corresponding values.": "以圖徵的屬性加上括弧作為標記,例如 {name},這樣子就可以動態被取代成對應的數值。", 208 | "Long credits": "詳細工作人員名單", 209 | "No licence has been set": "尚未設定授權條例", 210 | "Popup content template": "彈出內文範本", 211 | "Short credits": "簡短工作人員名單", 212 | "Will be displayed in the bottom right corner of the map": "將會顯示在地圖的右下角", 213 | "Will be visible in the caption of the map": "標題將出現在地圖上", 214 | "Map has been saved!": "地圖儲存已完成", 215 | "Save anyway": "通通都儲存吧!", 216 | "Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.": "糟糕,好像有人正在進行編輯,您仍可儲存資料,但您做的變更將被他人修改取代。", 217 | "Comma separated list of properties to use when filtering features": "以逗號分開列出篩選時要使用的屬性", 218 | "Keep current visible layers": "保留目前可見圖層", 219 | "Coordinates": "座標", 220 | "Latitude": "緯度", 221 | "Longitude": "經度", 222 | "Continue line (Ctrl-click)": "連續線(Ctrl+點擊鍵)", 223 | "Start a hole here": "開始一個凹洞", 224 | "Click last point to finish shape": "點下最後一點後完成外形", 225 | "Click to add a marker": "點選以新增標記", 226 | "Click to continue drawing": "點擊以繼續繪製", 227 | "Click to start drawing a line": "點擊以開始繪製直線", 228 | "Click to start drawing a polygon": "點選開始繪製多邊形", 229 | "Import in a new layer": "匯入至新圖層", 230 | "Layer": "圖層", 231 | "Please choose a format": "請選擇地圖格式", 232 | "Imports all umap data, including layers and settings.": "匯入所有 umap 資料,包含圖層與設定。", 233 | "Invalid umap data": "無效的 umap 資料", 234 | "Invalid umap data in {filename}": "無效的 umap 資料於檔案 {filename}", 235 | "Add a line to the current multi": "新增線段", 236 | "Add a polygon to the current multi": "新增多邊形", 237 | "Click to edit": "點擊開始編輯", 238 | "Continue line": "連續線段", 239 | "Delete this shape": "刪除外形", 240 | "Make main shape": "設為主要外形", 241 | "Merge lines": "合併線段", 242 | "Remove shape from the multi": "移除外形", 243 | "Transfer shape to edited feature": "將外形加到編輯中的特徵", 244 | "next": "下一個", 245 | "previous": "前一個", 246 | "Measure distances": "測量距離", 247 | "NM": "NM", 248 | "kilometers": "公里", 249 | "km": "km", 250 | "mi": "mi", 251 | "miles": "英里", 252 | "nautical miles": "海浬", 253 | "{area} acres": "{area} 弧線", 254 | "{area} ha": "{area} ha", 255 | "{area} m²": "{area} 平方公尺", 256 | "{area} mi²": "{area} 平方英哩", 257 | "{area} yd²": "{area} 平方英呎", 258 | "{distance} NM": "{距離} NM", 259 | "{distance} km": "{距離} km", 260 | "{distance} m": "{distance} 公尺", 261 | "{distance} miles": "{distance} 英哩", 262 | "{distance} yd": "{distance} 英呎", 263 | "Are you sure you want to restore this version?": "您確定要回復此一版本嗎?", 264 | "Extract shape to separate feature": "由外形分離出圖徵", 265 | "Layer properties": "圖層屬性", 266 | "Restore this version": "回復此版本", 267 | "Versions": "版本", 268 | "You have unsaved changes.": "您有變更尚未儲存", 269 | "A comma separated list of numbers that defines the stroke dash pattern. Ex.: \"5, 10, 15\".": "用逗號來分隔一列列的虛線模式,例如:\"5, 10, 15\"。", 270 | "Advanced transition": "進階轉換", 271 | "Allow interactions": "允許互動", 272 | "Autostart when map is loaded": "當讀取地圖時自動啟動", 273 | "Default interaction options": "預設互動選項", 274 | "Default shape properties": "預設形狀屬性", 275 | "Default zoom level": "預設縮放等級", 276 | "Define link to open in a new window on polygon click.": "指定點多邊形連結時開新視窗。", 277 | "Delete this vertex (Alt-click)": "刪除頂點 (Alt-click)", 278 | "Display the control to open OpenStreetMap editor": "顯示開啟開放街圖編輯器的按鍵", 279 | "Display the data layers control": "顯示資料圖層鍵", 280 | "Display the embed control": "顯示嵌入鍵", 281 | "Display the fullscreen control": "顯示全螢幕鍵", 282 | "Display the locate control": "顯示定位鍵", 283 | "Display the measure control": "顯示比例尺鍵", 284 | "Display the search control": "顯示搜尋鍵", 285 | "Display the tile layers control": "顯示圖層鍵", 286 | "Display the zoom control": "顯示縮放鍵", 287 | "Exit Fullscreen": "結束全螢幕模式", 288 | "Fetch data each time map view changes.": "每次地圖檢視改變時截取資料。", 289 | "Filter keys": "篩選鍵", 290 | "Icon shape": "圖示圖形", 291 | "Icon symbol": "圖示標誌", 292 | "Interaction options": "互動選項", 293 | "Label key": "標籤鍵", 294 | "Link to…": "連結至...", 295 | "Must be a valid CSS value (eg.: DarkBlue or #123456)": "必須是有效的 CSS 值 (例如:DarkBlue 或是 #123456)", 296 | "No results": "沒有結果", 297 | "Popup style": "彈出視窗樣式", 298 | "Replace layer content": "取代圖層內容", 299 | "Save this location as new feature": "將地點存為新的圖徵", 300 | "Search location": "搜尋地點", 301 | "Set URL": "設定網址", 302 | "Shape properties": "形狀屬性", 303 | "Simplify": "簡化", 304 | "Sort key": "排序鍵", 305 | "The name of the property to use as feature label (ex.: \"nom\")": "用作圖徵標籤的屬性名稱 (例如:“nom”)", 306 | "Toggle edit mode (shift-click)": "切換編輯模式 (shift-click)", 307 | "View Fullscreen": "以全屏幕模式顯示", 308 | "Whether to display or not polygons paths.": "是否顯示多邊形的路徑。", 309 | "Whether to fill polygons with color.": "是否將多邊形填入色彩。", 310 | "Zoom to this place": "縮放到這個地方", 311 | "always": "經常", 312 | "clear": "清除", 313 | "define": "定義", 314 | "hidden": "隱藏", 315 | "never": "永不", 316 | "Automatic": "自動", 317 | "Clone this feature": "複製此項目", 318 | "Display label": "顯示標籤", 319 | "Drag to reorder": "拖拽以排序", 320 | "Iframe with custom height and width (in px): {{{http://iframe.url.com|height*width}}}": "自訂 iframe 高度和寬度 (以 px 為單位):{{{http://iframe.url.com|height*width}}}", 321 | "Labels are clickable": "標籤可點擊", 322 | "Label direction": "標籤方向", 323 | "Manage layers": "管理圖層", 324 | "On the bottom": "在底部", 325 | "On the left": "在左側", 326 | "On the right": "在右側", 327 | "On the top": "在頂部", 328 | "Only display label on mouse hover": "僅在滑鼠移至標籤時顯示", 329 | "Open link in…": "開啓連結於...", 330 | "Unable to detect format of file {filename}": "無法偵測 {filename} 的檔案格式", 331 | "collapsed": "收起", 332 | "expanded": "展開", 333 | "iframe": "iframe", 334 | "new window": "新視窗", 335 | "parent window": "父視窗", 336 | "{count} errors during import: {message}": "於匯入時發生 {count} 項錯誤: {message}", 337 | "Are you sure you want to delete this layer?": "你確定要刪除這個圖層嗎?", 338 | "Delete layer": "刪除圖層", 339 | "Error while fetching {url}": "擷取網址時發生錯誤 {url}", 340 | "Home": "首頁", 341 | "Delete all layers": "刪除所有圖層", 342 | "Full map data": "全部地圖資料", 343 | "Smart transitions": "智慧轉換", 344 | "Activate slideshow mode": "開啟幻燈片模式", 345 | "Data is browsable": "資料是可檢視的", 346 | "Delay between two transitions when in play mode": "播放模式下兩個轉換間會延遲", 347 | "Set it to false to hide this layer from the slideshow, the data browser, the popup navigation…": "設定為假時,在幻燈片時、資料檢視器和彈出式導航中可將此圖層隱藏...", 348 | "{delay} seconds": "{delay} 秒", 349 | "Display measure": "Display measure" 350 | } -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": true, 4 | "happen": true, 5 | "assert": true, 6 | "before": true, 7 | "after": true, 8 | "it": true, 9 | "sinon": true, 10 | "qs": true, 11 | "enableEdit": true, 12 | "disableEdit": true, 13 | "changeInputValue": true, 14 | "resetMap": true, 15 | "initMap": true, 16 | "clickCancel": true, 17 | "map": true, 18 | "qs": true, 19 | "qsa": true, 20 | "qst": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/Controls.js: -------------------------------------------------------------------------------- 1 | describe('L.Storage.Controls', function(){ 2 | 3 | before(function () { 4 | this.server = sinon.fakeServer.create(); 5 | this.server.respondWith('/datalayer/62/', JSON.stringify(RESPONSES.datalayer62_GET)); 6 | this.map = initMap({storage_id: 99}); 7 | this.server.respond(); 8 | this.datalayer = this.map.getDataLayerByStorageId(62); 9 | }); 10 | after(function () { 11 | this.server.restore(); 12 | resetMap(); 13 | }); 14 | 15 | describe('#databrowser()', function(){ 16 | 17 | it('should be opened at datalayer button click', function() { 18 | var button = qs('.storage-browse-actions .storage-browse-link'); 19 | assert.ok(button); 20 | happen.click(button); 21 | assert.ok(qs('#storage-ui-container .storage-browse-data')); 22 | }); 23 | 24 | it('should contain datalayer section', function() { 25 | assert.ok(qs('#browse_data_datalayer_62')); 26 | }); 27 | 28 | it('should contain datalayer\'s features list', function() { 29 | assert.equal(qsa('#browse_data_datalayer_62 ul li').length, 3); 30 | }); 31 | 32 | it('should redraw datalayer\'s features list at feature delete', function() { 33 | var oldConfirm = window.confirm; 34 | window.confirm = function () {return true;}; 35 | enableEdit(); 36 | happen.once(qs('path[fill="DarkBlue"]'), {type: 'contextmenu'}); 37 | happen.click(qs('.leaflet-contextmenu .storage-delete')); 38 | assert.equal(qsa('#browse_data_datalayer_62 ul li').length, 2); 39 | window.confirm = oldConfirm; 40 | }); 41 | 42 | it('should redraw datalayer\'s features list on edit cancel', function() { 43 | clickCancel(); 44 | happen.click(qs('.storage-browse-actions .storage-browse-link')); 45 | assert.equal(qsa('#browse_data_datalayer_62 ul li').length, 3); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/DataLayer.js: -------------------------------------------------------------------------------- 1 | describe('L.DataLayer', function () { 2 | var path = '/map/99/datalayer/edit/62/'; 3 | 4 | before(function () { 5 | this.server = sinon.fakeServer.create(); 6 | this.server.respondWith('GET', '/datalayer/62/', JSON.stringify(RESPONSES.datalayer62_GET)); 7 | this.map = initMap({storage_id: 99}); 8 | this.datalayer = this.map.getDataLayerByStorageId(62); 9 | this.server.respond(); 10 | enableEdit(); 11 | }); 12 | after(function () { 13 | this.server.restore(); 14 | resetMap(); 15 | }); 16 | 17 | describe('#init()', function () { 18 | 19 | it('should be added in datalayers index', function () { 20 | assert.notEqual(this.map.datalayers_index.indexOf(this.datalayer), -1); 21 | }); 22 | 23 | }); 24 | 25 | describe('#edit()', function () { 26 | var editButton, form, input, forceButton; 27 | 28 | it('row in control should be active', function () { 29 | assert.notOk(qs('.leaflet-control-browse #browse_data_toggle_' + L.stamp(this.datalayer) + '.off')); 30 | }); 31 | 32 | it('should have edit button', function () { 33 | editButton = qs('#browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-edit'); 34 | assert.ok(editButton); 35 | }); 36 | 37 | it('should have toggle visibility element', function () { 38 | assert.ok(qs('.leaflet-control-browse i.layer-toggle')); 39 | }); 40 | 41 | it('should exist only one datalayer', function () { 42 | assert.equal(qsa('.leaflet-control-browse i.layer-toggle').length, 1); 43 | }); 44 | 45 | it('should build a form on edit button click', function () { 46 | happen.click(editButton); 47 | form = qs('form.storage-form'); 48 | input = qs('form.storage-form input[name="name"]'); 49 | assert.ok(form); 50 | assert.ok(input); 51 | }); 52 | 53 | it('should update name on input change', function () { 54 | var new_name = 'This is a new name'; 55 | input.value = new_name; 56 | happen.once(input, {type: 'input'}); 57 | assert.equal(this.datalayer.options.name, new_name); 58 | }); 59 | 60 | it('should have made datalayer dirty', function () { 61 | assert.ok(this.datalayer.isDirty); 62 | assert.notEqual(this.map.dirty_datalayers.indexOf(this.datalayer), -1); 63 | }); 64 | 65 | it('should have made Map dirty', function () { 66 | assert.ok(this.map.isDirty); 67 | }); 68 | 69 | it('should call datalayer.save on save button click', function (done) { 70 | sinon.spy(this.datalayer, 'save'); 71 | this.server.flush(); 72 | this.server.respondWith('POST', '/map/99/update/settings/', JSON.stringify({id: 99})); 73 | this.server.respondWith('POST', '/map/99/datalayer/update/62/', JSON.stringify(defaultDatalayerData())); 74 | clickSave(); 75 | this.server.respond(); 76 | this.server.respond(); 77 | assert(this.datalayer.save.calledOnce); 78 | this.datalayer.save.restore(); 79 | done(); 80 | }); 81 | 82 | it('should show alert if server respond 412', function () { 83 | cleanAlert(); 84 | this.server.flush(); 85 | this.server.respondWith('POST', '/map/99/update/settings/', JSON.stringify({id: 99})); 86 | this.server.respondWith('POST', '/map/99/datalayer/update/62/', [412, {}, '']); 87 | happen.click(editButton); 88 | input = qs('form.storage-form input[name="name"]'); 89 | input.value = 'a new name'; 90 | happen.once(input, {type: 'input'}); 91 | clickSave(); 92 | this.server.respond(); 93 | this.server.respond(); 94 | assert(L.DomUtil.hasClass(this.map._container, 'storage-alert')); 95 | assert.notEqual(this.map.dirty_datalayers.indexOf(this.datalayer), -1); 96 | forceButton = qs('#storage-alert-container .storage-action'); 97 | assert.ok(forceButton); 98 | }); 99 | 100 | it('should save anyway on force save button click', function () { 101 | sinon.spy(this.map, 'continueSaving'); 102 | happen.click(forceButton); 103 | this.server.flush(); 104 | this.server.respond('POST', '/map/99/datalayer/update/62/', JSON.stringify(defaultDatalayerData())); 105 | assert.notOk(qs('#storage-alert-container .storage-action')); 106 | assert(this.map.continueSaving.calledOnce); 107 | this.map.continueSaving.restore(); 108 | assert.equal(this.map.dirty_datalayers.indexOf(this.datalayer), -1); 109 | }); 110 | 111 | }); 112 | 113 | describe('#save() new', function () { 114 | var newLayerButton, form, input, newDatalayer, editButton, manageButton; 115 | 116 | it('should have a manage datalayers action', function () { 117 | enableEdit(); 118 | manageButton = qs('.manage-datalayers'); 119 | assert.ok(manageButton); 120 | happen.click(manageButton); 121 | }); 122 | 123 | it('should have a new layer button', function () { 124 | newLayerButton = qs('#storage-ui-container .add-datalayer'); 125 | assert.ok(newLayerButton); 126 | }); 127 | 128 | it('should build a form on new layer button click', function () { 129 | happen.click(newLayerButton); 130 | form = qs('form.storage-form'); 131 | input = qs('form.storage-form input[name="name"]'); 132 | assert.ok(form); 133 | assert.ok(input); 134 | }); 135 | 136 | it('should have an empty name', function () { 137 | assert.notOk(input.value); 138 | }); 139 | 140 | it('should have created a new datalayer', function () { 141 | assert.equal(this.map.datalayers_index.length, 2); 142 | newDatalayer = this.map.datalayers_index[1]; 143 | }); 144 | 145 | it('should have made Map dirty', function () { 146 | assert.ok(this.map.isDirty); 147 | }); 148 | 149 | it('should update name on input change', function () { 150 | var new_name = 'This is a new name'; 151 | input.value = new_name; 152 | happen.once(input, {type: 'input'}); 153 | assert.equal(newDatalayer.options.name, new_name); 154 | }); 155 | 156 | it('should set storage_id on save callback', function () { 157 | assert.notOk(newDatalayer.storage_id); 158 | this.server.flush(); 159 | this.server.respondWith('POST', '/map/99/update/settings/', JSON.stringify({id: 99})); 160 | this.server.respondWith('POST', '/map/99/datalayer/create/', JSON.stringify(defaultDatalayerData({id: 63}))); 161 | clickSave(); 162 | this.server.respond(); 163 | this.server.respond(); // First respond will then trigger another Xhr request (continueSaving) 164 | assert.equal(newDatalayer.storage_id, 63); 165 | }); 166 | 167 | it('should have unset map dirty', function () { 168 | assert.notOk(this.map.isDirty); 169 | }); 170 | 171 | it('should have edit button', function () { 172 | editButton = qs('#browse_data_toggle_' + L.stamp(newDatalayer) + ' .layer-edit'); 173 | assert.ok(editButton); 174 | }); 175 | 176 | it('should call update if we edit again', function () { 177 | happen.click(editButton); 178 | assert.notOk(this.map.isDirty); 179 | input = qs('form.storage-form input[name="name"]'); 180 | input.value = 'a new name again but we don\'t care which'; 181 | happen.once(input, {type: 'input'}); 182 | assert.ok(this.map.isDirty); 183 | var response = function (request) { 184 | return request.respond(200, {}, JSON.stringify(defaultDatalayerData({pk: 63}))); 185 | }; 186 | var spy = sinon.spy(response); 187 | this.server.flush(); 188 | this.server.respondWith('POST', '/map/99/update/settings/', JSON.stringify({id: 99})); 189 | this.server.respondWith('POST', '/map/99/datalayer/update/63/', spy); 190 | clickSave(); 191 | this.server.respond(); 192 | this.server.respond(); 193 | assert.ok(spy.calledOnce); 194 | }); 195 | 196 | }); 197 | 198 | describe('#iconClassChange()', function () { 199 | 200 | it('should change icon class', function () { 201 | happen.click(qs('[data-id="' + this.datalayer._leaflet_id +'"] .layer-edit')); 202 | changeSelectValue(qs('form#datalayer-advanced-properties select[name=iconClass]'), 'Circle'); 203 | assert.notOk(qs('div.storage-div-icon')); 204 | assert.ok(qs('div.storage-circle-icon')); 205 | clickCancel(); 206 | }); 207 | 208 | }); 209 | 210 | describe('#show/hide', function () { 211 | 212 | it('should hide features on hide', function () { 213 | assert.ok(qs('div.storage-div-icon')); 214 | assert.ok(qs('path[fill="none"]')); 215 | this.datalayer.hide(); 216 | assert.notOk(qs('div.storage-div-icon')); 217 | assert.notOk(qs('path[fill="none"]')); 218 | }); 219 | 220 | it('should show features on show', function () { 221 | assert.notOk(qs('div.storage-div-icon')); 222 | assert.notOk(qs('path[fill="none"]')); 223 | this.datalayer.show(); 224 | assert.ok(qs('div.storage-div-icon')); 225 | assert.ok(qs('path[fill="none"]')); 226 | }); 227 | 228 | }); 229 | 230 | describe('#clone()', function () { 231 | 232 | it('should clone everything but the id and the name', function () { 233 | enableEdit(); 234 | var clone = this.datalayer.clone(); 235 | assert.notOk(clone.storage_id); 236 | assert.notEqual(clone.options.name, this.datalayer.name); 237 | assert.ok(clone.options.name); 238 | assert.equal(clone.options.color, this.datalayer.options.color); 239 | assert.equal(clone.options.stroke, this.datalayer.options.stroke); 240 | clone._delete(); 241 | clickSave(); 242 | }); 243 | 244 | }); 245 | 246 | describe('#restore()', function () { 247 | var oldConfirm, 248 | newConfirm = function () { 249 | return true; 250 | }; 251 | 252 | before(function () { 253 | oldConfirm = window.confirm; 254 | window.confirm = newConfirm; 255 | }); 256 | after(function () { 257 | window.confirm = oldConfirm; 258 | }); 259 | 260 | it('should restore everything', function () { 261 | enableEdit(); 262 | var geojson = L.Util.CopyJSON(RESPONSES.datalayer62_GET); 263 | geojson.features.push({ 264 | 'geometry': { 265 | 'type': 'Point', 266 | 'coordinates': [-1.274658203125, 50.57634993749885] 267 | }, 268 | 'type': 'Feature', 269 | 'id': 1807, 270 | 'properties': {_storage_options: {}, name: 'new point from restore'} 271 | }); 272 | geojson._storage.color = 'Chocolate'; 273 | this.server.respondWith('GET', '/datalayer/62/olderversion.geojson', JSON.stringify(geojson)); 274 | sinon.spy(window, 'confirm'); 275 | this.datalayer.restore('olderversion.geojson'); 276 | this.server.respond(); 277 | assert(window.confirm.calledOnce); 278 | window.confirm.restore(); 279 | assert.equal(this.datalayer.storage_id, 62); 280 | assert.ok(this.datalayer.isDirty); 281 | assert.equal(this.datalayer._index.length, 4); 282 | assert.ok(qs('path[fill="Chocolate"]')); 283 | }); 284 | 285 | it('should revert anything on cancel click', function () { 286 | clickCancel(); 287 | assert.equal(this.datalayer._index.length, 3); 288 | assert.notOk(qs('path[fill="Chocolate"]')); 289 | }); 290 | 291 | }); 292 | 293 | describe('#delete()', function () { 294 | var deleteLink, deletePath = '/map/99/datalayer/delete/62/'; 295 | 296 | it('should have a delete link in update form', function () { 297 | enableEdit(); 298 | happen.click(qs('#browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-edit')); 299 | deleteLink = qs('a.delete_datalayer_button'); 300 | assert.ok(deleteLink); 301 | }); 302 | 303 | it('should delete features on datalayer delete', function () { 304 | happen.click(deleteLink); 305 | assert.notOk(qs('div.icon_container')); 306 | }); 307 | 308 | it('should have set map dirty', function () { 309 | assert.ok(this.map.isDirty); 310 | }); 311 | 312 | it('should delete layer control row on delete', function () { 313 | assert.notOk(qs('.leaflet-control-browse #browse_data_toggle_' + L.stamp(this.datalayer))); 314 | }); 315 | 316 | it('should be removed from map.datalayers_index', function () { 317 | assert.equal(this.map.datalayers_index.indexOf(this.datalayer), -1); 318 | }); 319 | 320 | it('should be removed from map.datalayers', function () { 321 | assert.notOk(this.map.datalayers[L.stamp(this.datalayer)]); 322 | }); 323 | 324 | it('should be visible again on edit cancel', function () { 325 | clickCancel(); 326 | assert.ok(qs('div.icon_container')); 327 | }); 328 | 329 | }); 330 | 331 | }); 332 | -------------------------------------------------------------------------------- /test/Feature.js: -------------------------------------------------------------------------------- 1 | describe('L.Storage.FeatureMixin', function () { 2 | 3 | before(function () { 4 | this.server = sinon.fakeServer.create(); 5 | this.server.respondWith('GET', '/datalayer/62/', JSON.stringify(RESPONSES.datalayer62_GET)); 6 | this.map = initMap({storage_id: 99}); 7 | this.datalayer = this.map.getDataLayerByStorageId(62); 8 | this.server.respond(); 9 | }); 10 | after(function () { 11 | this.server.restore(); 12 | resetMap(); 13 | }); 14 | 15 | describe('#edit()', function () { 16 | var link; 17 | 18 | it('should have datalayer features created', function () { 19 | assert.equal(document.querySelectorAll('#map > .leaflet-map-pane > .leaflet-overlay-pane path.leaflet-interactive').length, 2); 20 | assert.ok(qs('path[fill="none"]')); // Polyline 21 | assert.ok(qs('path[fill="DarkBlue"]')); // Polygon 22 | }); 23 | 24 | it('should take into account styles changes made in the datalayer', function () { 25 | enableEdit(); 26 | happen.click(qs('#browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-edit')); 27 | var colorInput = qs('form#datalayer-advanced-properties input[name=color]'); 28 | changeInputValue(colorInput, 'DarkRed'); 29 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 30 | assert.notOk(qs('path[fill="DarkBlue"]')); 31 | assert.ok(qs('path[fill="DarkRed"]')); 32 | }); 33 | 34 | it('should open a popup toolbar on feature click', function () { 35 | enableEdit(); 36 | happen.click(qs('path[fill="DarkRed"]')); 37 | var toolbar = qs('ul.leaflet-inplace-toolbar'); 38 | assert.ok(toolbar); 39 | link = qs('a.storage-toggle-edit', toolbar); 40 | assert.ok(link); 41 | }); 42 | 43 | it('should open a form on popup toolbar toggle edit click', function () { 44 | happen.click(link); 45 | var form = qs('form#storage-feature-properties'); 46 | var input = qs('form#storage-feature-properties input[name="name"]'); 47 | assert.ok(form); 48 | assert.ok(input); 49 | }); 50 | 51 | it('should not handle _storage_options has normal property', function () { 52 | assert.notOk(qs('form#storage-feature-properties input[name="_storage_options"]')); 53 | }); 54 | 55 | it('should give precedence to feature style over datalayer styles', function () { 56 | var input = qs('#storage-ui-container form input[name="color"]'); 57 | assert.ok(input); 58 | changeInputValue(input, 'DarkGreen'); 59 | assert.notOk(qs('path[fill="DarkRed"]')); 60 | assert.notOk(qs('path[fill="DarkBlue"]')); 61 | assert.ok(qs('path[fill="DarkGreen"]')); 62 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 63 | }); 64 | 65 | it('should remove stroke if set to no', function () { 66 | assert.notOk(qs('path[stroke="none"]')); 67 | var defineButton = qs('#storage-feature-shape-properties .formbox:nth-child(4) .define'); 68 | happen.click(defineButton); 69 | var input = qs('#storage-feature-shape-properties input[name="stroke"]'); 70 | assert.ok(input); 71 | input.checked = false; 72 | console.log(input, input.checked) 73 | happen.once(input, {type: 'change'}); 74 | assert.ok(qs('path[stroke="none"]')); 75 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 76 | }); 77 | 78 | it('should not override already set style on features', function () { 79 | happen.click(qs('#browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-edit')); 80 | changeInputValue(qs('#storage-ui-container form input[name=color]'), 'Chocolate'); 81 | assert.notOk(qs('path[fill="DarkBlue"]')); 82 | assert.notOk(qs('path[fill="DarkRed"]')); 83 | assert.notOk(qs('path[fill="Chocolate"]')); 84 | assert.ok(qs('path[fill="DarkGreen"]')); 85 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 86 | }); 87 | 88 | it('should reset style on cancel click', function () { 89 | clickCancel(); 90 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 91 | assert.ok(qs('path[fill="DarkBlue"]')); 92 | assert.notOk(qs('path[fill="DarkRed"]')); 93 | }); 94 | 95 | it('should set map.editedFeature on edit', function () { 96 | enableEdit(); 97 | assert.notOk(this.map.editedFeature); 98 | happen.click(qs('path[fill="DarkBlue"]')); 99 | happen.click(qs('ul.leaflet-inplace-toolbar a.storage-toggle-edit')); 100 | assert.ok(this.map.editedFeature); 101 | disableEdit(); 102 | }); 103 | 104 | it('should reset map.editedFeature on panel open', function (done) { 105 | enableEdit(); 106 | assert.notOk(this.map.editedFeature); 107 | happen.click(qs('path[fill="DarkBlue"]')); 108 | happen.click(qs('ul.leaflet-inplace-toolbar a.storage-toggle-edit')); 109 | assert.ok(this.map.editedFeature); 110 | this.map.displayCaption(); 111 | window.setTimeout(function () { 112 | assert.notOk(this.map.editedFeature); 113 | disableEdit(); 114 | done(); 115 | }, 1001); // CSS transition time. 116 | }); 117 | 118 | }); 119 | 120 | describe('#utils()', function () { 121 | var poly, marker; 122 | function setFeatures (datalayer) { 123 | datalayer.eachLayer(function (layer) { 124 | if (!poly && layer instanceof L.Polygon) { 125 | poly = layer; 126 | } 127 | if (!marker && layer instanceof L.Marker) { 128 | marker = layer; 129 | } 130 | }); 131 | } 132 | it('should generate a valid geojson', function () { 133 | setFeatures(this.datalayer); 134 | assert.ok(poly); 135 | console.log(poly.toGeoJSON().geometry); 136 | assert.deepEqual(poly.toGeoJSON().geometry, {'type': 'Polygon', 'coordinates': [[[11.25, 53.585984], [10.151367, 52.975108], [12.689209, 52.167194], [14.084473, 53.199452], [12.634277, 53.618579], [11.25, 53.585984], [11.25, 53.585984]]]}); 137 | // Ensure original latlngs has not been modified 138 | assert.equal(poly.getLatLngs()[0].length, 6); 139 | }); 140 | 141 | it('should remove empty _storage_options from exported geojson', function () { 142 | setFeatures(this.datalayer); 143 | assert.ok(poly); 144 | assert.deepEqual(poly.toGeoJSON().properties, {name: 'name poly'}); 145 | assert.ok(marker); 146 | assert.deepEqual(marker.toGeoJSON().properties, {_storage_options: {color: 'OliveDrab'}, name: 'test'}); 147 | }); 148 | 149 | }); 150 | 151 | describe('#changeDataLayer()', function () { 152 | 153 | it('should change style on datalayer select change', function () { 154 | enableEdit(); 155 | happen.click(qs('.manage-datalayers')); 156 | happen.click(qs('#storage-ui-container .add-datalayer')); 157 | changeInputValue(qs('form.storage-form input[name="name"]'), 'New layer'); 158 | changeInputValue(qs('form#datalayer-advanced-properties input[name=color]'), 'MediumAquaMarine'); 159 | happen.click(qs('path[fill="DarkBlue"]')); 160 | happen.click(qs('ul.leaflet-inplace-toolbar a.storage-toggle-edit')); 161 | var select = qs('select[name=datalayer]'); 162 | select.selectedIndex = 0; 163 | happen.once(select, {type: 'change'}); 164 | assert.ok(qs('path[fill="none"]')); // Polyline fill is unchanged 165 | assert.notOk(qs('path[fill="DarkBlue"]')); 166 | assert.ok(qs('path[fill="MediumAquaMarine"]')); 167 | clickCancel(); 168 | }); 169 | 170 | }); 171 | 172 | describe('#openPopup()', function () { 173 | 174 | it('should open a popup on click', function () { 175 | assert.notOk(qs('.leaflet-popup-content')); 176 | happen.click(qs('path[fill="DarkBlue"]')); 177 | var title = qs('.leaflet-popup-content'); 178 | assert.ok(title); 179 | assert.ok(title.innerHTML.indexOf('name poly')); 180 | }); 181 | 182 | }); 183 | 184 | describe('#properties()', function () { 185 | 186 | it('should rename property', function () { 187 | var poly = this.datalayer._lineToLayer({}, [[0, 0], [0, 1], [0, 2]]); 188 | poly.properties.prop1 = 'xxx'; 189 | poly.renameProperty('prop1', 'prop2'); 190 | assert.equal(poly.properties.prop2, 'xxx'); 191 | assert.ok(typeof poly.properties.prop1 === 'undefined'); 192 | }); 193 | 194 | it('should not create property when renaming', function () { 195 | var poly = this.datalayer._lineToLayer({}, [[0, 0], [0, 1], [0, 2]]); 196 | delete poly.properties.prop2; // Make sure it doesn't exist 197 | poly.renameProperty('prop1', 'prop2'); 198 | assert.ok(typeof poly.properties.prop2 === 'undefined'); 199 | }); 200 | 201 | it('should delete property', function () { 202 | var poly = this.datalayer._lineToLayer({}, [[0, 0], [0, 1], [0, 2]]); 203 | poly.properties.prop = 'xxx'; 204 | assert.equal(poly.properties.prop, 'xxx'); 205 | poly.deleteProperty('prop'); 206 | assert.ok(typeof poly.properties.prop === 'undefined'); 207 | }); 208 | 209 | }); 210 | 211 | describe('#matchFilter()', function () { 212 | var poly; 213 | 214 | it('should filter on properties', function () { 215 | poly = this.datalayer._lineToLayer({}, [[0, 0], [0, 1], [0, 2]]); 216 | poly.properties.name = 'mooring'; 217 | assert.ok(poly.matchFilter('moo', ['name'])); 218 | assert.notOk(poly.matchFilter('foo', ['name'])); 219 | }); 220 | 221 | it('should be case unsensitive', function () { 222 | assert.ok(poly.matchFilter('Moo', ['name'])); 223 | }); 224 | 225 | it('should match also in the middle of a string', function () { 226 | assert.ok(poly.matchFilter('oor', ['name'])); 227 | }); 228 | 229 | it('should handle multiproperties', function () { 230 | poly.properties.city = 'Teulada'; 231 | assert.ok(poly.matchFilter('eul', ['name', 'city', 'foo'])); 232 | }); 233 | 234 | }); 235 | 236 | }); 237 | -------------------------------------------------------------------------------- /test/Map.js: -------------------------------------------------------------------------------- 1 | describe('L.Storage.Map', function(){ 2 | 3 | before(function () { 4 | this.server = sinon.fakeServer.create(); 5 | this.server.respondWith('/datalayer/62/', JSON.stringify(RESPONSES.datalayer62_GET)); 6 | this.options = { 7 | storage_id: 99 8 | }; 9 | this.map = initMap({storage_id: 99}); 10 | this.server.respond(); 11 | this.datalayer = this.map.getDataLayerByStorageId(62); 12 | }); 13 | after(function () { 14 | this.server.restore(); 15 | clickCancel(); 16 | resetMap(); 17 | }); 18 | 19 | describe('#init()', function(){ 20 | 21 | it('should be initialized', function(){ 22 | assert.equal(this.map.options.storage_id, 99); 23 | }); 24 | 25 | it('should have created the edit button', function(){ 26 | assert.ok(qs('div.leaflet-control-edit-enable')); 27 | }); 28 | 29 | it('should have datalayer control div', function(){ 30 | assert.ok(qs('div.leaflet-control-browse')); 31 | }); 32 | 33 | it('should have datalayer actions div', function(){ 34 | assert.ok(qs('div.storage-browse-actions')); 35 | }); 36 | 37 | it('should have icon container div', function(){ 38 | assert.ok(qs('div.icon_container')); 39 | }); 40 | 41 | it('should hide icon container div when hiding datalayer', function() { 42 | var el = qs('.leaflet-control-browse #browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-toggle'); 43 | happen.click(el); 44 | assert.notOk(qs('div.icon_container')); 45 | }); 46 | 47 | it('enable edit on click on toggle button', function () { 48 | var el = qs('div.leaflet-control-edit-enable a'); 49 | happen.click(el); 50 | assert.isTrue(L.DomUtil.hasClass(document.body, 'storage-edit-enabled')); 51 | }); 52 | 53 | it('should have only one datalayer in its index', function () { 54 | assert.equal(this.map.datalayers_index.length, 1); 55 | }); 56 | }); 57 | 58 | describe('#editMetadata()', function () { 59 | var form, input; 60 | 61 | it('should build a form on editMetadata control click', function (done) { 62 | var button = qs('a.update-map-settings'); 63 | assert.ok(button); 64 | happen.click(button); 65 | form = qs('form.storage-form'); 66 | input = qs('form[class="storage-form"] input[name="name"]'); 67 | assert.ok(form); 68 | assert.ok(input); 69 | done(); 70 | }); 71 | 72 | it('should update map name on input change', function () { 73 | var new_name = 'This is a new name'; 74 | input.value = new_name; 75 | happen.once(input, {type: 'input'}); 76 | assert.equal(this.map.options.name, new_name); 77 | }); 78 | 79 | it('should have made Map dirty', function () { 80 | assert.ok(this.map.isDirty); 81 | }); 82 | 83 | it('should have added dirty class on map container', function () { 84 | assert.ok(L.DomUtil.hasClass(this.map._container, 'storage-is-dirty')); 85 | }); 86 | 87 | }); 88 | 89 | describe('#delete()', function () { 90 | var path = '/map/99/delete/', 91 | oldConfirm, 92 | newConfirm = function () { 93 | return true; 94 | }; 95 | 96 | before(function () { 97 | oldConfirm = window.confirm; 98 | window.confirm = newConfirm; 99 | }); 100 | after(function () { 101 | window.confirm = oldConfirm; 102 | }); 103 | 104 | it('should ask for confirmation on delete link click', function (done) { 105 | var button = qs('a.update-map-settings'); 106 | assert.ok(button, 'update map info button exists'); 107 | happen.click(button); 108 | var deleteLink = qs('a.storage-delete'); 109 | assert.ok(deleteLink, 'delete map button exists'); 110 | sinon.spy(window, 'confirm'); 111 | this.server.respondWith('POST', path, JSON.stringify({redirect: '#'})); 112 | happen.click(deleteLink); 113 | this.server.respond(); 114 | assert(window.confirm.calledOnce); 115 | window.confirm.restore(); 116 | done(); 117 | }); 118 | 119 | }); 120 | 121 | describe('#importData()', function () { 122 | var fileInput, textarea, submit, formatSelect, layerSelect, clearFlag; 123 | 124 | it('should build a form on click', function () { 125 | happen.click(qs('a.upload-data')); 126 | fileInput = qs('.storage-upload input[type="file"]'); 127 | textarea = qs('.storage-upload textarea'); 128 | submit = qs('.storage-upload input[type="button"]'); 129 | formatSelect = qs('.storage-upload select[name="format"]'); 130 | layerSelect = qs('.storage-upload select[name="datalayer"]'); 131 | assert.ok(fileInput); 132 | assert.ok(submit); 133 | assert.ok(textarea); 134 | assert.ok(formatSelect); 135 | assert.ok(layerSelect); 136 | }); 137 | 138 | it('should import geojson from textarea', function () { 139 | this.datalayer.empty() 140 | assert.equal(this.datalayer._index.length, 0); 141 | textarea.value = '{"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [6.922931671142578, 47.481161607175736]}, "type": "Feature", "properties": {"color": "", "name": "Chez R\u00e9my", "description": ""}}, {"geometry": {"type": "LineString", "coordinates": [[2.4609375, 48.88639177703194], [2.48291015625, 48.76343113791796], [2.164306640625, 48.719961222646276]]}, "type": "Feature", "properties": {"color": "", "name": "P\u00e9rif", "description": ""}}]}'; 142 | changeSelectValue(formatSelect, 'geojson'); 143 | happen.click(submit); 144 | assert.equal(this.datalayer._index.length, 2); 145 | }); 146 | 147 | it('should import kml from textarea', function () { 148 | this.datalayer.empty() 149 | happen.click(qs('a.upload-data')); 150 | textarea = qs('.storage-upload textarea'); 151 | submit = qs('.storage-upload input[type="button"]'); 152 | formatSelect = qs('.storage-upload select[name="format"]'); 153 | assert.equal(this.datalayer._index.length, 0); 154 | textarea.value = kml_example; 155 | changeSelectValue(formatSelect, 'kml'); 156 | happen.click(submit); 157 | assert.equal(this.datalayer._index.length, 3); 158 | }); 159 | 160 | it('should import gpx from textarea', function () { 161 | this.datalayer.empty() 162 | happen.click(qs('a.upload-data')); 163 | textarea = qs('.storage-upload textarea'); 164 | submit = qs('.storage-upload input[type="button"]'); 165 | formatSelect = qs('.storage-upload select[name="format"]'); 166 | assert.equal(this.datalayer._index.length, 0); 167 | textarea.value = gpx_example; 168 | changeSelectValue(formatSelect, 'gpx'); 169 | happen.click(submit); 170 | assert.equal(this.datalayer._index.length, 2); 171 | }); 172 | 173 | it('should import csv from textarea', function () { 174 | this.datalayer.empty() 175 | happen.click(qs('a.upload-data')); 176 | textarea = qs('.storage-upload textarea'); 177 | submit = qs('.storage-upload input[type="button"]'); 178 | formatSelect = qs('.storage-upload select[name="format"]'); 179 | assert.equal(this.datalayer._index.length, 0); 180 | textarea.value = csv_example; 181 | changeSelectValue(formatSelect, 'csv'); 182 | happen.click(submit); 183 | assert.equal(this.datalayer._index.length, 1); 184 | }); 185 | 186 | it('should replace content if asked so', function () { 187 | happen.click(qs('a.upload-data')); 188 | textarea = qs('.storage-upload textarea'); 189 | submit = qs('.storage-upload input[type="button"]'); 190 | formatSelect = qs('.storage-upload select[name="format"]'); 191 | clearFlag = qs('.storage-upload input[name="clear"]'); 192 | clearFlag.checked = true; 193 | assert.equal(this.datalayer._index.length, 1); 194 | textarea.value = csv_example; 195 | changeSelectValue(formatSelect, 'csv'); 196 | happen.click(submit); 197 | assert.equal(this.datalayer._index.length, 1); 198 | }); 199 | 200 | 201 | it('should import GeometryCollection from textarea', function () { 202 | this.datalayer.empty() 203 | textarea.value = '{"type": "GeometryCollection","geometries": [{"type": "Point","coordinates": [-80.66080570220947,35.04939206472683]},{"type": "Polygon","coordinates": [[[-80.66458225250244,35.04496519190309],[-80.66344499588013,35.04603679820616],[-80.66258668899536,35.045580049697556],[-80.66387414932251,35.044280059194946],[-80.66458225250244,35.04496519190309]]]},{"type": "LineString","coordinates": [[-80.66237211227417,35.05950973022538],[-80.66269397735596,35.0592638296087],[-80.66284418106079,35.05893010615862],[-80.66308021545409,35.05833291342246],[-80.66359519958496,35.057753281001425],[-80.66387414932251,35.05740198662245],[-80.66441059112549,35.05703312589789],[-80.66486120223999,35.056787217822475],[-80.66541910171509,35.05650617911516],[-80.66563367843628,35.05631296444281],[-80.66601991653441,35.055891403570705],[-80.66619157791138,35.05545227534804],[-80.66619157791138,35.05517123204622],[-80.66625595092773,35.05489018777713],[-80.6662130355835,35.054222703761525],[-80.6662130355835,35.05392409072499],[-80.66595554351807,35.05290528508858],[-80.66569805145262,35.052044560077285],[-80.66550493240356,35.0514824490509],[-80.665762424469,35.05048117920187],[-80.66617012023926,35.04972582715769],[-80.66651344299316,35.049286665781096],[-80.66692113876343,35.0485313026898],[-80.66700696945189,35.048215102112344],[-80.66707134246826,35.04777593261294],[-80.66704988479614,35.04738946150025],[-80.66696405410767,35.04698542156371],[-80.66681385040283,35.046353007216055],[-80.66659927368164,35.04596652937105],[-80.66640615463257,35.04561518428889],[-80.6659984588623,35.045193568195565],[-80.66552639007568,35.044877354697526],[-80.6649899482727,35.04454357245502],[-80.66449642181396,35.04417465365292],[-80.66385269165039,35.04387600387859],[-80.66303730010986,35.043717894732545]]}]}'; 204 | formatSelect = qs('.storage-upload select[name="format"]'); 205 | changeSelectValue(formatSelect, 'geojson'); 206 | happen.click(submit); 207 | assert.equal(this.datalayer._index.length, 3); 208 | }); 209 | 210 | it('should import multipolygon', function () { 211 | this.datalayer.empty() 212 | textarea.value = '{"type": "Feature", "properties": { "name": "Some states" }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[-109, 36], [-109, 40], [-102, 37], [-109, 36]], [[-108, 39], [-107, 37], [-104, 37], [-108, 39]]], [[[-119, 42], [-120, 39], [-114, 41], [-119, 42]]]] }}'; 213 | changeSelectValue(formatSelect, 'geojson'); 214 | happen.click(submit); 215 | assert.equal(this.datalayer._index.length, 1); 216 | var layer = this.datalayer.getFeatureByIndex(0); 217 | assert.equal(layer._latlngs.length, 2); // Two shapes. 218 | assert.equal(layer._latlngs[0].length, 2); // Hole. 219 | }); 220 | 221 | it('should import multipolyline', function () { 222 | this.datalayer.empty() 223 | textarea.value = '{"type": "FeatureCollection", "features": [{ "type": "Feature", "properties": {}, "geometry": { "type": "MultiLineString", "coordinates": [[[-108, 46], [-113, 43]], [[-112, 45], [-115, 44]]] } }]}'; 224 | changeSelectValue(formatSelect, 'geojson'); 225 | happen.click(submit); 226 | assert.equal(this.datalayer._index.length, 1); 227 | var layer = this.datalayer.getFeatureByIndex(0); 228 | assert.equal(layer._latlngs.length, 2); // Two shapes. 229 | }); 230 | 231 | it('should import raw umap data from textarea', function () { 232 | //Right now, the import function will try to save and reload. Stop this from happening. 233 | var disabledSaveFunction = this.map.save; 234 | this.map.save = function(){}; 235 | happen.click(qs('a.upload-data')); 236 | var initialLayerCount = Object.keys(this.map.datalayers).length; 237 | formatSelect = qs('.storage-upload select[name="format"]'); 238 | textarea = qs('.storage-upload textarea'); 239 | textarea.value = '{ "type": "umap", "geometry": { "type": "Point", "coordinates": [3.0528, 50.6269] }, "properties": { "storage_id": 666, "longCredit": "the illustrious mapmaker", "shortCredit": "the mapmaker", "slideshow": {}, "captionBar": true, "dashArray": "5,5", "fillOpacity": "0.5", "fillColor": "Crimson", "fill": true, "weight": "2", "opacity": "0.9", "smoothFactor": "1", "iconClass": "Drop", "color": "Red", "limitBounds": {}, "tilelayer": { "maxZoom": 18, "url_template": "http://{s}.tile.stamen.com/watercolor/{z}/{x}/{y}.jpg", "minZoom": 0, "attribution": "Map tiles by [[http://stamen.com|Stamen Design]], under [[http://creativecommons.org/licenses/by/3.0|CC BY 3.0]]. Data by [[http://openstreetmap.org|OpenStreetMap]], under [[http://creativecommons.org/licenses/by-sa/3.0|CC BY SA]].", "name": "Watercolor" }, "licence": { "url": "", "name": "No licence set" }, "description": "Map description", "name": "Imported map", "tilelayersControl": true, "onLoadPanel": "caption", "displayPopupFooter": true, "miniMap": true, "moreControl": true, "scaleControl": true, "zoomControl": true, "scrollWheelZoom": true, "datalayersControl": true, "zoom": 6 }, "layers": [{ "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [4.2939, 50.8893], [4.2441, 50.8196], [4.3869, 50.7642], [4.4813, 50.7929], [4.413, 50.9119], [4.2939, 50.8893] ] ] }, "properties": { "name": "Bruxelles", "description": "polygon" } }, { "type": "Feature", "geometry": { "type": "Point", "coordinates": [3.0528, 50.6269] }, "properties": { "_storage_options": { "color": "Orange" }, "name": "Lille", "description": "une ville" } }], "_storage": { "displayOnLoad": true, "name": "Cities", "id": 108, "remoteData": {}, "description": "A layer with some cities", "color": "Navy", "iconClass": "Drop", "smoothFactor": "1", "dashArray": "5,1", "fillOpacity": "0.5", "fillColor": "Blue", "fill": true } }, { "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [1.7715, 50.9255], [1.6589, 50.9696], [1.4941, 51.0128], [1.4199, 51.0638], [1.2881, 51.1104] ] }, "properties": { "_storage_options": { "weight": "4" }, "name": "tunnel sous la Manche" } }], "_storage": { "displayOnLoad": true, "name": "Tunnels", "id": 109, "remoteData": {} } }]}'; 240 | formatSelect.value = 'umap'; 241 | submit = qs('.storage-upload input[type="button"]'); 242 | happen.click(submit); 243 | assert.equal(Object.keys(this.map.datalayers).length, initialLayerCount + 2); 244 | assert.equal(this.map.options.name, "Imported map"); 245 | var foundFirstLayer = false; 246 | var foundSecondLayer = false; 247 | for (var idx in this.map.datalayers) { 248 | var datalayer = this.map.datalayers[idx]; 249 | if (datalayer.options.name === "Cities") { 250 | foundFirstLayer = true; 251 | assert.equal(datalayer._index.length, 2); 252 | } 253 | if (datalayer.options.name === "Tunnels") { 254 | foundSecondLayer = true; 255 | assert.equal(datalayer._index.length, 1); 256 | } 257 | } 258 | assert.equal(foundFirstLayer, true); 259 | assert.equal(foundSecondLayer, true); 260 | 261 | }); 262 | 263 | it('should only import options on the whitelist (umap format import)', function () { 264 | assert.equal(this.map.options.storage_id, 99); 265 | }); 266 | 267 | it('should update title bar (umap format import)', function () { 268 | var title = qs("#map div.storage-main-edit-toolbox h3 a.storage-click-to-edit"); 269 | assert.equal(title.innerHTML, "Imported map"); 270 | }); 271 | 272 | it('should reinitialize controls (umap format import)', function () { 273 | var minimap = qs("#map div.leaflet-control-container div.leaflet-control-minimap"); 274 | assert.ok(minimap); 275 | }); 276 | 277 | it('should update the tilelayer switcher control (umap format import)', function () { 278 | //The tilelayer in the imported data isn't in the tilelayer list (set in _pre.js), there should be no selection on the tilelayer switcher 279 | var selectedLayer = qs(".storage-tilelayer-switcher-container li.selected"); 280 | assert.equal(selectedLayer, null); 281 | }); 282 | 283 | it('should set the tilelayer (umap format import)', function () { 284 | assert.equal(this.map.selected_tilelayer._url, "http://{s}.tile.stamen.com/watercolor/{z}/{x}/{y}.jpg"); 285 | }); 286 | 287 | }); 288 | 289 | describe('#localizeUrl()', function () { 290 | 291 | it('should replace known variables', function () { 292 | assert.equal(this.map.localizeUrl('http://example.org/{zoom}'), 'http://example.org/' + this.map.getZoom()); 293 | }); 294 | 295 | it('should keep unknown variables', function () { 296 | assert.equal(this.map.localizeUrl('http://example.org/{unkown}'), 'http://example.org/{unkown}'); 297 | }); 298 | 299 | }); 300 | 301 | 302 | }); 303 | -------------------------------------------------------------------------------- /test/Polygon.js: -------------------------------------------------------------------------------- 1 | describe('L.Storage.Polygon', function () { 2 | var p2ll, map; 3 | 4 | before(function () { 5 | this.map = map = initMap({storage_id: 99}); 6 | enableEdit(); 7 | p2ll = function (x, y) { 8 | return map.containerPointToLatLng([x, y]); 9 | }; 10 | this.datalayer = this.map.createDataLayer(); 11 | this.datalayer.connectToMap();; 12 | }); 13 | 14 | after(function () { 15 | clickCancel(); 16 | resetMap(); 17 | }); 18 | 19 | afterEach(function () { 20 | this.datalayer.empty(); 21 | }); 22 | 23 | describe('#isMulti()', function () { 24 | 25 | it('should return false for basic Polygon', function () { 26 | var layer = new L.S.Polygon(this.map, [[1, 2], [3, 4], [5, 6]], {datalayer: this.datalayer}); 27 | assert.notOk(layer.isMulti()) 28 | }); 29 | 30 | it('should return false for nested basic Polygon', function () { 31 | var latlngs = [ 32 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]] 33 | ], 34 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}); 35 | assert.notOk(layer.isMulti()) 36 | }); 37 | 38 | it('should return false for simple Polygon with hole', function () { 39 | var layer = new L.S.Polygon(this.map, [[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]], {datalayer: this.datalayer}); 40 | assert.notOk(layer.isMulti()) 41 | }); 42 | 43 | it('should return true for multi Polygon', function () { 44 | var latLngs = [ 45 | [ 46 | [[1, 2], [3, 4], [5, 6]] 47 | ], 48 | [ 49 | [[7, 8], [9, 10], [11, 12]] 50 | ] 51 | ]; 52 | var layer = new L.S.Polygon(this.map, latLngs, {datalayer: this.datalayer}); 53 | assert.ok(layer.isMulti()) 54 | }); 55 | 56 | it('should return true for multi Polygon with hole', function () { 57 | var latLngs = [ 58 | [[[10, 20], [30, 40], [50, 60]]], 59 | [[[0, 10], [10, 10], [10, 0]], [[2, 3], [2, 4], [3, 4]]] 60 | ]; 61 | var layer = new L.S.Polygon(this.map, latLngs, {datalayer: this.datalayer}); 62 | assert.ok(layer.isMulti()) 63 | }); 64 | 65 | }); 66 | 67 | describe('#contextmenu', function () { 68 | 69 | afterEach(function () { 70 | // Make sure contextmenu is hidden 71 | happen.once(document, {type: 'keydown', keyCode: 27}); 72 | }); 73 | 74 | describe('#in edit mode', function () { 75 | 76 | it('should allow to remove shape when multi', function () { 77 | var latlngs = [ 78 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]], 79 | [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]] 80 | ], 81 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 82 | happen.once(layer._path, {type: 'contextmenu'}); 83 | assert.equal(qst('Remove shape from the multi'), 1); 84 | }); 85 | 86 | it('should not allow to remove shape when not multi', function () { 87 | var latlngs = [ 88 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]] 89 | ], 90 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 91 | happen.once(layer._path, {type: 'contextmenu'}); 92 | assert.notOk(qst('Remove shape from the multi')); 93 | }); 94 | 95 | it('should not allow to isolate shape when not multi', function () { 96 | var latlngs = [ 97 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]] 98 | ], 99 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 100 | happen.once(layer._path, {type: 'contextmenu'}); 101 | assert.notOk(qst('Extract shape to separate feature')); 102 | }); 103 | 104 | it('should allow to isolate shape when multi', function () { 105 | var latlngs = [ 106 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]], 107 | [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]] 108 | ], 109 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 110 | happen.once(layer._path, {type: 'contextmenu'}); 111 | assert.ok(qst('Extract shape to separate feature')); 112 | }); 113 | 114 | it('should not allow to transform to lines when multi', function () { 115 | var latlngs = [ 116 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]], 117 | [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]] 118 | ], 119 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 120 | happen.once(layer._path, {type: 'contextmenu'}); 121 | assert.notOk(qst('Transform to lines')); 122 | }); 123 | 124 | it('should not allow to transform to lines when hole', function () { 125 | var latlngs = [ 126 | [ 127 | [p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)], 128 | [p2ll(120, 150), p2ll(150, 180), p2ll(180, 120)] 129 | ] 130 | ], 131 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 132 | happen.once(layer._path, {type: 'contextmenu'}); 133 | assert.notOk(qst('Transform to lines')); 134 | }); 135 | 136 | it('should allow to transform to lines when not multi', function () { 137 | var latlngs = [ 138 | [[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]] 139 | ]; 140 | new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 141 | happen.at('contextmenu', 150, 150); 142 | assert.equal(qst('Transform to lines'), 1); 143 | }); 144 | 145 | it('should not allow to transfer shape when not editedFeature', function () { 146 | new L.S.Polygon(this.map, [p2ll(100, 150), p2ll(100, 200), p2ll(200, 150)], {datalayer: this.datalayer}).addTo(this.datalayer); 147 | happen.at('contextmenu', 110, 160); 148 | assert.equal(qst('Delete this feature'), 1); // Make sure we have right clicked on the polygon. 149 | assert.notOk(qst('Transfer shape to edited feature')); 150 | }); 151 | 152 | it('should not allow to transfer shape when editedFeature is not a polygon', function () { 153 | var layer = new L.S.Polygon(this.map, [p2ll(100, 150), p2ll(100, 200), p2ll(200, 150)], {datalayer: this.datalayer}).addTo(this.datalayer), 154 | other = new L.S.Polyline(this.map, [p2ll(200, 250), p2ll(200, 300)], {datalayer: this.datalayer}).addTo(this.datalayer); 155 | other.edit(); 156 | happen.once(layer._path, {type: 'contextmenu'}); 157 | assert.equal(qst('Delete this feature'), 1); // Make sure we have right clicked on the polygon. 158 | assert.notOk(qst('Transfer shape to edited feature')); 159 | }); 160 | 161 | it('should allow to transfer shape when another polygon is edited', function (done) { 162 | this.datalayer.empty(); 163 | var layer = new L.S.Polygon(this.map, [p2ll(200, 300), p2ll(300, 200), p2ll(200, 100)], {datalayer: this.datalayer}).addTo(this.datalayer); 164 | layer.edit(); // This moves the map to put "other" at the center. 165 | var other = new L.S.Polygon(this.map, [p2ll(100, 150), p2ll(100, 200), p2ll(200, 150)], {datalayer: this.datalayer}).addTo(this.datalayer); 166 | happen.once(other._path, {type: 'contextmenu'}); 167 | assert.equal(qst('Transfer shape to edited feature'), 1); 168 | done(); 169 | }); 170 | 171 | }); 172 | 173 | }); 174 | 175 | describe('#addShape', function () { 176 | 177 | it('"add shape" control should not be visible by default', function () { 178 | assert.notOk(qs('.storage-draw-polygon-multi')); 179 | }); 180 | 181 | it('"add shape" control should be visible when editing a Polygon', function () { 182 | var layer = new L.S.Polygon(this.map, [p2ll(100, 100), p2ll(100, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 183 | layer.edit(); 184 | assert.ok(qs('.storage-draw-polygon-multi')); 185 | }); 186 | 187 | it('"add shape" control should extend the same multi', function () { 188 | var layer = new L.S.Polygon(this.map, [p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)], {datalayer: this.datalayer}).addTo(this.datalayer); 189 | layer.edit(); 190 | assert.notOk(layer.isMulti()); 191 | happen.click(qs('.storage-draw-polygon-multi')); 192 | happen.at('mousedown', 300, 300); 193 | happen.at('mouseup', 300, 300); 194 | happen.at('mousedown', 350, 300); 195 | happen.at('mouseup', 350, 300); 196 | happen.at('click', 350, 300); 197 | assert.ok(layer.isMulti()); 198 | assert.equal(this.datalayer._index.length, 1); 199 | }); 200 | 201 | }); 202 | 203 | describe('#transferShape', function () { 204 | 205 | it('should transfer simple polygon shape to another polygon', function () { 206 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 207 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer), 208 | other = new L.S.Polygon(this.map, [p2ll(200, 350), p2ll(200, 300), p2ll(300, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 209 | assert.ok(this.map.hasLayer(layer)); 210 | layer.transferShape(p2ll(150, 150), other); 211 | assert.equal(other._latlngs.length, 2); 212 | assert.deepEqual(other._latlngs[1][0], latlngs); 213 | assert.notOk(this.map.hasLayer(layer)); 214 | }); 215 | 216 | it('should transfer multipolygon shape to another polygon', function () { 217 | var latlngs = [ 218 | [ 219 | [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 220 | [p2ll(120, 150), p2ll(150, 180), p2ll(180, 120)] 221 | ], 222 | [[p2ll(200, 300), p2ll(300, 200)]] 223 | ], 224 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer), 225 | other = new L.S.Polygon(this.map, [p2ll(200, 350), p2ll(200, 300), p2ll(300, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 226 | assert.ok(this.map.hasLayer(layer)); 227 | layer.transferShape(p2ll(150, 150), other); 228 | assert.equal(other._latlngs.length, 2); 229 | assert.deepEqual(other._latlngs[1][0], latlngs[0][0]); 230 | assert.ok(this.map.hasLayer(layer)); 231 | assert.equal(layer._latlngs.length, 1); 232 | }); 233 | 234 | }); 235 | 236 | describe('#isolateShape', function () { 237 | 238 | it('should not allow to isolate simple polygon', function () { 239 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 240 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 241 | assert.equal(this.datalayer._index.length, 1); 242 | assert.ok(this.map.hasLayer(layer)); 243 | layer.isolateShape(p2ll(150, 150)); 244 | assert.equal(layer._latlngs[0].length, 3); 245 | assert.equal(this.datalayer._index.length, 1); 246 | }); 247 | 248 | it('should isolate multipolygon shape', function () { 249 | var latlngs = [ 250 | [ 251 | [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 252 | [p2ll(120, 150), p2ll(150, 180), p2ll(180, 120)] 253 | ], 254 | [[p2ll(200, 300), p2ll(300, 200)]] 255 | ], 256 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 257 | assert.equal(this.datalayer._index.length, 1); 258 | assert.ok(this.map.hasLayer(layer)); 259 | var other = layer.isolateShape(p2ll(150, 150)); 260 | assert.equal(this.datalayer._index.length, 2); 261 | assert.equal(other._latlngs.length, 2); 262 | assert.deepEqual(other._latlngs[0], latlngs[0][0]); 263 | assert.ok(this.map.hasLayer(layer)); 264 | assert.ok(this.map.hasLayer(other)); 265 | assert.equal(layer._latlngs.length, 1); 266 | other.remove(); 267 | }); 268 | 269 | }); 270 | 271 | describe('#clone', function () { 272 | 273 | it('should clone polygon', function () { 274 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 275 | layer = new L.S.Polygon(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 276 | assert.equal(this.datalayer._index.length, 1); 277 | other = layer.clone(); 278 | assert.ok(this.map.hasLayer(other)); 279 | assert.equal(this.datalayer._index.length, 2); 280 | // Must not be the same reference 281 | assert.notEqual(layer._latlngs, other._latlngs); 282 | assert.equal(L.Util.formatNum(layer._latlngs[0][0].lat), other._latlngs[0][0].lat); 283 | assert.equal(L.Util.formatNum(layer._latlngs[0][0].lng), other._latlngs[0][0].lng); 284 | }); 285 | 286 | }); 287 | 288 | }); 289 | -------------------------------------------------------------------------------- /test/Polyline.js: -------------------------------------------------------------------------------- 1 | describe('L.Storage.Polyline', function () { 2 | var p2ll, map; 3 | 4 | before(function () { 5 | this.map = map = initMap({storage_id: 99}); 6 | enableEdit(); 7 | p2ll = function (x, y) { 8 | return map.containerPointToLatLng([x, y]); 9 | }; 10 | this.datalayer = this.map.createDataLayer(); 11 | this.datalayer.connectToMap();; 12 | }); 13 | 14 | after(function () { 15 | clickCancel(); 16 | resetMap(); 17 | }); 18 | 19 | afterEach(function () { 20 | this.datalayer.empty(); 21 | }); 22 | 23 | describe('#isMulti()', function () { 24 | 25 | it('should return false for basic Polyline', function () { 26 | var layer = new L.S.Polyline(this.map, [[1, 2], [3, 4], [5, 6]], {datalayer: this.datalayer}); 27 | assert.notOk(layer.isMulti()) 28 | }); 29 | 30 | it('should return false for nested basic Polyline', function () { 31 | var layer = new L.S.Polyline(this.map, [[[1, 2], [3, 4], [5, 6]]], {datalayer: this.datalayer}); 32 | assert.notOk(layer.isMulti()) 33 | }); 34 | 35 | it('should return true for multi Polyline', function () { 36 | var latLngs = [ 37 | [ 38 | [[1, 2], [3, 4], [5, 6]] 39 | ], 40 | [ 41 | [[7, 8], [9, 10], [11, 12]] 42 | ] 43 | ]; 44 | var layer = new L.S.Polyline(this.map, latLngs, {datalayer: this.datalayer}); 45 | assert.ok(layer.isMulti()) 46 | }); 47 | 48 | }); 49 | 50 | describe('#contextmenu', function () { 51 | 52 | afterEach(function () { 53 | // Make sure contextmenu is hidden. 54 | happen.once(document, {type: 'keydown', keyCode: 27}); 55 | }); 56 | 57 | describe('#in edit mode', function () { 58 | 59 | it('should allow to remove shape when multi', function () { 60 | var latlngs = [ 61 | [p2ll(100, 100), p2ll(100, 200)], 62 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 63 | ], 64 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 65 | happen.once(layer._path, {type: 'contextmenu'}) 66 | assert.equal(qst('Remove shape from the multi'), 1); 67 | }); 68 | 69 | it('should not allow to remove shape when not multi', function () { 70 | var latlngs = [ 71 | [p2ll(100, 100), p2ll(100, 200)] 72 | ], 73 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 74 | happen.once(layer._path, {type: 'contextmenu'}) 75 | assert.notOk(qst('Remove shape from the multi')); 76 | }); 77 | 78 | it('should not allow to isolate shape when not multi', function () { 79 | var latlngs = [ 80 | [p2ll(100, 100), p2ll(100, 200)] 81 | ], 82 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 83 | happen.once(layer._path, {type: 'contextmenu'}) 84 | assert.notOk(qst('Extract shape to separate feature')); 85 | }); 86 | 87 | it('should allow to isolate shape when multi', function () { 88 | var latlngs = [ 89 | [p2ll(100, 150), p2ll(100, 200)], 90 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 91 | ], 92 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 93 | happen.once(layer._path, {type: 'contextmenu'}); 94 | assert.ok(qst('Extract shape to separate feature')); 95 | }); 96 | 97 | it('should not allow to transform to polygon when multi', function () { 98 | var latlngs = [ 99 | [p2ll(100, 150), p2ll(100, 200)], 100 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 101 | ], 102 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 103 | happen.once(layer._path, {type: 'contextmenu'}); 104 | assert.notOk(qst('Transform to polygon')); 105 | }); 106 | 107 | it('should allow to transform to polygon when not multi', function () { 108 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 109 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 110 | happen.once(layer._path, {type: 'contextmenu'}); 111 | assert.equal(qst('Transform to polygon'), 1); 112 | }); 113 | 114 | it('should not allow to transfer shape when not editedFeature', function () { 115 | var layer = new L.S.Polyline(this.map, [p2ll(100, 150), p2ll(100, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 116 | happen.once(layer._path, {type: 'contextmenu'}); 117 | assert.notOk(qst('Transfer shape to edited feature')); 118 | }); 119 | 120 | it('should not allow to transfer shape when editedFeature is not a line', function () { 121 | var layer = new L.S.Polyline(this.map, [p2ll(100, 150), p2ll(100, 200)], {datalayer: this.datalayer}).addTo(this.datalayer), 122 | other = new L.S.Polygon(this.map, [p2ll(200, 300), p2ll(300, 200), p2ll(200, 100)], {datalayer: this.datalayer}).addTo(this.datalayer); 123 | other.edit(); 124 | happen.once(layer._path, {type: 'contextmenu'}); 125 | assert.notOk(qst('Transfer shape to edited feature')); 126 | }); 127 | 128 | it('should allow to transfer shape when another line is edited', function () { 129 | var layer = new L.S.Polyline(this.map, [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], {datalayer: this.datalayer}).addTo(this.datalayer), 130 | other = new L.S.Polyline(this.map, [p2ll(200, 300), p2ll(300, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 131 | other.edit(); 132 | happen.once(layer._path, {type: 'contextmenu'}); 133 | assert.equal(qst('Transfer shape to edited feature'), 1); 134 | }); 135 | 136 | it('should allow to merge lines when multi', function () { 137 | var latlngs = [ 138 | [p2ll(100, 100), p2ll(100, 200)], 139 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 140 | ], 141 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 142 | happen.once(layer._path, {type: 'contextmenu'}) 143 | assert.equal(qst('Merge lines'), 1); 144 | }); 145 | 146 | it('should not allow to merge lines when not multi', function () { 147 | var latlngs = [ 148 | [p2ll(100, 100), p2ll(100, 200)] 149 | ], 150 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 151 | happen.once(layer._path, {type: 'contextmenu'}) 152 | assert.notOk(qst('Merge lines')); 153 | }); 154 | 155 | it('should allow to split lines when clicking on vertex', function () { 156 | var latlngs = [ 157 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 158 | ], 159 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 160 | layer.enableEdit(); 161 | happen.at('contextmenu', 350, 400); 162 | assert.equal(qst('Split line'), 1); 163 | }); 164 | 165 | it('should not allow to split lines when clicking on first vertex', function () { 166 | var latlngs = [ 167 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 168 | ], 169 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 170 | layer.enableEdit(); 171 | happen.at('contextmenu', 300, 350); 172 | assert.equal(qst('Delete this feature'), 1); // Make sure we have clicked on the vertex. 173 | assert.notOk(qst('Split line')); 174 | }); 175 | 176 | it('should not allow to split lines when clicking on last vertex', function () { 177 | var latlngs = [ 178 | [p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)] 179 | ], 180 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 181 | layer.enableEdit(); 182 | happen.at('contextmenu', 400, 300); 183 | assert.equal(qst('Delete this feature'), 1); // Make sure we have clicked on the vertex. 184 | assert.notOk(qst('Split line')); 185 | }); 186 | 187 | }); 188 | 189 | }); 190 | 191 | describe('#addShape', function () { 192 | 193 | it('"add shape" control should not be visible by default', function () { 194 | assert.notOk(qs('.storage-draw-polyline-multi')); 195 | }); 196 | 197 | it('"add shape" control should be visible when editing a Polyline', function () { 198 | var layer = new L.S.Polyline(this.map, [p2ll(100, 100), p2ll(100, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 199 | layer.edit(); 200 | assert.ok(qs('.storage-draw-polyline-multi')); 201 | }); 202 | 203 | it('"add shape" control should extend the same multi', function () { 204 | var layer = new L.S.Polyline(this.map, [p2ll(100, 100), p2ll(100, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 205 | layer.edit(); 206 | assert.notOk(layer.isMulti()); 207 | happen.click(qs('.storage-draw-polyline-multi')); 208 | happen.at('mousemove', 300, 300); 209 | happen.at('click', 300, 300); 210 | happen.at('mousemove', 350, 300); 211 | happen.at('click', 350, 300); 212 | happen.at('click', 350, 300); 213 | assert.ok(layer.isMulti()); 214 | assert.equal(this.datalayer._index.length, 1); 215 | }); 216 | 217 | }); 218 | 219 | describe('#transferShape', function () { 220 | 221 | it('should transfer simple line shape to another line', function () { 222 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 223 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer), 224 | other = new L.S.Polyline(this.map, [p2ll(200, 300), p2ll(300, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 225 | assert.ok(this.map.hasLayer(layer)); 226 | layer.transferShape(p2ll(150, 150), other); 227 | assert.equal(other._latlngs.length, 2); 228 | assert.deepEqual(other._latlngs[1], latlngs); 229 | assert.notOk(this.map.hasLayer(layer)); 230 | }); 231 | 232 | it('should transfer multi line shape to another line', function () { 233 | var latlngs = [ 234 | [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 235 | [p2ll(200, 300), p2ll(300, 200)] 236 | ], 237 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer), 238 | other = new L.S.Polyline(this.map, [p2ll(250, 300), p2ll(350, 200)], {datalayer: this.datalayer}).addTo(this.datalayer); 239 | assert.ok(this.map.hasLayer(layer)); 240 | layer.transferShape(p2ll(150, 150), other); 241 | assert.equal(other._latlngs.length, 2); 242 | assert.deepEqual(other._latlngs[1], latlngs[0]); 243 | assert.ok(this.map.hasLayer(layer)); 244 | assert.equal(layer._latlngs.length, 1); 245 | }); 246 | 247 | }); 248 | 249 | describe('#mergeShapes', function () { 250 | 251 | it('should remove duplicated join point when merging', function () { 252 | var latlngs = [ 253 | [[0, 0], [0, 1]], 254 | [[0, 1], [0, 2]], 255 | ], 256 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 257 | layer.mergeShapes(); 258 | layer.disableEdit(); // Remove vertex from latlngs to compare them. 259 | assert.deepEqual(layer.getLatLngs(), [L.latLng([0, 0]), L.latLng([0, 1]), L.latLng([0, 2])]); 260 | assert(this.map.isDirty); 261 | }); 262 | 263 | it('should revert candidate if first point is closer', function () { 264 | var latlngs = [ 265 | [[0, 0], [0, 1]], 266 | [[0, 2], [0, 1]], 267 | ], 268 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 269 | layer.mergeShapes(); 270 | layer.disableEdit(); 271 | assert.deepEqual(layer.getLatLngs(), [L.latLng([0, 0]), L.latLng([0, 1]), L.latLng([0, 2])]); 272 | }); 273 | 274 | }); 275 | 276 | 277 | describe('#isolateShape', function () { 278 | 279 | it('should not allow to isolate simple line', function () { 280 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 281 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 282 | assert.equal(this.datalayer._index.length, 1); 283 | assert.ok(this.map.hasLayer(layer)); 284 | layer.isolateShape(p2ll(150, 150)); 285 | assert.equal(layer._latlngs.length, 3); 286 | assert.equal(this.datalayer._index.length, 1); 287 | }); 288 | 289 | it('should isolate multipolyline shape', function () { 290 | var latlngs = [ 291 | [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 292 | [[p2ll(200, 300), p2ll(300, 200)]] 293 | ], 294 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 295 | assert.equal(this.datalayer._index.length, 1); 296 | assert.ok(this.map.hasLayer(layer)); 297 | var other = layer.isolateShape(p2ll(150, 150)); 298 | assert.equal(this.datalayer._index.length, 2); 299 | assert.equal(other._latlngs.length, 3); 300 | assert.deepEqual(other._latlngs, latlngs[0]); 301 | assert.ok(this.map.hasLayer(layer)); 302 | assert.ok(this.map.hasLayer(other)); 303 | assert.equal(layer._latlngs.length, 1); 304 | other.remove(); 305 | }); 306 | 307 | }); 308 | 309 | describe('#clone', function () { 310 | 311 | it('should clone polyline', function () { 312 | var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)], 313 | layer = new L.S.Polyline(this.map, latlngs, {datalayer: this.datalayer}).addTo(this.datalayer); 314 | assert.equal(this.datalayer._index.length, 1); 315 | other = layer.clone(); 316 | assert.ok(this.map.hasLayer(other)); 317 | assert.equal(this.datalayer._index.length, 2); 318 | // Must not be the same reference 319 | assert.notEqual(layer._latlngs, other._latlngs); 320 | assert.equal(L.Util.formatNum(layer._latlngs[0].lat), other._latlngs[0].lat); 321 | assert.equal(L.Util.formatNum(layer._latlngs[0].lng), other._latlngs[0].lng); 322 | }); 323 | 324 | }); 325 | 326 | }); 327 | -------------------------------------------------------------------------------- /test/TableEditor.js: -------------------------------------------------------------------------------- 1 | describe('L.TableEditor', function () { 2 | var path = '/map/99/datalayer/edit/62/'; 3 | 4 | before(function () { 5 | this.server = sinon.fakeServer.create(); 6 | this.server.respondWith('GET', '/datalayer/62/', JSON.stringify(RESPONSES.datalayer62_GET)); 7 | this.map = initMap({storage_id: 99}); 8 | this.datalayer = this.map.getDataLayerByStorageId(62); 9 | this.server.respond(); 10 | enableEdit(); 11 | }); 12 | after(function () { 13 | clickCancel(); 14 | this.server.restore(); 15 | resetMap(); 16 | }); 17 | 18 | describe('#open()', function () { 19 | var button; 20 | 21 | it('should exist table click on edit mode', function () { 22 | button = qs('#browse_data_toggle_' + L.stamp(this.datalayer) + ' .layer-table-edit'); 23 | expect(button).to.be.ok; 24 | }); 25 | 26 | it('should open table button click', function () { 27 | happen.click(button); 28 | expect(qs('#storage-ui-container div.table')).to.be.ok; 29 | expect(qsa('#storage-ui-container div.table form').length).to.eql(3); // One per feature. 30 | expect(qsa('#storage-ui-container div.table input').length).to.eql(3); // One per feature and per property. 31 | }); 32 | 33 | }); 34 | describe('#properties()', function () { 35 | var feature; 36 | 37 | before(function () { 38 | var firstIndex = this.datalayer._index[0]; 39 | feature = this.datalayer._layers[firstIndex]; 40 | }); 41 | 42 | it('should create new property column', function () { 43 | var newPrompt = function () { 44 | return 'newprop'; 45 | }; 46 | var oldPrompt = window.prompt; 47 | window.prompt = newPrompt; 48 | var button = qs('#storage-ui-container .add-property'); 49 | expect(button).to.be.ok; 50 | happen.click(button); 51 | expect(qsa('#storage-ui-container div.table input').length).to.eql(6); // One per feature and per property. 52 | window.prompt = oldPrompt; 53 | }); 54 | 55 | it('should populate feature property on fill', function () { 56 | var input = qs('form#storage-feature-properties_' + L.stamp(feature) + ' input[name=newprop]'); 57 | changeInputValue(input, 'the value'); 58 | expect(feature.properties.newprop).to.eql('the value'); 59 | }); 60 | 61 | it('should update property name on update click', function () { 62 | var newPrompt = function () { 63 | return 'newname'; 64 | }; 65 | var oldPrompt = window.prompt; 66 | window.prompt = newPrompt; 67 | var button = qs('#storage-ui-container div.thead div.tcell:last-of-type .storage-edit'); 68 | expect(button).to.be.ok; 69 | happen.click(button); 70 | expect(qsa('#storage-ui-container div.table input').length).to.eql(6); 71 | expect(feature.properties.newprop).to.be.undefined; 72 | expect(feature.properties.newname).to.eql('the value'); 73 | window.prompt = oldPrompt; 74 | }); 75 | 76 | it('should update property on delete click', function () { 77 | var oldConfirm, 78 | newConfirm = function () { 79 | return true; 80 | }; 81 | oldConfirm = window.confirm; 82 | window.confirm = newConfirm; 83 | var button = qs('#storage-ui-container div.thead div.tcell:last-of-type .storage-delete'); 84 | expect(button).to.be.ok; 85 | happen.click(button); 86 | FEATURE = feature; 87 | expect(qsa('#storage-ui-container div.table input').length).to.eql(3); 88 | expect(feature.properties.newname).to.be.undefined; 89 | window.confirm = oldConfirm; 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /test/Util.js: -------------------------------------------------------------------------------- 1 | describe('L.Util', function () { 2 | 3 | describe('#toHTML()', function () { 4 | 5 | it('should handle title', function () { 6 | assert.equal(L.Util.toHTML('# A title'), '

A title

'); 7 | }); 8 | 9 | it('should handle title in the middle of the content', function () { 10 | assert.equal(L.Util.toHTML('A phrase\n## A title'), 'A phrase
\n

A title

'); 11 | }); 12 | 13 | it('should handle hr', function () { 14 | assert.equal(L.Util.toHTML('---'), '
'); 15 | }); 16 | 17 | it('should handle bold', function () { 18 | assert.equal(L.Util.toHTML('Some **bold**'), 'Some bold'); 19 | }); 20 | 21 | it('should handle italic', function () { 22 | assert.equal(L.Util.toHTML('Some *italic*'), 'Some italic'); 23 | }); 24 | 25 | it('should handle newlines', function () { 26 | assert.equal(L.Util.toHTML('two\nlines'), 'two
\nlines'); 27 | }); 28 | 29 | it('should not change last newline', function () { 30 | assert.equal(L.Util.toHTML('two\nlines\n'), 'two
\nlines\n'); 31 | }); 32 | 33 | it('should handle two successive newlines', function () { 34 | assert.equal(L.Util.toHTML('two\n\nlines\n'), 'two
\n
\nlines\n'); 35 | }); 36 | 37 | it('should handle links without formatting', function () { 38 | assert.equal(L.Util.toHTML('A simple http://osm.org link'), 'A simple http://osm.org link'); 39 | }); 40 | 41 | it('should handle simple link in title', function () { 42 | assert.equal(L.Util.toHTML('# http://osm.org'), '

http://osm.org

'); 43 | }); 44 | 45 | it('should handle links with url parameter', function () { 46 | assert.equal(L.Util.toHTML('A simple https://osm.org/?url=https%3A//anotherurl.com link'), 'A simple https://osm.org/?url=https%3A//anotherurl.com link'); 47 | }); 48 | 49 | it('should handle simple link inside parenthesis', function () { 50 | assert.equal(L.Util.toHTML('A simple link (http://osm.org)'), 'A simple link (http://osm.org)'); 51 | }); 52 | 53 | it('should handle simple link with formatting', function () { 54 | assert.equal(L.Util.toHTML('A simple [[http://osm.org]] link'), 'A simple http://osm.org link'); 55 | }); 56 | 57 | it('should handle simple link with formatting and content', function () { 58 | assert.equal(L.Util.toHTML('A simple [[http://osm.org|link]]'), 'A simple link'); 59 | }); 60 | 61 | it('should handle simple link followed by a carriage return', function () { 62 | assert.equal(L.Util.toHTML('A simple link http://osm.org\nAnother line'), 'A simple link http://osm.org
\nAnother line'); 63 | }); 64 | 65 | it('should handle image', function () { 66 | assert.equal(L.Util.toHTML('A simple image: {{http://osm.org/pouet.png}}'), 'A simple image: '); 67 | }); 68 | 69 | it('should handle image without text', function () { 70 | assert.equal(L.Util.toHTML('{{http://osm.org/pouet.png}}'), ''); 71 | }); 72 | 73 | it('should handle image with width', function () { 74 | assert.equal(L.Util.toHTML('A simple image: {{http://osm.org/pouet.png|100}}'), 'A simple image: '); 75 | }); 76 | 77 | it('should handle iframe', function () { 78 | assert.equal(L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html}}}'), 'A simple iframe: '); 79 | }); 80 | 81 | it('should handle iframe with height', function () { 82 | assert.equal(L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200}}}'), 'A simple iframe: '); 83 | }); 84 | 85 | it('should handle iframe with height and width', function () { 86 | assert.equal(L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200*400}}}'), 'A simple iframe: '); 87 | }); 88 | 89 | it('should handle iframe with height with px', function () { 90 | assert.equal(L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200px}}}'), 'A simple iframe: '); 91 | }); 92 | 93 | it('should handle iframe with url parameter', function () { 94 | assert.equal(L.Util.toHTML('A simple iframe: {{{https://osm.org/?url=https%3A//anotherurl.com}}}'), 'A simple iframe: '); 95 | }); 96 | 97 | }); 98 | 99 | describe('#escapeHTML', function () { 100 | 101 | it('should escape HTML tags', function () { 102 | assert.equal(L.Util.escapeHTML(''), '<a href="pouet">'); 103 | }); 104 | 105 | it('should not fail with int value', function () { 106 | assert.equal(L.Util.escapeHTML(25), '25'); 107 | }); 108 | 109 | it('should not fail with null value', function () { 110 | assert.equal(L.Util.escapeHTML(null), ''); 111 | }); 112 | 113 | }); 114 | 115 | describe('#greedyTemplate', function () { 116 | 117 | it('should replace simple props', function () { 118 | assert.equal(L.Util.greedyTemplate('A phrase with a {variable}.', {variable: 'thing'}), 'A phrase with a thing.'); 119 | }); 120 | 121 | it('should not fail when missing key', function () { 122 | assert.equal(L.Util.greedyTemplate('A phrase with a {missing}', {}), 'A phrase with a '); 123 | }); 124 | 125 | it('should process brakets in brakets', function () { 126 | assert.equal(L.Util.greedyTemplate('A phrase with a {{{variable}}}.', {variable: 'value'}), 'A phrase with a {{value}}.'); 127 | }); 128 | 129 | it('should not process http links', function () { 130 | assert.equal(L.Util.greedyTemplate('A phrase with a {{{http://iframeurl.com}}}.', {'http://iframeurl.com': 'value'}), 'A phrase with a {{{http://iframeurl.com}}}.'); 131 | }); 132 | 133 | it('should not accept dash', function () { 134 | assert.equal(L.Util.greedyTemplate('A phrase with a {var-iable}.', {'var-iable': 'value'}), 'A phrase with a {var-iable}.'); 135 | }); 136 | 137 | it('should accept colon', function () { 138 | assert.equal(L.Util.greedyTemplate('A phrase with a {variable:fr}.', {'variable:fr': 'value'}), 'A phrase with a value.'); 139 | }); 140 | 141 | it('should replace even with ignore if key is found', function () { 142 | assert.equal(L.Util.greedyTemplate('A phrase with a {variable:fr}.', {'variable:fr': 'value'}, true), 'A phrase with a value.'); 143 | }); 144 | 145 | it('should keep string when using ignore if key is not found', function () { 146 | assert.equal(L.Util.greedyTemplate('A phrase with a {variable:fr}.', {}, true), 'A phrase with a {variable:fr}.'); 147 | }); 148 | 149 | }); 150 | 151 | describe('#TextColorFromBackgroundColor', function () { 152 | 153 | it('should output white for black', function () { 154 | document.body.style.backgroundColor = 'black'; 155 | assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#ffffff'); 156 | }); 157 | 158 | it('should output white for brown', function () { 159 | document.body.style.backgroundColor = 'brown'; 160 | assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#ffffff'); 161 | }); 162 | 163 | it('should output black for white', function () { 164 | document.body.style.backgroundColor = 'white'; 165 | assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000'); 166 | }); 167 | 168 | it('should output black for tan', function () { 169 | document.body.style.backgroundColor = 'tan'; 170 | assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000'); 171 | }); 172 | 173 | it('should output black by default', function () { 174 | document.body.style.backgroundColor = 'transparent'; 175 | assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000'); 176 | }); 177 | 178 | }); 179 | 180 | 181 | describe('#flattenCoordinates()', function () { 182 | 183 | it('should not alter already flat coords', function () { 184 | var coords = [[1, 2], [3, 4]]; 185 | assert.deepEqual(L.Util.flattenCoordinates(coords), coords); 186 | }) 187 | 188 | it('should flatten nested coords', function () { 189 | var coords = [[[1, 2], [3, 4]]]; 190 | assert.deepEqual(L.Util.flattenCoordinates(coords), coords[0]); 191 | coords = [[[[1, 2], [3, 4]]]]; 192 | assert.deepEqual(L.Util.flattenCoordinates(coords), coords[0][0]); 193 | }) 194 | 195 | it('should not fail on empty coords', function () { 196 | var coords = []; 197 | assert.deepEqual(L.Util.flattenCoordinates(coords), coords); 198 | }) 199 | 200 | }); 201 | 202 | }); 203 | -------------------------------------------------------------------------------- /test/_pre.js: -------------------------------------------------------------------------------- 1 | var qs = function (selector, element) {return (element || document).querySelector(selector);}; 2 | var qsa = function (selector) {return document.querySelectorAll(selector);}; 3 | var qst = function (text, parent) { 4 | // find element by its text content 5 | var r = document.evaluate("descendant::*[contains(text(),'" + text + "')]", parent || qs('#map'), null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null), count = 0; 6 | while(r.iterateNext()) console.log(++count); 7 | return count; 8 | }; 9 | happen.at = function (what, x, y, props) { 10 | this.once(document.elementFromPoint(x, y), L.Util.extend({ 11 | type: what, 12 | clientX: x, 13 | clientY: y, 14 | screenX: x, 15 | screenY: y, 16 | which: 1, 17 | button: 0 18 | }, props || {})); 19 | }; 20 | var resetMap = function () { 21 | var mapElement = qs('#map'); 22 | mapElement.innerHTML = 'Done'; 23 | delete mapElement._leaflet_id; 24 | document.body.className = ''; 25 | }; 26 | var enableEdit = function () { 27 | happen.click(qs('div.leaflet-control-edit-enable a')); 28 | }; 29 | var disableEdit = function () { 30 | happen.click(qs('a.leaflet-control-edit-disable')); 31 | }; 32 | var clickSave = function () { 33 | happen.click(qs('a.leaflet-control-edit-save')); 34 | }; 35 | var clickCancel = function () { 36 | var _confirm = window.confirm; 37 | window.confirm = function (text) { 38 | return true; 39 | }; 40 | happen.click(qs('a.leaflet-control-edit-cancel')); 41 | happen.once(document.body, {type: 'keypress', keyCode: 13}); 42 | window.confirm = _confirm; 43 | }; 44 | var changeInputValue = function (input, value) { 45 | input.value = value; 46 | happen.once(input, {type: 'input'}); 47 | }; 48 | var changeSelectValue = function (path_or_select, value) { 49 | if (typeof path_or_select === 'string') path_or_select = qs(path_or_select); 50 | var found = false; 51 | for (var i = 0; i < path_or_select.length; i++) { 52 | if (path_or_select.options[i].value === value) { 53 | path_or_select.options[i].selected = true; 54 | found = true; 55 | } 56 | } 57 | happen.once(path_or_select, {type: 'change'}); 58 | if (!found) throw new Error('Value ' + value + 'not found in select ' + path_or_select); 59 | return path_or_select; 60 | } 61 | var cleanAlert = function () { 62 | L.DomUtil.removeClass(qs('#map'), 'storage-alert'); 63 | L.DomUtil.get('storage-alert-container').innerHTML = ''; 64 | UI_ALERT_ID = null; // Prevent setTimeout to be called 65 | }; 66 | var defaultDatalayerData = function (custom) { 67 | var _default = { 68 | icon_class: 'Default', 69 | name: 'Elephants', 70 | displayOnLoad: true, 71 | id: 62, 72 | pictogram_url: null, 73 | opacity: null, 74 | weight: null, 75 | fillColor: '', 76 | color: '', 77 | stroke: true, 78 | smoothFactor: null, 79 | dashArray: '', 80 | fillOpacity: null, 81 | fill: true 82 | }; 83 | return L.extend({}, _default, custom); 84 | }; 85 | 86 | function initMap (options) { 87 | default_options = { 88 | "geometry": { 89 | "type": "Point", 90 | "coordinates": [5.0592041015625, 52.05924589011585] 91 | }, 92 | "type": "Feature", 93 | "properties": { 94 | "storage_id": 42, 95 | "datalayers": [], 96 | "urls": { 97 | "map": "/map/{slug}_{pk}", 98 | "datalayer_view": "/datalayer/{pk}/", 99 | "map_update": "/map/{map_id}/update/settings/", 100 | "map_old_url": "/map/{username}/{slug}/", 101 | "map_clone": "/map/{map_id}/update/clone/", 102 | "map_short_url": "/m/{pk}/", 103 | "map_anonymous_edit_url": "/map/anonymous-edit/{signature}", 104 | "map_new": "/map/new/", 105 | "datalayer_update": "/map/{map_id}/datalayer/update/{pk}/", 106 | "map_delete": "/map/{map_id}/update/delete/", 107 | "map_create": "/map/create/", 108 | "logout": "/logout/", 109 | "datalayer_create": "/map/{map_id}/datalayer/create/", 110 | "login_popup_end": "/login/popupd/", 111 | "login": "/login/", 112 | "datalayer_delete": "/map/{map_id}/datalayer/delete/{pk}/", 113 | "datalayer_versions": "/map/{map_id}/datalayer/{pk}/versions/", 114 | "datalayer_version": "/datalayer/{pk}/{name}", 115 | "pictogram_list_json": "/pictogram/json/", 116 | "map_update_permissions": "/map/{map_id}/update/permissions/" 117 | }, 118 | "default_iconUrl": "../src/img/marker.png", 119 | "zoom": 6, 120 | "tilelayers": [ 121 | { 122 | "attribution": "\u00a9 OSM Contributors", 123 | "name": "OpenStreetMap", 124 | "url_template": "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", 125 | "minZoom": 0, 126 | "maxZoom": 18, 127 | "id": 1, 128 | "selected": true 129 | }, 130 | { 131 | "attribution": "HOT and friends", 132 | "name": "HOT OSM-fr server", 133 | "url_template": "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", 134 | "rank": 99, 135 | "minZoom": 0, 136 | "maxZoom": 20, 137 | "id": 2 138 | }], 139 | "tilelayer": { 140 | "attribution": "HOT and friends", 141 | "name": "HOT OSM-fr server", 142 | "url_template": "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", 143 | "rank": 99, 144 | "minZoom": 0, 145 | "maxZoom": 20, 146 | "id": 2 147 | }, 148 | "licences": { 149 | "No licence set": { 150 | "url": "", 151 | "name": "No licence set" 152 | }, 153 | "Licence ouverte/Open Licence": { 154 | "url": "http://www.data.gouv.fr/Licence-Ouverte-Open-Licence", 155 | "name": "Licence ouverte/Open Licence" 156 | }, 157 | "WTFPL": { 158 | "url": "http://www.wtfpl.net/", 159 | "name": "WTFPL" 160 | }, 161 | "ODbl": { 162 | "url": "http://opendatacommons.org/licenses/odbl/", 163 | "name": "ODbl" 164 | } 165 | }, 166 | "name": "name of the map", 167 | "description": "The description of the map", 168 | "allowEdit": true, 169 | "moreControl": true, 170 | "scaleControl": true, 171 | "miniMap": true, 172 | "datalayersControl": true, 173 | "displayCaptionOnLoad": false, 174 | "displayPopupFooter": false, 175 | "displayDataBrowserOnLoad": false 176 | } 177 | }; 178 | default_options.properties.datalayers.push(defaultDatalayerData()); 179 | options.properties = L.extend({}, default_options.properties, options); 180 | return new L.Storage.Map("map", options); 181 | } 182 | 183 | var RESPONSES = { 184 | 'datalayer62_GET': { 185 | "crs": null, 186 | "type": "FeatureCollection", 187 | "_storage": defaultDatalayerData(), 188 | "features": [{ 189 | "geometry": { 190 | "type": "Point", 191 | "coordinates": [-0.274658203125, 52.57634993749885] 192 | }, 193 | "type": "Feature", 194 | "id": 1807, 195 | "properties": {_storage_options: {color: "OliveDrab"}, name: "test"} 196 | }, 197 | { 198 | "geometry": { 199 | "type": "LineString", 200 | "coordinates": [[-0.5712890625, 54.47642158429295], [0.439453125, 54.610254981579146], [1.724853515625, 53.44880683542759], [4.163818359375, 53.98839506479995], [5.306396484375, 53.533778184257805], [6.591796875, 53.70971358510174], [7.042236328124999, 53.35055131839989]] 201 | }, 202 | "type": "Feature", 203 | "id": 20, "properties": {"_storage_options": {"fill": false}, "name": "test"} 204 | }, 205 | { 206 | "geometry": { 207 | "type": "Polygon", 208 | "coordinates": [[[11.25, 53.585983654559804], [10.1513671875, 52.9751081817353], [12.689208984375, 52.16719363541221], [14.084472656249998, 53.199451902831555], [12.63427734375, 53.61857936489517], [11.25, 53.585983654559804], [11.25, 53.585983654559804]]] 209 | }, 210 | "type": "Feature", 211 | "id": 76, 212 | "properties": {name: "name poly"} 213 | }] 214 | } 215 | }; 216 | 217 | 218 | sinon.fakeServer.getRequest = function (path, method) { 219 | var request; 220 | for (var i=0, l=this.requests.length; i' + 238 | ''+ 239 | 'Simple point'+ 240 | 'Here is a simple description.'+ 241 | ''+ 242 | '-122.0822035425683,37.42228990140251,0'+ 243 | ''+ 244 | ''+ 245 | ''+ 246 | 'Simple path'+ 247 | 'Simple description'+ 248 | ''+ 249 | '-112.2550785337791,36.07954952145647,2357 -112.2549277039738,36.08117083492122,2357 -112.2552505069063,36.08260761307279,2357'+ 250 | ''+ 251 | ''+ 252 | ''+ 253 | 'Simple polygon'+ 254 | 'A description.'+ 255 | ''+ 256 | ''+ 257 | ''+ 258 | ''+ 259 | ' -77.05788457660967,38.87253259892824,100 '+ 260 | ' -77.05465973756702,38.87291016281703,100 '+ 261 | ' -77.05315536854791,38.87053267794386,100 '+ 262 | ' -77.05788457660967,38.87253259892824,100 '+ 263 | ''+ 264 | ''+ 265 | ''+ 266 | ''+ 267 | ''+ 268 | ''; 269 | 270 | var gpx_example = '' + 276 | ' 1374Simple PointSimple description' + 277 | ' ' + 278 | ' Simple path' + 279 | ' Simple description' + 280 | ' ' + 281 | ' ' + 282 | ' ' + 283 | ' ' + 284 | ' ' + 285 | ' ' + 286 | ''; 287 | 288 | var csv_example = 'Foo,Latitude,Longitude,title,description\n' + 289 | 'bar,41.34,122.86,a point somewhere,the description of this point'; 290 | 291 | // Make Sinon log readable 292 | sinon.format = function (what) { 293 | if (typeof what === 'object') { 294 | return JSON.stringify(what, null, 4); 295 | } else if (typeof what === "undefined") { 296 | return ''; 297 | } else { 298 | return what.toString(); 299 | } 300 | }; 301 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Leaflet.Storage Tests 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 88 | 89 | 90 |
91 |
92 | 108 | 109 | 110 | --------------------------------------------------------------------------------