├── .eslintignore ├── CNAME ├── .gitignore ├── test ├── index.js └── user.js ├── .prettierrc.json ├── css ├── spinner.gif ├── tailwind_src.css ├── fontawesome │ ├── webfonts │ │ ├── fa-brands-400.ttf │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 │ └── css │ │ └── solid.min.css ├── github_browse.css ├── theme.css ├── marker.css ├── base.css └── codemirror.css ├── img ├── favicon.png ├── twitter-card-image.png ├── rectangle.svg ├── edit.svg ├── circle.svg └── polygon.svg ├── src ├── config.js ├── constants.js ├── lib │ ├── zoomextent.js │ ├── smartzoom.js │ ├── validate.js │ ├── meta.js │ ├── popup.js │ └── readfile.js ├── ui │ ├── flash.js │ ├── message.js │ ├── map │ │ ├── clickable_marker.js │ │ ├── styles.js │ │ └── controls.js │ ├── draw │ │ ├── util.js │ │ ├── extend_draw_bar.js │ │ ├── linestring.js │ │ ├── circle.js │ │ ├── rectangle.js │ │ └── styles.js │ ├── user.js │ ├── modal.js │ ├── dnd.js │ ├── mode_buttons.js │ ├── projection_switch.js │ ├── layer_switch.js │ ├── share.js │ ├── saver.js │ └── import.js ├── panel │ ├── help.js │ ├── table.js │ └── json.js ├── source │ ├── local.js │ ├── gist.js │ └── github.js ├── mobile.js ├── core │ ├── recovery.js │ ├── repo.js │ ├── router.js │ ├── loader.js │ ├── user.js │ └── data.js ├── index.js └── ui.js ├── lib ├── d3.trigger.js ├── d3-compat.js ├── bucket.css ├── queue.js ├── bucket.js ├── hashchange.js ├── d3-tooltip.js ├── blob.js ├── d3.keybinding.js └── base64.js ├── .vscode └── settings.json ├── tailwind.config.js ├── .eslintrc ├── LICENSE ├── CONTRIBUTING.md ├── unsupported.html ├── bookmarklet.html ├── FAQ.md ├── API.md ├── data ├── help.html └── share.html ├── README.md ├── Makefile ├── package.json ├── about.html ├── CHANGELOG.md └── index.html /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | geojson.io 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./user'); 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } -------------------------------------------------------------------------------- /css/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/spinner.gif -------------------------------------------------------------------------------- /img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/img/favicon.png -------------------------------------------------------------------------------- /css/tailwind_src.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | GithubAPI: false 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /img/twitter-card-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/img/twitter-card-image.png -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DEFAULT_PROJECTION: 'globe', 3 | DEFAULT_STYLE: 'Streets' 4 | }; 5 | -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /css/fontawesome/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/geojson.io/gh-pages/css/fontawesome/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /src/lib/zoomextent.js: -------------------------------------------------------------------------------- 1 | const bbox = require('@turf/bbox').default; 2 | 3 | module.exports = function (context) { 4 | const bounds = bbox(context.data.get('map')); 5 | context.map.fitBounds(bounds, { 6 | padding: 50 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/d3.trigger.js: -------------------------------------------------------------------------------- 1 | d3.selection.prototype.trigger = function (type) { 2 | this.each(function() { 3 | var evt = document.createEvent('HTMLEvents'); 4 | evt.initEvent(type, true, true); 5 | this.dispatchEvent(evt); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /img/rectangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/smartzoom.js: -------------------------------------------------------------------------------- 1 | const bbox = require('@turf/bbox').default; 2 | 3 | module.exports = function (map, feature) { 4 | if (feature.geometry.type === 'Point') { 5 | map.flyTo({ 6 | center: feature.geometry.coordinates 7 | }); 8 | } else { 9 | const bounds = bbox(feature); 10 | map.fitBounds(bounds, { padding: 60 }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/ui/flash.js: -------------------------------------------------------------------------------- 1 | const message = require('./message'); 2 | 3 | module.exports = flash; 4 | 5 | function flash(selection, txt) { 6 | 'use strict'; 7 | 8 | const msg = message(selection); 9 | 10 | if (txt) msg.select('.content').html(txt); 11 | 12 | setTimeout(() => { 13 | msg.transition().style('opacity', 0).remove(); 14 | }, 5000); 15 | 16 | return msg; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emeraldwalk.runonsave": { 3 | "commands": [ 4 | { 5 | "match": "lib|src|css/tailwind_src.css", 6 | "cmd": "make" 7 | } 8 | ] 9 | }, 10 | "liveServer.settings.ignoreFiles": [ 11 | "lib/*", 12 | "src/*", 13 | "css/tailwind_src.css" 14 | ], 15 | "liveServer.settings.wait": 500, 16 | "search.exclude": { 17 | "**/dist": true, 18 | }, 19 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html','./src/**/*.js'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | 'mb-gray-dark': '#0e2127', 7 | 'mb-purple': { 8 | 500: '#4264fb', 9 | 700: '#0f38bf' 10 | } 11 | }, 12 | fontFamily: { 13 | 'sans': ['"Open Sans"', 'sans-serif'] 14 | } 15 | } 16 | }, 17 | plugins: [] 18 | }; 19 | -------------------------------------------------------------------------------- /src/panel/help.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const marked = require('marked'); 3 | 4 | module.exports = function () { 5 | const html = 6 | fs.readFileSync('data/help.html') + 7 | '


' + 8 | marked.parse(fs.readFileSync('API.md', 'utf8')); 9 | function render(selection) { 10 | selection.html('').append('div').attr('class', 'pad2 prose').html(html); 11 | } 12 | 13 | render.off = function () {}; 14 | 15 | return render; 16 | }; 17 | -------------------------------------------------------------------------------- /test/user.js: -------------------------------------------------------------------------------- 1 | var User = require('../src/core/user'), 2 | tape = require('tape'); 3 | 4 | tape('user', function(t) { 5 | var context = { 6 | storage: { 7 | get: function() { 8 | return null; 9 | } 10 | } 11 | }; 12 | var user = User(context); 13 | t.ok(user, 'creates a user object'); 14 | user.details(function(err, res) { 15 | t.equal(err.message, 'not logged in'); 16 | t.end(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/source/local.js: -------------------------------------------------------------------------------- 1 | let fs; 2 | 3 | try { 4 | fs = require('fs'); 5 | } catch (e) { 6 | console.warn(e); 7 | } 8 | 9 | module.exports.save = save; 10 | 11 | function save(context, callback) { 12 | const path = context.data.get('path'), 13 | map = context.data.get('map'); 14 | 15 | const content = JSON.stringify(map, null, 2); 16 | 17 | fs.writeFile(path, content, () => { 18 | callback(null, { 19 | type: 'local', 20 | path: path, 21 | content: map 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /css/fontawesome/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900} -------------------------------------------------------------------------------- /css/github_browse.css: -------------------------------------------------------------------------------- 1 | .treeui-caret { 2 | color:#555; 3 | font-size:10px; 4 | padding-right:5px; 5 | } 6 | 7 | .treeui-caret:hover { 8 | cursor:pointer; 9 | } 10 | 11 | .treeui-level { 12 | padding-left:15px; 13 | border-left:1px solid #555; 14 | } 15 | 16 | .gist-map-item { 17 | padding:5px 0; 18 | border-bottom:1px solid #eee; 19 | cursor:pointer; 20 | } 21 | 22 | .gist-map-title { 23 | font-weight:bold; 24 | } 25 | 26 | .gist-map-contents { 27 | font-family:monospace; 28 | } 29 | 30 | .gist-map-next { 31 | text-align:center; 32 | padding:5px; 33 | background:#eee; 34 | cursor:pointer; 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@mapbox/eslint-config-mapbox/base", 4 | "prettier" 5 | ], 6 | "plugins": [ 7 | "prettier" 8 | ], 9 | "rules": { 10 | "prettier/prettier": "error", 11 | "strict": 0 12 | }, 13 | "env": { 14 | "es6": true, 15 | "node": true, 16 | "browser": true 17 | }, 18 | "globals": { 19 | "mapboxgl": true, 20 | "MapboxGeocoder": true, 21 | "MapboxDraw": true, 22 | "turf": true, 23 | "CodeMirror": true, 24 | "Base64": true, 25 | "d3": true 26 | }, 27 | "parserOptions": { 28 | "ecmaVersion": 9 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Mapbox 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /src/ui/message.js: -------------------------------------------------------------------------------- 1 | module.exports = message; 2 | 3 | function message(selection) { 4 | 'use strict'; 5 | 6 | selection.select('div.message').remove(); 7 | 8 | const sel = selection.append('div').attr('class', 'message pad1'); 9 | 10 | sel 11 | .append('a') 12 | .attr('class', 'icon-remove fr') 13 | .on('click', () => { 14 | sel.remove(); 15 | }); 16 | 17 | sel.append('div').attr('class', 'content'); 18 | 19 | sel.style('opacity', 0).transition().duration(200).style('opacity', 1); 20 | 21 | sel.close = function () { 22 | sel.transition().duration(200).style('opacity', 0).remove(); 23 | sel.transition().duration(200).style('top', '0px'); 24 | }; 25 | 26 | return sel; 27 | } 28 | -------------------------------------------------------------------------------- /img/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/map/clickable_marker.js: -------------------------------------------------------------------------------- 1 | // extend mapboxGL Marker so we can pass in an onClick handler 2 | const mapboxgl = require('mapbox-gl'); 3 | 4 | class ClickableMarker extends mapboxgl.Marker { 5 | // new method onClick, sets _handleClick to a function you pass in 6 | onClick(handleClick) { 7 | this._handleClick = handleClick; 8 | return this; 9 | } 10 | 11 | // the existing _onMapClick was there to trigger a popup 12 | // but we are hijacking it to run a function we define 13 | _onMapClick(e) { 14 | const targetElement = e.originalEvent.target; 15 | const element = this._element; 16 | 17 | if ( 18 | this._handleClick && 19 | (targetElement === element || element.contains(targetElement)) 20 | ) { 21 | this._handleClick(); 22 | } 23 | } 24 | } 25 | 26 | module.exports = ClickableMarker; 27 | -------------------------------------------------------------------------------- /src/mobile.js: -------------------------------------------------------------------------------- 1 | const ui = require('./ui'), 2 | map = require('./ui/map'), 3 | data = require('./core/data'), 4 | loader = require('./core/loader'), 5 | router = require('./core/router'), 6 | repo = require('./core/repo'), 7 | user = require('./core/user'), 8 | store = require('store'); 9 | 10 | const gjIO = geojsonIO(), 11 | gjUI = ui(gjIO).read; 12 | 13 | d3.select('.geojsonio').call(gjUI); 14 | 15 | gjIO.router.on(); 16 | 17 | function geojsonIO() { 18 | const context = {}; 19 | context.dispatch = d3.dispatch('change', 'route'); 20 | context.storage = store; 21 | context.map = map(context, true); 22 | context.data = data(context); 23 | context.dispatch.on('route', loader(context)); 24 | context.repo = repo(context); 25 | context.router = router(context); 26 | context.user = user(context); 27 | return context; 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # geojson.io Contributing 2 | 3 | Note: development of geojson.io is currently paused. You may be interested in the new fork at https://geojson.net. Until development restarts, please refrain from adding issues to the tracker. 4 | 5 | FAQ 6 | 7 | Q: **Why are my coordinates flipped?** 8 | 9 | A: In the [GeoJSON](http://geojson.org/) format, longitude comes first, before 10 | latitude. 11 | 12 | Q: **Isn't that wrong?** 13 | 14 | A: No, [almost every kind of spatial file that exists, including KML and Shapefiles](http://www.macwright.org/lonlat/), 15 | does the same thing. Math also tends to tell things in X, Y order, and we usually 16 | size things by width and then height. 17 | 18 | Q: **Why does geojson.io require access to private repos?** 19 | 20 | A: Because if it didn't, you wouldn't be able to open files in your private 21 | repos via GitHub. 22 | -------------------------------------------------------------------------------- /unsupported.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Unsupported Browser or missing API 7 | 8 | 9 | 16 | 17 | 18 | 19 |

Unsupported Browser or missing API

20 |

Sorry, the browser you're currently using does not seem to support WebGL. It could be that WebGL is disabled, or it could be that the browser is out of date. Please check if WebGL is supported and enabled, or supported, or upgrade to a modern browser in order to use the site.

21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /img/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /bookmarklet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | geojson.io 6 | 7 | 18 | 19 | 20 | edit on geojson.io ← drag this link to you address bar, and click it to edit a GitHub file in geojson.io 21 | 22 | 23 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | ## How do I add another tile layer to geojson.io? 2 | 3 | [Use the Console API](https://github.com/mapbox/geojson.io/blob/gh-pages/API.md#console-api) 4 | 5 | ## Can I use geojson.io with some other source / API? 6 | 7 | Yes, if you implement that. geojson.io currently works with GitHub - you would need to implement a new [source](https://github.com/mapbox/geojson.io/tree/gh-pages/src/source). 8 | This is an exercise for the reader - you'll need to figure out how to do this on your own. 9 | 10 | ## Can I use geojson.io with my own server / authentication? 11 | 12 | Yes, if you set up your own gatekeeper instance and [configure your instance with your own api keys](https://github.com/mapbox/geojson.io/blob/gh-pages/src/config.js). 13 | Like the previous question, you will need to figure this out. 14 | 15 | ## Where's the Server? 16 | 17 | There is none - geojson.io is a static application that's hosted on GitHub Pages, and could be hosted anywhere 18 | else, as static files. 19 | 20 | ## Where are the Templates? 21 | 22 | There are none - geojson.io bootstraps its entire user interface with JavaScript and d3. It does not use templates. 23 | -------------------------------------------------------------------------------- /src/ui/map/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | title: 'Streets', 4 | style: 'mapbox://styles/mapbox/streets-v11' 5 | }, 6 | { 7 | title: 'Satellite Streets', 8 | style: 'mapbox://styles/mapbox/satellite-streets-v11' 9 | }, 10 | { 11 | title: 'Outdoors', 12 | style: 'mapbox://styles/mapbox/outdoors-v11' 13 | }, 14 | { 15 | title: 'Light', 16 | style: 'mapbox://styles/mapbox/light-v10' 17 | }, 18 | { 19 | title: 'Dark', 20 | style: 'mapbox://styles/mapbox/dark-v10' 21 | }, 22 | { 23 | title: 'OSM', 24 | style: { 25 | name: 'osm', 26 | version: 8, 27 | sources: { 28 | 'osm-raster-tiles': { 29 | type: 'raster', 30 | tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'], 31 | tileSize: 256, 32 | attribution: 33 | '© OpenStreetMap' 34 | } 35 | }, 36 | layers: [ 37 | { 38 | id: 'osm-raster-layer', 39 | type: 'raster', 40 | source: 'osm-raster-tiles', 41 | minzoom: 0, 42 | maxzoom: 22 43 | } 44 | ] 45 | } 46 | } 47 | ]; 48 | -------------------------------------------------------------------------------- /css/theme.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-gutters { 2 | background:#fff; 3 | } 4 | .cm-s-eclipse span.cm-meta {color: #FF1717;} 5 | .cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } 6 | .cm-s-eclipse span.cm-atom {color: #219;} 7 | .cm-s-eclipse span.cm-number {color: #164;} 8 | .cm-s-eclipse span.cm-def {color: #00f;} 9 | .cm-s-eclipse span.cm-variable {color: black;} 10 | .cm-s-eclipse span.cm-variable-2 {color: #0000C0;} 11 | .cm-s-eclipse span.cm-variable-3 {color: #0000C0;} 12 | .cm-s-eclipse span.cm-property {color: black;} 13 | .cm-s-eclipse span.cm-operator {color: black;} 14 | .cm-s-eclipse span.cm-comment {color: #3F7F5F;} 15 | .cm-s-eclipse span.cm-string {color: #2A00FF;} 16 | .cm-s-eclipse span.cm-string-2 {color: #f50;} 17 | .cm-s-eclipse span.cm-error {color: #f00;} 18 | .cm-s-eclipse span.cm-qualifier {color: #555;} 19 | .cm-s-eclipse span.cm-builtin {color: #30a;} 20 | .cm-s-eclipse span.cm-bracket {color: #cc7;} 21 | .cm-s-eclipse span.cm-tag {color: #170;} 22 | .cm-s-eclipse span.cm-attribute {color: #00c;} 23 | .cm-s-eclipse span.cm-link {color: #219;} 24 | 25 | .cm-s-eclipse .CodeMirror-matchingbracket { 26 | outline:1px solid grey; 27 | color:black !important; 28 | } 29 | -------------------------------------------------------------------------------- /src/core/recovery.js: -------------------------------------------------------------------------------- 1 | const zoomextent = require('../lib/zoomextent'), 2 | qs = require('qs-hash'); 3 | 4 | module.exports = function (context) { 5 | d3.select(window).on('unload', onunload); 6 | context.dispatch.on('change', onchange); 7 | 8 | const query = qs.stringQs(location.hash.split('#')[1] || ''); 9 | 10 | if (location.hash !== '#new' && !query.id && !query.data) { 11 | const rec = context.storage.get('recover'); 12 | if (rec && confirm('recover your map from the last time you edited?')) { 13 | context.data.set(rec); 14 | setTimeout(() => { 15 | zoomextent(context); 16 | }, 100); 17 | } else { 18 | context.storage.remove('recover'); 19 | } 20 | } 21 | 22 | function onunload() { 23 | if (context.data.get('type') === 'local' && context.data.hasFeatures()) { 24 | try { 25 | context.storage.set('recover', context.data.all()); 26 | } catch (e) { 27 | // QuotaStorageExceeded 28 | } 29 | } else { 30 | context.storage.remove('recover'); 31 | } 32 | } 33 | 34 | function onchange() { 35 | if (context.data.get('type') !== 'local') { 36 | context.storage.remove('recover'); 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/browser'); 2 | const { BrowserTracing } = require('@sentry/tracing'); 3 | 4 | const ui = require('./ui'), 5 | map = require('./ui/map'), 6 | data = require('./core/data'), 7 | loader = require('./core/loader'), 8 | router = require('./core/router'), 9 | recovery = require('./core/recovery'), 10 | repo = require('./core/repo'), 11 | user = require('./core/user'), 12 | store = require('store'); 13 | 14 | const gjIO = geojsonIO(), 15 | gjUI = ui(gjIO).write; 16 | 17 | d3.select('.geojsonio').call(gjUI); 18 | 19 | gjIO.recovery = recovery(gjIO); 20 | gjIO.router.on(); 21 | 22 | function geojsonIO() { 23 | const context = {}; 24 | context.dispatch = d3.dispatch('change', 'route'); 25 | context.storage = store; 26 | context.map = map(context); 27 | context.data = data(context); 28 | context.dispatch.on('route', loader(context)); 29 | context.repo = repo(context); 30 | context.router = router(context); 31 | context.user = user(context); 32 | return context; 33 | } 34 | 35 | Sentry.init({ 36 | dsn: 'https://c2d096c944dd4150ab7e44b0881b4a46@o5937.ingest.sentry.io/11480', 37 | release: 'geojson.io@latest', 38 | integrations: [new BrowserTracing()], 39 | tracesSampleRate: 1.0 40 | }); 41 | -------------------------------------------------------------------------------- /src/ui/draw/util.js: -------------------------------------------------------------------------------- 1 | const turfLength = require('@turf/length').default; 2 | const numeral = require('numeral'); 3 | 4 | function getDisplayMeasurements(feature) { 5 | // should log both metric and standard display strings for the current drawn feature 6 | 7 | // metric calculation 8 | const drawnLength = turfLength(feature) * 1000; // meters 9 | 10 | let metricUnits = 'm'; 11 | let metricFormat = '0,0'; 12 | let metricMeasurement; 13 | 14 | let standardUnits = 'ft'; 15 | let standardFormat = '0,0'; 16 | let standardMeasurement; 17 | 18 | metricMeasurement = drawnLength; 19 | if (drawnLength >= 1000) { 20 | // if over 1000 meters, upgrade metric 21 | metricMeasurement = drawnLength / 1000; 22 | metricUnits = 'km'; 23 | metricFormat = '0.00'; 24 | } 25 | 26 | standardMeasurement = drawnLength * 3.28084; 27 | if (standardMeasurement >= 5280) { 28 | // if over 5280 feet, upgrade standard 29 | standardMeasurement /= 5280; 30 | standardUnits = 'mi'; 31 | standardFormat = '0.00'; 32 | } 33 | 34 | const displayMeasurements = { 35 | metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`, 36 | standard: `${numeral(standardMeasurement).format( 37 | standardFormat 38 | )} ${standardUnits}` 39 | }; 40 | return displayMeasurements; 41 | } 42 | 43 | module.exports = { 44 | getDisplayMeasurements 45 | }; 46 | -------------------------------------------------------------------------------- /src/ui/draw/extend_draw_bar.js: -------------------------------------------------------------------------------- 1 | // from https://jsfiddle.net/fxi/xf51zet4/ 2 | class extendDrawBar { 3 | constructor(opt) { 4 | const ctrl = this; 5 | ctrl.draw = opt.draw; 6 | ctrl.buttons = opt.buttons || []; 7 | ctrl.onAddOrig = opt.draw.onAdd; 8 | ctrl.onRemoveOrig = opt.draw.onRemove; 9 | } 10 | onAdd(map) { 11 | const ctrl = this; 12 | ctrl.map = map; 13 | ctrl.elContainer = ctrl.onAddOrig(map); 14 | ctrl.buttons.forEach((b) => { 15 | ctrl.addButton(b); 16 | }); 17 | return ctrl.elContainer; 18 | } 19 | onRemove(map) { 20 | const ctrl = this; 21 | ctrl.buttons.forEach((b) => { 22 | ctrl.removeButton(b); 23 | }); 24 | ctrl.onRemoveOrig(map); 25 | } 26 | addButton(opt) { 27 | const ctrl = this; 28 | const elButton = document.createElement('button'); 29 | elButton.className = 'mapbox-gl-draw_ctrl-draw-btn'; 30 | if (opt.classes instanceof Array) { 31 | opt.classes.forEach((c) => { 32 | elButton.classList.add(c); 33 | }); 34 | } 35 | elButton.addEventListener(opt.on, opt.action); 36 | elButton.title = opt.title; 37 | ctrl.elContainer.appendChild(elButton); 38 | opt.elButton = elButton; 39 | } 40 | removeButton(opt) { 41 | opt.elButton.removeEventListener(opt.on, opt.action); 42 | opt.elButton.remove(); 43 | } 44 | } 45 | 46 | module.exports = extendDrawBar; 47 | -------------------------------------------------------------------------------- /src/ui/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context) { 2 | if ( 3 | // !/a\.tiles\.mapbox\.com/.test(L.mapbox.config.HTTP_URL) && 4 | !require('../config.js')(location.hostname).GithubAPI 5 | ) { 6 | return function () {}; 7 | } 8 | return function (selection) { 9 | const name = selection.append('a').attr('target', '_blank'); 10 | 11 | selection.append('span').text(' | '); 12 | 13 | const action = selection.append('a').attr('href', '#'); 14 | 15 | function nextLogin() { 16 | action.text('login').on('click', login); 17 | name 18 | .text('anon') 19 | .attr('href', '#') 20 | .on('click', () => { 21 | d3.event.preventDefault(); 22 | }); 23 | } 24 | 25 | function nextLogout() { 26 | name.on('click', null); 27 | action.text('logout').on('click', logout); 28 | } 29 | 30 | function login() { 31 | d3.event.preventDefault(); 32 | context.user.authenticate(); 33 | } 34 | 35 | function logout() { 36 | d3.event.preventDefault(); 37 | context.user.logout(); 38 | nextLogin(); 39 | } 40 | 41 | nextLogin(); 42 | 43 | if (context.user.token()) { 44 | context.user.details((err, d) => { 45 | if (err) return; 46 | name.text(d.login); 47 | name.attr('href', d.html_url); 48 | nextLogout(); 49 | }); 50 | } 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/core/repo.js: -------------------------------------------------------------------------------- 1 | const config = require('../config.js')(location.hostname); 2 | 3 | module.exports = function (context) { 4 | const repo = {}; 5 | 6 | repo.details = function (callback) { 7 | const endpoint = config.GithubAPI 8 | ? config.GithubAPI + '/api/v3/repos/' 9 | : 'https://api.github.com/repos/'; 10 | const cached = context.storage.get('github_repo_details'), 11 | meta = context.data.get('meta'), 12 | login = meta.login, 13 | repo = meta.repo; 14 | 15 | if ( 16 | cached && 17 | cached.login === login && 18 | cached.repo === repo && 19 | cached.when > +new Date() - 1000 * 60 * 60 20 | ) { 21 | callback(null, cached.data); 22 | } else { 23 | context.storage.remove('github_repo_details'); 24 | 25 | d3.json(endpoint + [login, repo].join('/')) 26 | .header('Authorization', 'token ' + context.storage.get('github_token')) 27 | .on('load', onload) 28 | .on('error', onerror) 29 | .get(); 30 | } 31 | 32 | function onload(repo) { 33 | context.storage.set('github_repo_details', { 34 | when: +new Date(), 35 | data: repo 36 | }); 37 | context.storage.set('github_repo', repo); 38 | callback(null, repo); 39 | } 40 | 41 | function onerror(err) { 42 | context.storage.remove('github_repo_details'); 43 | callback(new Error(err)); 44 | } 45 | }; 46 | 47 | return repo; 48 | }; 49 | -------------------------------------------------------------------------------- /src/ui/modal.js: -------------------------------------------------------------------------------- 1 | module.exports = function (selection, blocking) { 2 | const previous = selection.select('div.modal'); 3 | const animate = previous.empty(); 4 | 5 | previous.transition().duration(200).style('opacity', 0).remove(); 6 | 7 | const shaded = selection 8 | .append('div') 9 | .attr('class', 'shaded') 10 | .style('opacity', 0); 11 | 12 | const modal = shaded.append('div').attr('class', 'modal fillL col6'); 13 | 14 | const keybinding = d3 15 | .keybinding('modal') 16 | .on('⌫', shaded.close) 17 | .on('⎋', shaded.close); 18 | 19 | shaded.close = function () { 20 | shaded.transition().duration(200).style('opacity', 0).remove(); 21 | modal.transition().duration(200).style('top', '0px'); 22 | keybinding.off(); 23 | }; 24 | 25 | d3.select(document).call(keybinding); 26 | 27 | shaded.on('click.remove-modal', function () { 28 | if (d3.event.target === this && !blocking) shaded.close(); 29 | }); 30 | 31 | modal 32 | .append('button') 33 | .attr('class', 'close') 34 | .on('click', () => { 35 | if (!blocking) shaded.close(); 36 | }) 37 | .append('div') 38 | .attr('class', 'icon close'); 39 | 40 | modal.append('div').attr('class', 'content'); 41 | 42 | if (animate) { 43 | shaded.transition().style('opacity', 1); 44 | modal.style('top', '0px').transition().duration(200).style('top', '40px'); 45 | } else { 46 | shaded.style('opacity', 1); 47 | } 48 | 49 | return shaded; 50 | }; 51 | -------------------------------------------------------------------------------- /src/ui/dnd.js: -------------------------------------------------------------------------------- 1 | const readDrop = require('../lib/readfile.js').readDrop, 2 | flash = require('./flash.js'), 3 | zoomextent = require('../lib/zoomextent'); 4 | 5 | module.exports = function (context) { 6 | d3.select('body') 7 | .attr('dropzone', 'copy') 8 | .on( 9 | 'drop.import', 10 | readDrop((err, gj, warning) => { 11 | if (err && err.message) { 12 | flash(context.container, err.message).classed('error', 'true'); 13 | } 14 | if (gj && gj.features) { 15 | context.data.mergeFeatures(gj.features); 16 | if (warning) { 17 | flash(context.container, warning.message); 18 | } else { 19 | flash( 20 | context.container, 21 | 'Imported ' + gj.features.length + ' features.' 22 | ).classed('success', 'true'); 23 | } 24 | zoomextent(context); 25 | } 26 | d3.select('body').classed('dragover', false); 27 | }) 28 | ) 29 | .on('dragenter.import', over) 30 | .on('dragleave.import', exit) 31 | .on('dragover.import', over); 32 | 33 | function over() { 34 | d3.event.stopPropagation(); 35 | d3.event.preventDefault(); 36 | d3.event.dataTransfer.dropEffect = 'copy'; 37 | d3.select('body').classed('dragover', true); 38 | } 39 | 40 | function exit() { 41 | d3.event.stopPropagation(); 42 | d3.event.preventDefault(); 43 | d3.event.dataTransfer.dropEffect = 'copy'; 44 | d3.select('body').classed('dragover', false); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/d3-compat.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // get a reference to the d3.selection prototype, 4 | // and keep a reference to the old d3.selection.on 5 | var d3_selectionPrototype = d3.selection.prototype, 6 | d3_on = d3_selectionPrototype.on; 7 | 8 | // our shims are organized by event: 9 | // "desired-event": ["shimmed-event", wrapperFunction] 10 | var shims = { 11 | "mouseenter": ["mouseover", relatedTarget], 12 | "mouseleave": ["mouseout", relatedTarget] 13 | }; 14 | 15 | // rewrite the d3.selection.on function to shim the events with wrapped 16 | // callbacks 17 | d3_selectionPrototype.on = function(evt, callback, useCapture) { 18 | var bits = evt.split("."), 19 | type = bits.shift(), 20 | shim = shims[type]; 21 | if (shim) { 22 | evt = bits.length ? [shim[0], bits].join(".") : shim[0]; 23 | if (typeof callback === "function") { 24 | callback = shim[1](callback); 25 | } 26 | return d3_on.call(this, evt, callback, useCapture); 27 | } else { 28 | return d3_on.apply(this, arguments); 29 | } 30 | }; 31 | 32 | function relatedTarget(callback) { 33 | return function() { 34 | var related = d3.event.relatedTarget; 35 | if (this === related || childOf(this, related)) { 36 | return undefined; 37 | } 38 | return callback.apply(this, arguments); 39 | }; 40 | } 41 | 42 | function childOf(p, c) { 43 | if (p === c) return false; 44 | while (c && c !== p) c = c.parentNode; 45 | return c === p; 46 | } 47 | 48 | })(); 49 | -------------------------------------------------------------------------------- /src/ui/mode_buttons.js: -------------------------------------------------------------------------------- 1 | const table = require('../panel/table'), 2 | json = require('../panel/json'), 3 | help = require('../panel/help'); 4 | 5 | module.exports = function (context, pane) { 6 | return function (selection) { 7 | let mode = null; 8 | 9 | const buttonData = [ 10 | { 11 | icon: 'code', 12 | title: ' JSON', 13 | alt: 'JSON Source', 14 | behavior: json 15 | }, 16 | { 17 | icon: 'table', 18 | title: ' Table', 19 | alt: 'Edit feature properties in a table', 20 | behavior: table 21 | }, 22 | { 23 | icon: 'question', 24 | title: ' Help', 25 | alt: 'Help', 26 | behavior: help 27 | } 28 | ]; 29 | 30 | const buttons = selection.selectAll('button').data(buttonData, (d) => { 31 | return d.icon; 32 | }); 33 | 34 | const enter = buttons 35 | .enter() 36 | .append('button') 37 | .attr('class', 'grow') 38 | .attr('title', (d) => { 39 | return d.alt; 40 | }) 41 | .on('click', buttonClick); 42 | enter.append('i').attr('class', (d) => { 43 | return `fa-solid fa-${d.icon}`; 44 | }); 45 | enter.append('span').text((d) => { 46 | return d.title; 47 | }); 48 | 49 | d3.select(buttons.node()).trigger('click'); 50 | 51 | function buttonClick(d) { 52 | buttons.classed('active', (_) => { 53 | return d.icon === _.icon; 54 | }); 55 | if (mode) mode.off(); 56 | mode = d.behavior(context); 57 | pane.call(mode); 58 | } 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/ui/projection_switch.js: -------------------------------------------------------------------------------- 1 | const { DEFAULT_PROJECTION } = require('../constants'); 2 | 3 | module.exports = function (context) { 4 | return function (selection) { 5 | const projections = [ 6 | { 7 | label: 'Globe', 8 | value: 'globe' 9 | }, 10 | { 11 | label: 'Mercator', 12 | value: 'mercator' 13 | } 14 | ]; 15 | 16 | const projectionButtons = selection 17 | .append('div') 18 | .attr( 19 | 'class', 20 | 'projection-switch absolute left-0 bottom-0 mb-16 text-xs transition-all duration-200 z-10' 21 | ) 22 | .selectAll('button') 23 | .data(projections); 24 | 25 | const setProjection = function () { 26 | const clicked = this instanceof d3.selection ? this.node() : this; 27 | projectionButtons.classed('active', function () { 28 | return clicked === this; 29 | }); 30 | 31 | if (context.map._loaded) { 32 | const { value } = d3.select(clicked).datum(); 33 | context.map.setProjection(value); 34 | context.storage.set('projection', value); 35 | } 36 | }; 37 | 38 | projectionButtons 39 | .enter() 40 | .append('button') 41 | .attr('class', 'pad0x') 42 | .on('click', setProjection) 43 | .text((d) => { 44 | return d.label; 45 | }); 46 | 47 | const activeProjection = 48 | context.storage.get('projection') || DEFAULT_PROJECTION; 49 | projectionButtons 50 | .filter(({ value }) => { 51 | return value === activeProjection; 52 | }) 53 | .call(setProjection); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/ui/layer_switch.js: -------------------------------------------------------------------------------- 1 | const styles = require('./map/styles'); 2 | const { DEFAULT_STYLE } = require('../constants'); 3 | 4 | module.exports = function (context) { 5 | return function (selection) { 6 | const layerButtons = selection 7 | .append('div') 8 | .attr('class', 'layer-switch absolute left-0 bottom-0 mb-9 text-xs z-10') 9 | .selectAll('button') 10 | .data(styles); 11 | 12 | const layerSwap = function () { 13 | const clicked = this instanceof d3.selection ? this.node() : this; 14 | layerButtons.classed('active', function () { 15 | return clicked === this; 16 | }); 17 | 18 | // set user-layer button to inactive 19 | d3.select('.user-layer-button').classed('active', false); 20 | 21 | // this will likely run before the initial map style is loaded 22 | // streets is default, but on subsequent runs we must change styles 23 | if (context.map._loaded) { 24 | const { title, style } = d3.select(clicked).datum(); 25 | 26 | context.map.setStyle(style); 27 | 28 | context.storage.set('style', title); 29 | 30 | context.data.set({ 31 | mapStyleLoaded: true 32 | }); 33 | } 34 | }; 35 | 36 | layerButtons 37 | .enter() 38 | .append('button') 39 | .attr('class', 'pad0x') 40 | .on('click', layerSwap) 41 | .text((d) => { 42 | return d.title; 43 | }); 44 | 45 | const activeStyle = context.storage.get('style') || DEFAULT_STYLE; 46 | 47 | layerButtons 48 | .filter(({ title }) => { 49 | return title === activeStyle; 50 | }) 51 | .call(layerSwap); 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /src/ui/share.js: -------------------------------------------------------------------------------- 1 | const gist = require('../source/gist'), 2 | modal = require('./modal'); 3 | 4 | module.exports = share; 5 | 6 | // function facebookUrl(_) { 7 | // return ( 8 | // 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(_) 9 | // ); 10 | // } 11 | 12 | // function twitterUrl(_) { 13 | // return ( 14 | // 'https://twitter.com/intent/tweet?source=webclient&text=' + 15 | // encodeURIComponent(_) 16 | // ); 17 | // } 18 | 19 | // function emailUrl(_) { 20 | // return ( 21 | // 'mailto:?subject=' + 22 | // encodeURIComponent('My Map on geojson.io') + 23 | // "&body=Here's the link: " + 24 | // encodeURIComponent(_) 25 | // ); 26 | // } 27 | 28 | function share(context) { 29 | return function () { 30 | gist.saveBlocks(context.data.get('map'), (err, res) => { 31 | const m = modal(d3.select('div.geojsonio')); 32 | m.select('.m').attr('class', 'modal-splash modal col6'); 33 | 34 | const content = m.select('.content'); 35 | 36 | content 37 | .append('div') 38 | .attr('class', 'header pad2 fillD') 39 | .append('h1') 40 | .text('Share'); 41 | 42 | if (err || !res) { 43 | content 44 | .append('div') 45 | .attr('class', 'pad2') 46 | .text('Could not share: an error occurred: ' + err); 47 | } else { 48 | const container = content.append('div').attr('class', 'pad2'); 49 | container 50 | .append('input') 51 | .style('width', '100%') 52 | .property('value', 'http://bl.ocks.org/d/' + res.id); 53 | container.append('p').text('URL to the full-screen map in that embed'); 54 | } 55 | }); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /lib/bucket.css: -------------------------------------------------------------------------------- 1 | .bucket-deposit { 2 | padding:10px; 3 | background:#f0f0f0; 4 | text-align:center; 5 | } 6 | 7 | .bucket-store { 8 | padding:10px; 9 | background:#f7f7f7; 10 | border-top:0; 11 | } 12 | 13 | .bucket-actions { 14 | text-align:center; 15 | padding:10px; 16 | } 17 | 18 | .bucket { 19 | color:#aaa; 20 | box-shadow: inset 1px 1px 1px 1px #eee; 21 | border:1px solid #888; 22 | border-radius:20px; 23 | padding:10px 20px; 24 | line-height:20px; 25 | height:20px; 26 | margin:5px; 27 | display:inline-block; 28 | background:#fff; 29 | transition:all 400ms; 30 | -webkit-transition:all 400ms; 31 | } 32 | 33 | .bucket.filled { 34 | background:#BBDAB4; 35 | box-shadow:none; 36 | color:#444; 37 | } 38 | 39 | .remove-button:after { 40 | content: '×'; 41 | background:#D8E7D5; 42 | width:20px; 43 | height:20px; 44 | border-radius:10px; 45 | font-weight:bold; 46 | vertical-align:bottom; 47 | line-height:17px; 48 | text-align:center; 49 | display:inline-block; 50 | cursor:pointer; 51 | margin-left:10px; 52 | } 53 | 54 | .bucket-join .bucket { 55 | border-radius:0; 56 | border-right:0; 57 | margin:0px; 58 | } 59 | 60 | .bucket-join .bucket:first-child { 61 | border-radius:20px 0 0 20px; 62 | } 63 | 64 | .bucket-join .bucket:last-child { 65 | border-right:1px solid #888; 66 | border-radius:0 20px 20px 0 ; 67 | } 68 | 69 | .bucket-source { 70 | border:1px solid #888; 71 | border-radius:5px; 72 | padding:5px 10px; 73 | line-height:20px; 74 | height:20px; 75 | margin:5px; 76 | display:inline-block; 77 | background:#fff; 78 | z-index:999; 79 | } 80 | 81 | .example { 82 | padding:10px; 83 | text-align:center; 84 | font-style:italic; 85 | } 86 | -------------------------------------------------------------------------------- /src/panel/table.js: -------------------------------------------------------------------------------- 1 | const metatable = require('d3-metatable')(d3), 2 | smartZoom = require('../lib/smartzoom.js'); 3 | 4 | module.exports = function (context) { 5 | function render(selection) { 6 | selection.html(''); 7 | 8 | function rerender() { 9 | const geojson = context.data.get('map'); 10 | let props; 11 | 12 | if ( 13 | !geojson || 14 | (!geojson.geometry && (!geojson.features || !geojson.features.length)) 15 | ) { 16 | selection 17 | .html('') 18 | .append('div') 19 | .attr('class', 'blank-banner center') 20 | .text('no features'); 21 | } else { 22 | props = geojson.geometry 23 | ? [geojson.properties] 24 | : geojson.features.map(getProperties); 25 | selection.select('.blank-banner').remove(); 26 | selection.data([props]).call( 27 | metatable() 28 | .on('change', (row, i) => { 29 | const geojson = context.data.get('map'); 30 | if (geojson.geometry) { 31 | geojson.properties = row; 32 | } else { 33 | geojson.features[i].properties = row; 34 | } 35 | context.data.set('map', geojson); 36 | }) 37 | .on('rowfocus', (row, i) => { 38 | const geojson = context.data.get('map'); 39 | if (!geojson.geometry) { 40 | smartZoom(context.map, geojson.features[i]); 41 | } 42 | }) 43 | ); 44 | } 45 | } 46 | 47 | context.dispatch.on('change.table', () => { 48 | rerender(); 49 | }); 50 | 51 | rerender(); 52 | 53 | function getProperties(f) { 54 | return f.properties; 55 | } 56 | } 57 | 58 | render.off = function () { 59 | context.dispatch.on('change.table', null); 60 | }; 61 | 62 | return render; 63 | }; 64 | -------------------------------------------------------------------------------- /src/core/router.js: -------------------------------------------------------------------------------- 1 | const qs = require('qs-hash'), 2 | xtend = require('xtend'); 3 | 4 | module.exports = function (context) { 5 | const router = {}; 6 | 7 | router.on = function () { 8 | d3.select(window).on('hashchange.router', route); 9 | context.dispatch.on('change.route', unroute); 10 | context.dispatch.route(getQuery()); 11 | return router; 12 | }; 13 | 14 | router.off = function () { 15 | d3.select(window).on('hashchange.router', null); 16 | return router; 17 | }; 18 | 19 | function route() { 20 | const oldHash = d3.event.oldURL.split('#')[1] || '', 21 | newHash = d3.event.newURL.split('#')[1] || '', 22 | oldQuery = qs.stringQs(oldHash), 23 | newQuery = qs.stringQs(newHash); 24 | 25 | if (isOld(oldHash)) return upgrade(oldHash); 26 | if (newQuery.id !== oldQuery.id) context.dispatch.route(newQuery); 27 | } 28 | 29 | function isOld(id) { 30 | return ( 31 | id.indexOf('gist') === 0 || 32 | id.indexOf('github') === 0 || 33 | !isNaN(parseInt(id, 10)) 34 | ); 35 | } 36 | 37 | function upgrade(id) { 38 | let split; 39 | if (isNaN(parseInt(id, 10))) { 40 | split = id.split(':'); 41 | location.hash = 42 | '#id=' + 43 | (split[1].indexOf('/') === 0 44 | ? [split[0], split[1].substring(1)].join(':') 45 | : id); 46 | } else { 47 | location.hash = '#id=gist:/' + id; 48 | } 49 | } 50 | 51 | function unroute() { 52 | const query = getQuery(); 53 | const rev = reverseRoute(); 54 | if (rev.id && query.id !== rev.id) { 55 | location.hash = '#' + qs.qsString(rev); 56 | } 57 | } 58 | 59 | function getQuery() { 60 | return qs.stringQs(window.location.hash.substring(1)); 61 | } 62 | 63 | function reverseRoute() { 64 | const query = getQuery(); 65 | 66 | return xtend(query, { 67 | id: context.data.get('route') 68 | }); 69 | } 70 | 71 | return router; 72 | }; 73 | -------------------------------------------------------------------------------- /src/ui/saver.js: -------------------------------------------------------------------------------- 1 | const flash = require('./flash'); 2 | 3 | module.exports = function (context) { 4 | if (d3.event) d3.event.preventDefault(); 5 | 6 | function success(err, res) { 7 | if (err) return flash(context.container, err.toString()); 8 | 9 | let message, url, path; 10 | const type = context.data.get('type'); 11 | 12 | if (type === 'gist' || res.type === 'gist') { 13 | // Saved as Gist 14 | message = 'Changes to this map saved to Gist: '; 15 | url = res.html_url; 16 | path = res.id; 17 | } else if (type === 'github') { 18 | // Committed to GitHub 19 | message = 'Changes committed to GitHub: '; 20 | url = res.commit.html_url; 21 | path = res.commit.sha.substring(0, 10); 22 | } else { 23 | // Saved as a file 24 | message = 'Changes saved to disk.'; 25 | } 26 | 27 | flash( 28 | context.container, 29 | message + (url ? '' + path + '' : '') 30 | ); 31 | 32 | context.container.select('.map').classed('loading', false); 33 | context.data.parse(res); 34 | } 35 | 36 | const map = context.data.get('map'); 37 | const features = 38 | (map && map.geometry) || (map.features && map.features.length); 39 | const type = context.data.get('type'); 40 | 41 | if (!features) { 42 | return flash(context.container, 'Add a feature to the map to save it'); 43 | } 44 | 45 | context.container.select('.map').classed('loading', true); 46 | 47 | if (type === 'github') { 48 | context.repo.details(onrepo); 49 | } else { 50 | context.data.save(success); 51 | } 52 | 53 | function onrepo(err, repo) { 54 | if (!err && repo.permissions.push) { 55 | const msg = prompt('Commit Message'); 56 | if (!msg) { 57 | context.container.select('.map').classed('loading', false); 58 | return; 59 | } 60 | context.commitMessage = msg; 61 | context.data.save(success); 62 | } else { 63 | context.data.save(success); 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Geojson.io API 2 | 3 | You can interact with geojson.io programmatically via url parameters: 4 | 5 | ## URL API 6 | 7 | You can do a few interesting things with just URLs and geojson.io. Here are the 8 | current URL formats. 9 | 10 | ### `map` 11 | 12 | Open the map at a specific location. The argument is numbers separated by `/` 13 | in the form `zoom/latitude/longitude`. 14 | 15 | #### Example: 16 | 17 | http://geojson.io/#map=2/20.0/0.0 18 | 19 | ### `data=data:application/json,` 20 | 21 | Open the map and load a chunk of [GeoJSON](http://geojson.org/) data from a 22 | URL segment directly onto the map. The GeoJSON data should be encoded 23 | as per `encodeURIComponent(JSON.stringify(geojson_data))`. 24 | 25 | #### Example: 26 | 27 | http://geojson.io/#data=data:application/json,%7B%22type%22%3A%22LineString%22%2C%22coordinates%22%3A%5B%5B0%2C0%5D%2C%5B10%2C10%5D%5D%7D 28 | 29 | ### `data=data:text/x-url,` 30 | 31 | Load GeoJSON data from a URL on the internet onto the map. The URL must 32 | refer directly to a resource that is: 33 | 34 | - Freely accessible (not behind a password) 35 | - Supports [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 36 | - Is valid GeoJSON 37 | 38 | The URL should be encoded as per `encodeURIComponent(url)`. 39 | 40 | #### Example: 41 | 42 | http://geojson.io/#data=data:text/x-url,http%3A%2F%2Fapi.tiles.mapbox.com%2Fv3%2Ftmcw.map-gdv4cswo%2Fmarkers.geojson 43 | 44 | ### `id=gist:` 45 | 46 | Load GeoJSON data from a [GitHub Gist](https://gist.github.com/), given an argument 47 | in the form of `login/gistid`. The Gist can be public or private, and must 48 | contain a file with a `.geojson` extension that is valid GeoJSON. 49 | 50 | #### Example: 51 | 52 | http://geojson.io/#id=gist:tmcw/e9a29ad54dbaa83dee08&map=8/39.198/-76.981 53 | 54 | ### `id=github:` 55 | 56 | Load a file from a GitHub repository. You must have access to the file, and 57 | it must be valid GeoJSON. 58 | 59 | The url is in the form: 60 | 61 | login/repository/blob/branch/filepath 62 | 63 | #### Example: 64 | 65 | http://geojson.io/#id=github:benbalter/dc-wifi-social/blob/master/bars.geojson&map=14/38.9140/-77.0302 66 | -------------------------------------------------------------------------------- /src/ui/draw/linestring.js: -------------------------------------------------------------------------------- 1 | // this mode extends the build-in linestring tool, displaying the current length 2 | // of the line as the user draws using a point feature and a symbol layer 3 | const MapboxDraw = require('@mapbox/mapbox-gl-draw'); 4 | 5 | const { getDisplayMeasurements } = require('./util.js'); 6 | 7 | const ExtendedLineStringMode = { 8 | ...MapboxDraw.modes.draw_line_string, 9 | 10 | toDisplayFeatures: function (state, geojson, display) { 11 | const isActiveLine = geojson.properties.id === state.line.id; 12 | geojson.properties.active = isActiveLine ? 'true' : 'false'; 13 | if (!isActiveLine) return display(geojson); 14 | // Only render the line if it has at least one real coordinate 15 | if (geojson.geometry.coordinates.length < 2) return; 16 | geojson.properties.meta = 'feature'; 17 | display({ 18 | type: 'Feature', 19 | properties: { 20 | meta: 'vertex', 21 | parent: state.line.id, 22 | coord_path: `${ 23 | state.direction === 'forward' 24 | ? geojson.geometry.coordinates.length - 2 25 | : 1 26 | }`, 27 | active: 'false' 28 | }, 29 | geometry: { 30 | type: 'Point', 31 | coordinates: 32 | geojson.geometry.coordinates[ 33 | state.direction === 'forward' 34 | ? geojson.geometry.coordinates.length - 2 35 | : 1 36 | ] 37 | } 38 | }); 39 | 40 | display(geojson); 41 | 42 | const displayMeasurements = getDisplayMeasurements(geojson); 43 | 44 | // create custom feature for the current pointer position 45 | const currentVertex = { 46 | type: 'Feature', 47 | properties: { 48 | meta: 'currentPosition', 49 | radius: `${displayMeasurements.metric}\n${displayMeasurements.standard}`, 50 | parent: state.line.id 51 | }, 52 | geometry: { 53 | type: 'Point', 54 | coordinates: 55 | geojson.geometry.coordinates[geojson.geometry.coordinates.length - 1] 56 | } 57 | }; 58 | 59 | display(currentVertex); 60 | } 61 | }; 62 | 63 | module.exports = ExtendedLineStringMode; 64 | -------------------------------------------------------------------------------- /src/ui/map/controls.js: -------------------------------------------------------------------------------- 1 | class EditControl { 2 | onAdd(map) { 3 | this.map = map; 4 | this._container = document.createElement('div'); 5 | this._container.className = 6 | 'mapboxgl-ctrl-group mapboxgl-ctrl edit-control hidden'; 7 | 8 | this._container.innerHTML = ` 9 | 12 | `; 13 | 14 | return this._container; 15 | } 16 | } 17 | class SaveCancelControl { 18 | onAdd(map) { 19 | this.map = map; 20 | this._container = document.createElement('div'); 21 | this._container.className = 22 | 'save-cancel-control bg-white rounded pt-1 pb-2 px-2 mt-2 mr-2 float-right clear-both pointer-events-auto'; 23 | this._container.style = 'display: none;'; 24 | this._container.innerHTML = ` 25 |
Editing Geometries
26 |
27 | 30 | 33 |
34 | `; 35 | 36 | return this._container; 37 | } 38 | } 39 | class TrashControl { 40 | onAdd(map) { 41 | this.map = map; 42 | this._container = document.createElement('div'); 43 | this._container.className = 44 | 'mapboxgl-ctrl-group mapboxgl-ctrl trash-control'; 45 | this._container.style = 'display: none;'; 46 | this._container.innerHTML = ` 47 | 49 | `; 50 | 51 | return this._container; 52 | } 53 | } 54 | 55 | module.exports = { 56 | EditControl, 57 | SaveCancelControl, 58 | TrashControl 59 | }; 60 | -------------------------------------------------------------------------------- /lib/queue.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (typeof module === "undefined") self.queue = queue; 3 | else module.exports = queue; 4 | queue.version = "1.0.4"; 5 | 6 | var slice = [].slice; 7 | 8 | function queue(parallelism) { 9 | var q, 10 | tasks = [], 11 | started = 0, // number of tasks that have been started (and perhaps finished) 12 | active = 0, // number of tasks currently being executed (started but not finished) 13 | remaining = 0, // number of tasks not yet finished 14 | popping, // inside a synchronous task callback? 15 | error = null, 16 | await = noop, 17 | all; 18 | 19 | if (!parallelism) parallelism = Infinity; 20 | 21 | function pop() { 22 | while (popping = started < tasks.length && active < parallelism) { 23 | var i = started++, 24 | t = tasks[i], 25 | a = slice.call(t, 1); 26 | a.push(callback(i)); 27 | ++active; 28 | t[0].apply(null, a); 29 | } 30 | } 31 | 32 | function callback(i) { 33 | return function(e, r) { 34 | --active; 35 | if (error != null) return; 36 | if (e != null) { 37 | error = e; // ignore new tasks and squelch active callbacks 38 | started = remaining = NaN; // stop queued tasks from starting 39 | notify(); 40 | } else { 41 | tasks[i] = r; 42 | if (--remaining) popping || pop(); 43 | else notify(); 44 | } 45 | }; 46 | } 47 | 48 | function notify() { 49 | if (error != null) await(error); 50 | else if (all) await(error, tasks); 51 | else await.apply(null, [error].concat(tasks)); 52 | } 53 | 54 | return q = { 55 | defer: function() { 56 | if (!error) { 57 | tasks.push(arguments); 58 | ++remaining; 59 | pop(); 60 | } 61 | return q; 62 | }, 63 | await: function(f) { 64 | await = f; 65 | all = false; 66 | if (!remaining) notify(); 67 | return q; 68 | }, 69 | awaitAll: function(f) { 70 | await = f; 71 | all = true; 72 | if (!remaining) notify(); 73 | return q; 74 | } 75 | }; 76 | } 77 | 78 | function noop() {} 79 | })(); 80 | -------------------------------------------------------------------------------- /data/help.html: -------------------------------------------------------------------------------- 1 |

Help

2 | 3 |

geojson.io is a quick, simple tool for creating, 4 | viewing, and sharing spatial data. geojson.io is named after GeoJSON, 5 | an open source spatial data format, and it supports GeoJSON in all ways - but also 6 | accepts KML, GPX, CSV, GTFS, TopoJSON, and other formats.

7 | 8 |

Want to request a feature or report a bug? Open 10 | an issue 11 | on geojson.io's issue tracker. 12 | 13 |

I've got data

14 | 15 |

If you have data, like a KML, GeoJSON, or CSV file, just drag & drop 16 | it onto the page or click 'Open' and select your file. Your data should appear on 17 | the map!

18 | 19 |

You may also paste geojson data into the code editor.

20 | 21 |

To ensure consistent rendering and interoperability with other spatial data formats, the working dataset in geojson.io is always a geojson FeatureCollection. If you import a standalone Feature or Geometry, it will be normalized into a FeatureCollection.

22 | 23 |

I want to draw features

24 | 25 |

Use the drawing tools to draw points, polygons, 26 | lines, rectangles, and circles. After you're done drawing the shapes, you can add 27 | information to each feature by clicking on it, editing the feature's properties, 28 | and clicking 'Save'.

29 | 30 |

Note: Circles are not supported in GeoJSON, so the circle drawing tool is really creating a circle-shaped polygon 31 | with 64 vertices.

32 | 33 |

Properties in GeoJSON are stored as 'key value pairs' - so, for instance, 34 | if you wanted to add a name to each feature, type 'name' in the first table 35 | field, and the name of the feature in the second.

36 | 37 |

I'm a coder

38 | 39 |

geojson.io accepts URL parameters 40 | that make it easy to go from a GeoJSON file on your computer to geojson.io. 41 | 42 |

Privacy & License Issues

43 | 44 | -------------------------------------------------------------------------------- /css/marker.css: -------------------------------------------------------------------------------- 1 | .marker-properties, 2 | .metadata { 3 | border-collapse:collapse; 4 | font-size:11px; 5 | width:100%; 6 | overflow:auto; 7 | border-bottom:1px solid #ccc; 8 | 9 | /* Equal to 6 rows */ 10 | max-height:189px; 11 | } 12 | 13 | .marker-properties { 14 | display:block; 15 | } 16 | 17 | .metadata { 18 | display:table; 19 | } 20 | 21 | .marker-properties th { 22 | width:33.3333%; 23 | min-width:100px; 24 | white-space:nowrap; 25 | border:1px solid #ccc; 26 | } 27 | 28 | .marker-properties td { 29 | width:60%; 30 | } 31 | 32 | .marker-properties.display td, 33 | .marker-properties.display th { 34 | padding:5px 10px; 35 | } 36 | 37 | .marker-properties tr:last-child td, 38 | .marker-properties tr:last-child th { 39 | border-bottom:none; 40 | } 41 | 42 | .marker-properties tr:nth-child(even) th, 43 | .marker-properties tr:nth-child(even) td, 44 | .metadata tr:nth-child(even) td { 45 | background-color:#f7f7f7; 46 | } 47 | 48 | 49 | .geojsonio-feature .mapboxgl-popup-content { 50 | float: left; 51 | padding: 0 !important; 52 | } 53 | 54 | .geojsonio-feature .leaflet-popup-tip-container { 55 | float: left; 56 | margin-left: 50%; 57 | right: 10px; 58 | } 59 | 60 | datalist { 61 | overflow: auto; 62 | height: 150px; 63 | } 64 | 65 | /* Tabs 66 | ------------------------------------------------------- */ 67 | .tabs-ui { 68 | position:relative; 69 | width: 250px; 70 | } 71 | 72 | .tabs-ui > * { 73 | box-sizing: border-box; 74 | } 75 | 76 | .tab .tab-toggle { 77 | background:#eee; 78 | cursor:pointer; 79 | } 80 | 81 | .tab .tab-toggle:hover { 82 | background:#f7f7f7; 83 | } 84 | 85 | .tab .hide { display:none; } 86 | 87 | .tab .content { 88 | display: none; 89 | background:white; 90 | overflow:auto; 91 | } 92 | 93 | .tab [type=radio]:checked ~ label { 94 | background:white; 95 | border-top-width: 0; 96 | z-index:2; 97 | } 98 | 99 | .tab [type=radio]:checked ~ label ~ .content { 100 | z-index:1; 101 | display: block; 102 | } 103 | 104 | .add-row-button, .add-simplestyle-properties-button { 105 | color: #2980b9; 106 | } 107 | 108 | .add-row-button:hover, .add-simplestyle-properties-button:hover { 109 | cursor: pointer; 110 | color: #199CF4; 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fgeojson.io.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fgeojson.io?ref=badge_shield) 2 | 3 | # geojson.io 4 | 5 | ![](http://i.cloudup.com/kz3BAF7Hnx.png) 6 | 7 | A fast, simple editor for map data. Read more on [Mapbox](https://www.mapbox.com/blog/geojsonio-announce/), 8 | [macwright.org](https://macwright.org/2013/07/26/geojsonio.html). 9 | 10 | ## Goes Great With! 11 | 12 | **Tools** 13 | 14 | - [Using geojson.io with GitHub is better with the Chrome Extension](https://chrome.google.com/webstore/detail/geojsonio/oibjgofbhldcajfamjganpeacipebckp) 15 | - [geojsonio-cli](https://github.com/mapbox/geojsonio-cli) lets you shoot geojson from your terminal to geojson.io! (with nodejs) 16 | - [geojsonio.py](https://github.com/jwass/geojsonio.py) lets you shoot geojson from your terminal to geojson.io! (with python) 17 | 18 | ## API 19 | 20 | You can interact with geojson.io programmatically via URL parameters. Here is an example of geojson encoded into the URL: 21 | 22 | http://geojson.io/#data=data:application/json,%7B%22type%22%3A%22LineString%22%2C%22coordinates%22%3A%5B%5B0%2C0%5D%2C%5B10%2C10%5D%5D%7D 23 | 24 | Full API documentation can be found in [API.md](API.md). 25 | 26 | ## Development 27 | 28 | Install [browserify](https://github.com/substack/node-browserify)'ied libraries: 29 | 30 | `npm install` 31 | 32 | Browserify libraries, concat other libraries, build minimal d3, build tailwind css: 33 | 34 | `make` 35 | 36 | Run a local server to preview your changes. 37 | 38 | ### Development with VSCode (hot reloading) 39 | 40 | An optimized development workflow is possible with the `Live Server` and `Run on Save` VS Code extensions. Both have workspace-specific settings in `settings.json`: 41 | 42 | - Start a live server using `Live Server's` "Go Live" button 43 | - `Run on Save` will watch `/lib`,`/src`, and `css/tailwind_src.css` and run `make` when any of them change. 44 | - `Live Server` will ignore `/lib`,`/src`, and `css/tailwind_src.css`, but will hot reload whenever any other file changes (including the files created by `make`) 45 | 46 | ## License 47 | 48 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fgeojson.io.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fgeojson.io?ref=badge_large) 49 | -------------------------------------------------------------------------------- /src/lib/validate.js: -------------------------------------------------------------------------------- 1 | const geojsonhint = require('@mapbox/geojsonhint'); 2 | 3 | module.exports = function (callback) { 4 | return function (editor, changeObj) { 5 | const err = geojsonhint.hint(editor.getValue()); 6 | editor.clearGutter('error'); 7 | 8 | // check err for objects that don't have `level` properties 9 | // if any exist, reject the geojson 10 | const rejectableErrors = err.filter( 11 | (d) => !Object.prototype.hasOwnProperty.call(d, 'level') 12 | ); 13 | 14 | if (err instanceof Error) { 15 | handleError(err.message); 16 | return callback({ 17 | class: 'icon-circle-blank', 18 | title: 'invalid JSON', 19 | message: 'invalid JSON' 20 | }); 21 | } else if (rejectableErrors.length) { 22 | handleErrors(err); 23 | return callback({ 24 | class: 'icon-circle-blank', 25 | title: 'invalid GeoJSON', 26 | message: 'invalid GeoJSON' 27 | }); 28 | } else { 29 | // err should only include warnings at this point 30 | // accept the geojson as valid but show the warnings 31 | handleErrors(err); 32 | 33 | const zoom = 34 | changeObj.from.ch === 0 && 35 | changeObj.from.line === 0 && 36 | changeObj.origin === 'paste'; 37 | 38 | const gj = JSON.parse(editor.getValue()); 39 | 40 | try { 41 | return callback(null, gj, zoom); 42 | } catch (e) { 43 | return callback({ 44 | class: 'icon-circle-blank', 45 | title: 'invalid GeoJSON', 46 | message: 'invalid GeoJSON' 47 | }); 48 | } 49 | } 50 | 51 | function handleError(msg) { 52 | const match = msg.match(/line (\d+)/); 53 | if (match && match[1]) { 54 | editor.clearGutter('error'); 55 | editor.setGutterMarker( 56 | parseInt(match[1], 10) - 1, 57 | 'error', 58 | makeMarker(msg) 59 | ); 60 | } 61 | } 62 | 63 | function handleErrors(errors) { 64 | editor.clearGutter('error'); 65 | errors.forEach((e) => { 66 | editor.setGutterMarker(e.line, 'error', makeMarker(e.message, e.level)); 67 | }); 68 | } 69 | 70 | function makeMarker(msg, level) { 71 | let className = 'error-marker'; 72 | if (level === 'message') { 73 | className += ' warning'; 74 | } 75 | 76 | return d3 77 | .select(document.createElement('div')) 78 | .attr('class', className) 79 | .attr('message', msg) 80 | .node(); 81 | } 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BROWSERIFY = node_modules/.bin/browserify 2 | SMASH = node_modules/.bin/smash 3 | CLEANCSS = node_modules/.bin/cleancss 4 | UGLIFY = node_modules/.bin/uglifyjs 5 | LIBS = $(shell find lib -type f -name '*.js') 6 | 7 | all: dist/site.js css/tailwind_dist.css 8 | 9 | node_modules: package.json 10 | npm install 11 | 12 | dist: 13 | mkdir -p dist 14 | 15 | dist/d3.js: node_modules node_modules/d3/* 16 | $(SMASH) node_modules/d3/src/start.js \ 17 | node_modules/d3/src/arrays/entries.js \ 18 | node_modules/d3/src/arrays/set.js \ 19 | node_modules/d3/src/arrays/pairs.js \ 20 | node_modules/d3/src/arrays/range.js \ 21 | node_modules/d3/src/behavior/drag.js \ 22 | node_modules/d3/src/core/rebind.js \ 23 | node_modules/d3/src/core/functor.js \ 24 | node_modules/d3/src/event/dispatch.js \ 25 | node_modules/d3/src/event/event.js \ 26 | node_modules/d3/src/selection/select.js \ 27 | node_modules/d3/src/selection/transition.js \ 28 | node_modules/d3/src/transition/each.js \ 29 | node_modules/d3/src/xhr/json.js \ 30 | node_modules/d3/src/xhr/json.js \ 31 | node_modules/d3/src/time/time.js \ 32 | node_modules/d3/src/time/format.js \ 33 | node_modules/d3/src/xhr/text.js \ 34 | node_modules/d3/src/geo/mercator.js \ 35 | node_modules/d3/src/geo/path.js \ 36 | node_modules/d3/src/end.js > dist/d3.js 37 | 38 | dist/d3.min.js: dist/d3.js 39 | $(UGLIFY) dist/d3.js > dist/d3.min.js 40 | 41 | dist/lib.js: dist dist/d3.js $(LIBS) 42 | cat dist/d3.js \ 43 | lib/hashchange.js \ 44 | lib/blob.js \ 45 | lib/base64.js \ 46 | lib/bucket.js \ 47 | lib/queue.js \ 48 | lib/d3.keybinding.js \ 49 | lib/d3.trigger.js \ 50 | lib/d3-compat.js > dist/lib.js 51 | 52 | dist/site.js: dist/lib.js src/index.js $(shell $(BROWSERIFY) --list src/index.js) 53 | $(BROWSERIFY) --noparse=src/source/local.js -t brfs src/index.js > dist/site.js 54 | 55 | css/tailwind_dist.css: 56 | npx tailwindcss -i ./css/tailwind_src.css -o ./css/tailwind_dist.css 57 | 58 | css/codemirror.css: 59 | cat node_modules/codemirror/lib/codemirror.css \ 60 | node_modules/codemirror/addon/fold/foldgutter.css > ./css/codemirror.css 61 | 62 | css/fontawesome.css: 63 | cat node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css > ./css/fontawesome/css/fontawesome.min.css 64 | cat node_modules/@fortawesome/fontawesome-free/css/solid.min.css > ./css/fontawesome/css/solid.min.css 65 | cp -R node_modules/@fortawesome/fontawesome-free/webfonts ./css/fontawesome 66 | 67 | css/mapboxgl-bundle.css: 68 | cat node_modules/mapbox-gl/dist/mapbox-gl.css \ 69 | node_modules/@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css \ 70 | node_modules/@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css > ./css/mapbox-gl-bundle.css 71 | 72 | clean: 73 | rm -f dist/* 74 | 75 | test: 76 | npm test 77 | -------------------------------------------------------------------------------- /data/share.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/core/loader.js: -------------------------------------------------------------------------------- 1 | const qs = require('qs-hash'), 2 | zoomextent = require('../lib/zoomextent'), 3 | flash = require('../ui/flash'); 4 | 5 | module.exports = function (context) { 6 | function success(err, d) { 7 | context.container.select('.map').classed('loading', false); 8 | 9 | let message; 10 | const url = /(http:\/\/\S*)/g; 11 | 12 | if (err) { 13 | try { 14 | message = 15 | err.message || 16 | JSON.parse(err.responseText).message.replace( 17 | url, 18 | '$&' 19 | ); 20 | } catch (e) { 21 | message = 'Sorry, an error occurred.'; 22 | } 23 | return flash(context.container, message); 24 | } 25 | 26 | context.data.parse(d); 27 | 28 | if (!qs.stringQs(location.hash.substring(1)).map || mapDefault()) { 29 | zoomextent(context); 30 | } 31 | } 32 | 33 | function mapDefault() { 34 | return ( 35 | context.map.getZoom() === 2 || 36 | JSON.stringify(context.map.getCenter()) === 37 | JSON.stringify({ lng: 20, lat: 2 }) 38 | ); 39 | } 40 | 41 | function inlineJSON(data) { 42 | try { 43 | context.data.set({ 44 | map: JSON.parse(data) 45 | }); 46 | history.replaceState('', document.title, window.location.pathname); 47 | 48 | zoomextent(context); 49 | } catch (e) { 50 | return flash(context.container, 'Could not parse JSON'); 51 | } 52 | } 53 | 54 | function loadUrl(data) { 55 | d3.json(data) 56 | .header('Accept', 'application/vnd.geo+json') 57 | .on('load', onload) 58 | .on('error', onerror) 59 | .get(); 60 | 61 | function onload(d) { 62 | context.data.set({ map: d }); 63 | history.replaceState('', document.title, window.location.pathname); 64 | zoomextent(context); 65 | } 66 | 67 | function onerror() { 68 | return flash( 69 | context.container, 70 | 'Could not load external file. External files must be served with CORS and be valid GeoJSON.' 71 | ); 72 | } 73 | } 74 | 75 | return function (query) { 76 | if (!query.id && !query.data) return; 77 | 78 | const oldRoute = d3.event 79 | ? qs.stringQs(d3.event.oldURL.split('#')[1]).id 80 | : context.data.get('route'); 81 | 82 | if (query.data) { 83 | // eslint-disable-next-line 84 | var type = query.data.match(/^(data\:[\w\-]+\/[\w\-]+\,?)/); 85 | if (type) { 86 | if (type[0] === 'data:application/json,') { 87 | inlineJSON(query.data.replace(type[0], '')); 88 | } else if (type[0] === 'data:text/x-url,') { 89 | loadUrl(query.data.replace(type[0], '')); 90 | } 91 | } 92 | } else if (query.id !== oldRoute) { 93 | context.container.select('.map').classed('loading', true); 94 | context.data.fetch(query, success); 95 | } 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /src/ui/import.js: -------------------------------------------------------------------------------- 1 | const importSupport = !!window.FileReader, 2 | flash = require('./flash.js'), 3 | readFile = require('../lib/readfile.js'), 4 | zoomextent = require('../lib/zoomextent'); 5 | 6 | module.exports = function (context) { 7 | return function (selection) { 8 | selection.html(''); 9 | 10 | const wrap = selection.append('div').attr('class', 'pad1'); 11 | 12 | wrap 13 | .append('div') 14 | .attr('class', 'modal-message') 15 | .text('Drop files to map!'); 16 | 17 | if (importSupport) { 18 | const import_landing = wrap.append('div').attr('class', 'pad2 fillL'); 19 | 20 | const message = import_landing.append('div').attr('class', 'center'); 21 | 22 | const fileInput = message 23 | .append('input') 24 | .attr('type', 'file') 25 | .style('visibility', 'hidden') 26 | .style('position', 'absolute') 27 | .style('height', '0') 28 | .on('change', function () { 29 | const files = this.files; 30 | if (!(files && files[0])) return; 31 | readFile.readAsText(files[0], (err, text) => { 32 | readFile.readFile(files[0], text, onImport); 33 | // node-webkit: path included 34 | if (files[0].path) { 35 | context.data.set({ 36 | path: files[0].path 37 | }); 38 | } 39 | }); 40 | }); 41 | 42 | const button = message.append('button').on('click', () => { 43 | fileInput.node().click(); 44 | }); 45 | button.append('span').attr('class', 'icon-arrow-down'); 46 | button.append('span').text(' Open'); 47 | message 48 | .append('p') 49 | .attr('class', 'deemphasize') 50 | .append('small') 51 | .text( 52 | 'GeoJSON, TopoJSON, KML, CSV, GPX and OSM XML supported. You can also drag & drop files.' 53 | ); 54 | } else { 55 | wrap 56 | .append('p') 57 | .attr('class', 'blank-banner center') 58 | .text( 59 | 'Sorry, geojson.io supports importing GeoJSON, TopoJSON, KML, CSV, GPX, and OSM XML files, but ' + 60 | "your browser isn't compatible. Please use Google Chrome, Safari 6, IE10, Firefox, or Opera for an optimal experience." 61 | ); 62 | } 63 | 64 | function onImport(err, gj, warning) { 65 | if (err) { 66 | if (err.message) { 67 | flash(context.container, err.message).classed('error', 'true'); 68 | } 69 | } else if (gj && gj.features) { 70 | context.data.mergeFeatures(gj.features); 71 | if (warning) { 72 | flash(context.container, warning.message); 73 | } else { 74 | flash( 75 | context.container, 76 | 'Imported ' + gj.features.length + ' features.' 77 | ).classed('success', 'true'); 78 | } 79 | zoomextent(context); 80 | } 81 | } 82 | 83 | wrap.append('div').attr('class', 'pad1'); 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geojson.io", 3 | "version": "0.1.1", 4 | "description": "create and edit maps, on the internet", 5 | "main": "index.html", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-free": "^6.2.0", 8 | "@mapbox/geojson-extent": "^1.0.1", 9 | "@mapbox/geojson-normalize": "0.0.1", 10 | "@mapbox/geojson-rewind": "0.5.2", 11 | "@mapbox/geojsonhint": "^3.1.0", 12 | "@mapbox/gist-map-browser": "0.2.1", 13 | "@mapbox/github-file-browser": "0.6.1", 14 | "@mapbox/mapbox-gl-draw": "^1.3.0", 15 | "@mapbox/mapbox-gl-geocoder": "^5.0.1", 16 | "@mapbox/polyline": "^1.1.1", 17 | "@placemarkio/tokml": "^0.3.3", 18 | "@popperjs/core": "^2.11.6", 19 | "@sentry/browser": "^7.20.0", 20 | "@sentry/tracing": "^7.20.0", 21 | "@tmcw/togeojson": "5.2.2", 22 | "@turf/area": "^6.5.0", 23 | "@turf/bbox": "^6.5.0", 24 | "@turf/circle": "^6.5.0", 25 | "@turf/length": "^6.5.0", 26 | "bowser": "^2.11.0", 27 | "brfs": "^2.0.2", 28 | "clone": "0.1.16", 29 | "codemirror": "5.65.9", 30 | "csv2geojson": "5.1.2", 31 | "d3": "3.5.17", 32 | "d3-metatable": "0.3.0", 33 | "escape-html": "^1.0.1", 34 | "file-saver": "2.0.5", 35 | "geojson-flatten": "^1.0.4", 36 | "geojson-random": "0.1.0", 37 | "geojson2dsv": "0.0.0", 38 | "gtfs2geojson": "^2.0.0", 39 | "lodash": "^4.17.21", 40 | "marked": "4.1.1", 41 | "numeral": "^2.0.6", 42 | "osmtogeojson": "3.0.0-beta.5", 43 | "polytogeojson": "0.0.1", 44 | "qs-hash": "0.0.0", 45 | "shp-write": "0.3.2", 46 | "smash": "0.0.15", 47 | "store": "1.3.14", 48 | "topojson-client": "3.1.0", 49 | "topojson-server": "3.0.1", 50 | "uglify-js": "3.17.3", 51 | "wellknown": "0.5.0", 52 | "wkx": "^0.4.1", 53 | "xtend": "3.0.0" 54 | }, 55 | "devDependencies": { 56 | "@mapbox/eslint-config-mapbox": "^3.0.0", 57 | "@vercel/git-hooks": "^1.0.0", 58 | "babelify": "^10.0.0", 59 | "browserify": "^17.0.0", 60 | "envify": "^4.1.0", 61 | "eslint": "^8.25.0", 62 | "eslint-config-prettier": "^8.5.0", 63 | "eslint-plugin-prettier": "^4.2.1", 64 | "lint-staged": "^13.0.3", 65 | "prettier": "^2.7.1", 66 | "tailwindcss": "^3.2.3", 67 | "tape": "^5.6.1" 68 | }, 69 | "scripts": { 70 | "lint": "eslint src/**", 71 | "format": "prettier --write src/**", 72 | "git-pre-commit": "lint-staged", 73 | "test": "eslint src", 74 | "test-browser": "browserify test/index.js | testling" 75 | }, 76 | "lint-staged": { 77 | "src/**/*.{js,jsx,ts,tsx}": "eslint --fix", 78 | "src/**/*.{js,jsx,ts,tsx,md,html,css}": "prettier --write" 79 | }, 80 | "repository": { 81 | "type": "git", 82 | "url": "git://github.com/mapbox/geojson.io.git" 83 | }, 84 | "author": "MapBox", 85 | "license": "ISC", 86 | "bugs": { 87 | "url": "https://github.com/mapbox/geojson.io/issues" 88 | }, 89 | "window": { 90 | "toolbar": false 91 | }, 92 | "engines": { 93 | "node": "14" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/core/user.js: -------------------------------------------------------------------------------- 1 | const config = require('../config.js')(location.hostname); 2 | 3 | module.exports = function (context) { 4 | const user = {}; 5 | 6 | user.details = function (callback) { 7 | if (!context.storage.get('github_token')) 8 | return callback(new Error('not logged in')); 9 | 10 | const cached = context.storage.get('github_user_details'); 11 | 12 | if (cached && cached.when > +new Date() - 1000 * 60 * 60) { 13 | callback(null, cached.data); 14 | } else { 15 | context.storage.remove('github_user_details'); 16 | const endpoint = config.GithubAPI 17 | ? config.GithubAPI + '/api/v3' 18 | : 'https://api.github.com'; 19 | 20 | d3.json(endpoint + '/user') 21 | .header('Authorization', 'token ' + context.storage.get('github_token')) 22 | .on('load', onload) 23 | .on('error', onerror) 24 | .get(); 25 | } 26 | 27 | function onload(user) { 28 | context.storage.set('github_user_details', { 29 | when: +new Date(), 30 | data: user 31 | }); 32 | context.storage.set('github_user', user); 33 | callback(null, user); 34 | } 35 | 36 | function onerror() { 37 | user.logout(); 38 | context.storage.remove('github_user_details'); 39 | callback(new Error('not logged in')); 40 | } 41 | }; 42 | 43 | user.signXHR = function (xhr) { 44 | return user.token() 45 | ? xhr.header('Authorization', 'token ' + user.token()) 46 | : xhr; 47 | }; 48 | 49 | user.authenticate = function () { 50 | window.location.href = 51 | (config.GithubAPI || 'https://github.com') + 52 | '/login/oauth/authorize?client_id=' + 53 | config.client_id + 54 | '&scope=gist,repo'; 55 | }; 56 | 57 | user.token = function () { 58 | return context.storage.get('github_token'); 59 | }; 60 | 61 | user.logout = function () { 62 | context.storage.remove('github_token'); 63 | }; 64 | 65 | user.login = function () { 66 | context.storage.remove('github_token'); 67 | }; 68 | 69 | function killTokenUrl() { 70 | if (window.location.href.indexOf('?code') !== -1) { 71 | window.location.href = window.location.href.replace(/\?code=.*$/, ''); 72 | } 73 | } 74 | 75 | if (window.location.search && window.location.search.indexOf('?code') === 0) { 76 | const code = window.location.search.replace( 77 | // eslint-disable-next-line 78 | /\?{0,1}code=([^\#\&]+).*$/g, 79 | '$1' 80 | ); 81 | d3.select('.map').classed('loading', true); 82 | d3.json(config.gatekeeper_url + '/authenticate/' + code) 83 | .on('load', (l) => { 84 | d3.select('.map').classed('loading', false); 85 | if (l.token) window.localStorage.github_token = l.token; 86 | killTokenUrl(); 87 | }) 88 | .on('error', () => { 89 | d3.select('.map').classed('loading', false); 90 | alert('Authentication with GitHub failed'); 91 | }) 92 | .get(); 93 | } 94 | 95 | return user; 96 | }; 97 | -------------------------------------------------------------------------------- /lib/bucket.js: -------------------------------------------------------------------------------- 1 | function bucket() { 2 | 3 | var event = d3.dispatch('chosen'), 4 | deposits; 5 | 6 | return { 7 | deposit: deposit, 8 | store: store 9 | }; 10 | 11 | function deposit() { 12 | return function(selection) { 13 | deposits = selection; 14 | selection.each(function() { 15 | var sel = d3.select(this); 16 | sel.attr('data-text', sel.text()); 17 | }); 18 | }; 19 | } 20 | 21 | function store() { 22 | var clone, dropped, dims; 23 | function change() { 24 | event.chosen( 25 | deposits.filter(function() { 26 | return d3.select(this).classed('filled'); 27 | }) 28 | .map(function(elems) { 29 | return elems.map(function(e) { 30 | return d3.select(e).text(); 31 | }); 32 | })[0]); 33 | } 34 | var drag = d3.behavior.drag() 35 | .origin(function() { 36 | // return { x: d3.event.pageX, y: d3.event.pageY }; 37 | return { x: this.offsetLeft, y: this.offsetTop }; 38 | }) 39 | .on('dragstart', function() { 40 | clone = d3.select(this.parentNode.insertBefore(this.cloneNode(true), this)); 41 | d3.select(this) 42 | .style('position', 'absolute') 43 | .style('pointer-events', 'none'); 44 | dims = [this.offsetWidth, this.offsetHeight]; 45 | }) 46 | .on('drag', function() { 47 | d3.select(this) 48 | .style('left', d3.event.x - (dims[0] / 2) + 'px') 49 | .style('top', d3.event.y - (dims[1] / 2) + 'px'); 50 | }) 51 | .on('dragend', function() { 52 | var self = d3.select(this); 53 | var target = d3.select(d3.event.sourceEvent.target); 54 | if (target.classed('bucket')) { 55 | target 56 | .text(self.text()) 57 | .classed('filled', true); 58 | target 59 | .append('span') 60 | .classed('remove-button', true) 61 | .on('click', function() { 62 | target 63 | .text(target.attr('data-text')) 64 | .classed('filled', false); 65 | change(); 66 | }); 67 | self.remove(); 68 | clone.call(drag); 69 | change(); 70 | } else { 71 | self.remove(); 72 | clone.call(drag); 73 | } 74 | }); 75 | 76 | return d3.rebind(function(selection) { 77 | selection.each(function() { 78 | var sel = d3.select(this).call(drag); 79 | sel.attr('data-text', sel.text()); 80 | }); 81 | }, event, 'on'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | const buttons = require('./ui/mode_buttons'), 2 | file_bar = require('./ui/file_bar'), 3 | dnd = require('./ui/dnd'), 4 | // userUi = require('./ui/user'), 5 | layer_switch = require('./ui/layer_switch'), 6 | projection_switch = require('./ui/projection_switch'); 7 | 8 | module.exports = ui; 9 | 10 | function ui(context) { 11 | function init(selection) { 12 | const container = selection 13 | .append('div') 14 | .attr( 15 | 'class', 16 | 'ui-container grow flex-shrink-0 flex flex-col md:flex-row w-full relative overflow-x-hidden' 17 | ); 18 | 19 | const map = container 20 | .append('div') 21 | .attr('id', 'map') 22 | .attr( 23 | 'class', 24 | 'map grow shrink-0 top-0 bottom-0 left-0 basis-0 transition-all duration-300' 25 | ) 26 | .call(layer_switch(context)) 27 | .call(projection_switch(context)); 28 | 29 | // sidebar handle 30 | map 31 | .append('div') 32 | .attr( 33 | 'class', 34 | 'sidebar-handle absolute right-0 bottom-9 px-4 bg-white cursor-pointer hidden md:block z-10' 35 | ) 36 | .attr('title', 'Toggle Sidebar') 37 | .on('click', () => { 38 | const collapsed = !d3.select('.map').classed('md:basis-full'); 39 | d3.select('.map').classed('md:basis-0', !collapsed); 40 | d3.select('.map').classed('md:basis-full', collapsed); 41 | 42 | d3.select('.sidebar-handle-icon') 43 | .classed('fa-caret-left', collapsed) 44 | .classed('fa-caret-right', !collapsed); 45 | 46 | setTimeout(() => { 47 | context.map.resize(); 48 | }, 300); 49 | }) 50 | .append('i') 51 | .attr('class', 'sidebar-handle-icon fa-solid fa-caret-right'); 52 | 53 | context.container = container; 54 | 55 | return container; 56 | } 57 | 58 | function render(selection) { 59 | const container = init(selection); 60 | 61 | const right = container 62 | .append('div') 63 | .attr( 64 | 'class', 65 | 'right flex flex-col overflow-x-hidden bottom-0 top-0 right-0 box-border bg-white relative grow-0 shrink-0 w-full md:w-2/5 md:max-w-md h-2/5 md:h-auto' 66 | ); 67 | 68 | const top = right 69 | .append('div') 70 | .attr('class', 'top border-b border-solid border-gray-200'); 71 | 72 | const pane = right.append('div').attr('class', 'pane group'); 73 | 74 | // user ui, disabled for now 75 | // top 76 | // .append('div') 77 | // .attr('class', 'user fr pad1 deemphasize') 78 | // .call(userUi(context)); 79 | 80 | top 81 | .append('div') 82 | .attr('class', 'buttons flex') 83 | .call(buttons(context, pane)); 84 | 85 | container 86 | .append('div') 87 | .attr('class', 'file-bar hidden md:block') 88 | .call(file_bar(context)); 89 | 90 | dnd(context); 91 | 92 | // initialize the map after the ui has been created to avoid flex container size issues 93 | context.map(); 94 | } 95 | 96 | return { 97 | read: init, 98 | write: render 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /lib/hashchange.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hashchange Event Polyfill 3 | */ 4 | 5 | (function() { 6 | 7 | window.HashChangeEvent = (function() { 8 | var ret = function(oldURL, newURL) { 9 | this.oldURL = oldURL; 10 | this.newURL = newURL; 11 | this.timeStamp = (new Date()).getTime(); 12 | }; 13 | ret.prototype = { 14 | bubbles: false, 15 | cancelable: false, 16 | currentTarget: null, 17 | defaultPrevented: false, 18 | returnValue: true, 19 | srcElement: null, 20 | target: null, 21 | timeStamp: null, 22 | type: 'hashchange' 23 | }; 24 | return ret; 25 | }()); 26 | 27 | var fix = { 28 | 29 | // Bound event listeners 30 | listeners: { 31 | funcs: [ ], 32 | remove: function(func) { 33 | var arr = [ ]; 34 | for (var i = 0, c = fix.listeners.funcs.length; i < c; i++) { 35 | if (fix.listeners.funcs[i] !== func) { 36 | arr.push(fix.listeners.funcs[i]); 37 | } 38 | } 39 | fix.listeners.funcs = arr; 40 | } 41 | }, 42 | 43 | // Start the poller 44 | init: function() { 45 | // Get the starting hash 46 | fix.lastHash = fix.getHash(); 47 | fix.lastLocation = String(location); 48 | // Patch addEventListener 49 | if (window.addEventListener) { 50 | var nativeAEL = window.addEventListener; 51 | window.addEventListener = function(evt, func) { 52 | if (evt === 'hashchange') { 53 | fix.listeners.funcs.push(func); 54 | } else { 55 | return nativeAEL.apply(window, arguments); 56 | } 57 | }; 58 | } 59 | // Patch attachEvent 60 | if (window.attachEvent) { 61 | var nativeAE = window.attachEvent; 62 | window.attachEvent = function(evt, func) { 63 | if (evt === 'onhashchange') { 64 | fix.listeners.funcs.push(func); 65 | } else { 66 | return nativeAE.apply(window, arguments); 67 | } 68 | }; 69 | } 70 | // Start polling 71 | fix.setTimeout(); 72 | }, 73 | 74 | // The previous hash value 75 | lastHash: null, 76 | lastLocation: null, 77 | 78 | // The number of miliseconds between pollings 79 | pollerRate: 50, 80 | 81 | // Read the hash value from the URL 82 | getHash: function() { 83 | return location.hash.slice(1); 84 | }, 85 | 86 | // Sets the next interval for the timer 87 | setTimeout: function() { 88 | window.setTimeout(fix.pollerInterval, fix.pollerRate); 89 | }, 90 | 91 | // Creates a new hashchange event object 92 | createEventObject: function(oldURL, newURL) { 93 | return new window.HashChangeEvent(oldURL, newURL); 94 | }, 95 | 96 | // Runs on an interval testing the hash 97 | pollerInterval: function() { 98 | var hash = fix.getHash(); 99 | if (hash !== fix.lastHash) { 100 | var funcs = fix.listeners.funcs.slice(0); 101 | if (typeof window.onhashchange === 'function') { 102 | funcs.push(window.onhashchange); 103 | } 104 | for (var i = 0, c = funcs.length; i < c; i++) { 105 | var evt = fix.createEventObject({ 106 | oldURL: fix.lastLocation, 107 | newURL: String(location) 108 | }); 109 | } 110 | fix.lastHash = fix.getHash(); 111 | fix.lastLocation = String(location); 112 | } 113 | fix.setTimeout(); 114 | } 115 | 116 | }; 117 | 118 | fix.init(); 119 | 120 | }()); 121 | -------------------------------------------------------------------------------- /src/ui/draw/circle.js: -------------------------------------------------------------------------------- 1 | // custom mapbopx-gl-draw mode that extends draw_line_string 2 | // shows a center point, radius line, and circle polygon while drawing 3 | // forces draw.create on creation of second vertex 4 | const circle = require('@turf/circle').default; 5 | const length = require('@turf/length').default; 6 | const MapboxDraw = require('@mapbox/mapbox-gl-draw'); 7 | 8 | const { getDisplayMeasurements } = require('./util.js'); 9 | 10 | function circleFromTwoVertexLineString(geojson) { 11 | const center = geojson.geometry.coordinates[0]; 12 | const radiusInKm = length(geojson); 13 | 14 | return circle(center, radiusInKm); 15 | } 16 | 17 | const CircleMode = { 18 | ...MapboxDraw.modes.draw_line_string, 19 | 20 | clickAnywhere: function (state, e) { 21 | // this ends the drawing after the user creates a second point, triggering this.onStop 22 | if (state.currentVertexPosition === 1) { 23 | state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); 24 | return this.changeMode('simple_select', { featureIds: [state.line.id] }); 25 | } 26 | 27 | state.line.updateCoordinate( 28 | state.currentVertexPosition, 29 | e.lngLat.lng, 30 | e.lngLat.lat 31 | ); 32 | if (state.direction === 'forward') { 33 | state.currentVertexPosition += 1; 34 | state.line.updateCoordinate( 35 | state.currentVertexPosition, 36 | e.lngLat.lng, 37 | e.lngLat.lat 38 | ); 39 | } else { 40 | state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); 41 | } 42 | 43 | return null; 44 | }, 45 | 46 | onStop: function (state) { 47 | // remove last added coordinate 48 | state.line.removeCoordinate('0'); 49 | if (state.line.isValid()) { 50 | const lineGeoJson = state.line.toGeoJSON(); 51 | const circleFeature = circleFromTwoVertexLineString(lineGeoJson); 52 | 53 | this.map.fire('draw.create', { 54 | features: [circleFeature] 55 | }); 56 | } else { 57 | this.deleteFeature([state.line.id], { silent: true }); 58 | this.changeMode('simple_select', {}, { silent: true }); 59 | } 60 | }, 61 | 62 | toDisplayFeatures: function (state, geojson, display) { 63 | // Only render the line if it has at least one real coordinate 64 | if (geojson.geometry.coordinates.length < 2) return null; 65 | 66 | display({ 67 | type: 'Feature', 68 | properties: { 69 | active: 'true' 70 | }, 71 | geometry: { 72 | type: 'Point', 73 | coordinates: geojson.geometry.coordinates[0] 74 | } 75 | }); 76 | 77 | // displays the line as it is drawn 78 | geojson.properties.active = 'true'; 79 | display(geojson); 80 | 81 | const displayMeasurements = getDisplayMeasurements(geojson); 82 | 83 | // create custom feature for the current pointer position 84 | const currentVertex = { 85 | type: 'Feature', 86 | properties: { 87 | meta: 'currentPosition', 88 | radius: `${displayMeasurements.metric} ${displayMeasurements.standard}`, 89 | parent: state.line.id 90 | }, 91 | geometry: { 92 | type: 'Point', 93 | coordinates: geojson.geometry.coordinates[1] 94 | } 95 | }; 96 | 97 | display(currentVertex); 98 | 99 | const circleFeature = circleFromTwoVertexLineString(geojson); 100 | 101 | circleFeature.properties = { 102 | active: 'true' 103 | }; 104 | 105 | display(circleFeature); 106 | 107 | return null; 108 | } 109 | }; 110 | 111 | module.exports = CircleMode; 112 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About Edit GeoJSON 6 | 7 | 8 | 9 | 26 | 27 | 28 |

geojson.io

29 |

A fast, simple tool to create, change, and publish maps.

30 |

Chrome Extension

31 | 32 |

33 | Download the latest release of the geojson.io extension 34 | to add a 'geojson.io' button to applicable Gists and files on GitHub. 35 |

36 |

Open Source

37 |

This site is an open source project.

38 |

License & Restrictions

39 |

This is open source software under the permissive MIT license. 40 | That license applies to the software and not the data you 41 | create with geojson.io - you can safely edit private, copywritten, 42 | open, or any other kind of data with this tool without the tool changing 43 | its legal status.

44 |

FAQ

45 |

So I've made a map, what do I do now?

46 |

If you just want to share your map, you can click the 'Share' tab 47 | and send a link to friends, or grab HTML for embedding it on your 48 | website.

49 |

For more flexibility, download the GeoJSON data and you can use it 50 | with a number of different tools:

51 | 59 |

How Do I Import Shapefiles or unsupported formats?

60 |

geojson.io doesn't support Shapefile import directly, but you can 61 | convert Shapefiles to GeoJSON first, and then upload GeoJSON to 62 | geojson.io.

63 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/source/gist.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const tmpl = fs.readFileSync('data/share.html', 'utf8'); 3 | 4 | const config = require('../config.js')(location.hostname); 5 | const githubBase = config.GithubAPI 6 | ? config.GithubAPI + '/api/v3' 7 | : 'https://api.github.com'; 8 | 9 | module.exports.save = save; 10 | module.exports.saveBlocks = saveBlocks; 11 | module.exports.load = load; 12 | module.exports.loadRaw = loadRaw; 13 | 14 | function saveBlocks(content, callback) { 15 | d3.json(githubBase + '/gists') 16 | .on('load', (data) => { 17 | callback(null, data); 18 | }) 19 | .on('error', (err) => { 20 | const url = /(http:\/\/\S*)/g; 21 | 22 | const message = JSON.parse(err.responseText).message.replace( 23 | url, 24 | '$&' 25 | ); 26 | 27 | callback(message); 28 | }) 29 | .send( 30 | 'POST', 31 | JSON.stringify({ 32 | description: 'via:geojson.io', 33 | public: false, 34 | files: { 35 | 'index.html': { content: tmpl }, 36 | 'map.geojson': { content: JSON.stringify(content) } 37 | } 38 | }) 39 | ); 40 | } 41 | 42 | function save(context, callback) { 43 | const meta = context.data.get('meta'); 44 | const name = (meta && meta.name) || 'map.geojson'; 45 | const map = context.data.get('map'); 46 | context.user.details(onuser); 47 | 48 | function onuser(err, user) { 49 | let method = 'POST'; 50 | const source = context.data.get('source'); 51 | const files = {}; 52 | let endpoint = githubBase + '/gists'; 53 | 54 | if ( 55 | !err && 56 | user && 57 | user.login && 58 | meta && 59 | // check that it's not previously a github 60 | source && 61 | source.id && 62 | // and it is mine 63 | meta.login && 64 | user.login === meta.login 65 | ) { 66 | endpoint += '/' + source.id; 67 | method = 'PATCH'; 68 | } else if (!err && source && source.id) { 69 | endpoint += '/' + source.id + '/forks'; 70 | } 71 | 72 | files[name] = { 73 | content: JSON.stringify(map, null, 2) 74 | }; 75 | 76 | context.user 77 | .signXHR(d3.json(endpoint)) 78 | .on('load', (data) => { 79 | data.type = 'gist'; 80 | callback(null, data); 81 | }) 82 | .on('error', (err) => { 83 | let message; 84 | const url = /(http:\/\/\S*)/g; 85 | 86 | try { 87 | message = JSON.parse(err.responseText).message.replace( 88 | url, 89 | '$&' 90 | ); 91 | } catch (e) { 92 | message = 'Sorry, an error occurred'; 93 | } 94 | 95 | callback(message); 96 | }) 97 | .send( 98 | method, 99 | JSON.stringify({ 100 | files: files 101 | }) 102 | ); 103 | } 104 | } 105 | 106 | function load(id, context, callback) { 107 | const endpoint = githubBase + '/gists/'; 108 | context.user 109 | .signXHR(d3.json(endpoint + id)) 110 | .on('load', onLoad) 111 | .on('error', onError) 112 | .get(); 113 | 114 | function onLoad(json) { 115 | callback(null, json); 116 | } 117 | function onError(err) { 118 | callback(err, null); 119 | } 120 | } 121 | 122 | function loadRaw(url, context, callback) { 123 | context.user 124 | .signXHR(d3.text(url)) 125 | .on('load', onLoad) 126 | .on('error', onError) 127 | .get(); 128 | 129 | function onLoad(file) { 130 | callback(null, file); 131 | } 132 | function onError(err) { 133 | callback(err, null); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/meta.js: -------------------------------------------------------------------------------- 1 | const escape = require('escape-html'), 2 | geojsonRandom = require('geojson-random'), 3 | geojsonExtent = require('@mapbox/geojson-extent'), 4 | geojsonFlatten = require('geojson-flatten'), 5 | polyline = require('@mapbox/polyline'), 6 | wkx = require('wkx'), 7 | zoomextent = require('../lib/zoomextent'); 8 | 9 | module.exports.adduserlayer = function (context, _url, _name) { 10 | const url = escape(_url), 11 | name = escape(_name); 12 | 13 | // reset the control if a user-layer was added before 14 | d3.select('.user-layer-button').remove(); 15 | 16 | function addUserSourceAndLayer() { 17 | // if the source and layer aren't present, add them 18 | context.map.setStyle({ 19 | name: 'user-layer', 20 | version: 8, 21 | sources: { 22 | 'user-layer': { 23 | type: 'raster', 24 | tiles: [url], 25 | tileSize: 256 26 | } 27 | }, 28 | layers: [ 29 | { 30 | id: 'user-layer', 31 | type: 'raster', 32 | source: 'user-layer', 33 | minzoom: 0, 34 | maxzoom: 22 35 | } 36 | ] 37 | }); 38 | 39 | // make this layer's button active 40 | d3.select('.layer-switch .active').classed('active', false); 41 | d3.select('.user-layer-button').classed('active', true); 42 | 43 | context.data.set({ 44 | mapStyleLoaded: true 45 | }); 46 | } 47 | 48 | // append a button to the existing style selection UI 49 | d3.select('.layer-switch') 50 | .append('button') 51 | .attr('class', 'pad0x user-layer-button') 52 | .on('click', addUserSourceAndLayer) 53 | .text(name); 54 | 55 | addUserSourceAndLayer(); 56 | }; 57 | 58 | module.exports.zoomextent = function (context) { 59 | zoomextent(context); 60 | }; 61 | 62 | module.exports.clear = function (context) { 63 | context.data.clear(); 64 | }; 65 | 66 | module.exports.random = function (context, count, type) { 67 | context.data.mergeFeatures(geojsonRandom(count, type).features, 'meta'); 68 | }; 69 | 70 | module.exports.bboxify = function (context) { 71 | context.data.set({ map: geojsonExtent.bboxify(context.data.get('map')) }); 72 | }; 73 | 74 | module.exports.flatten = function (context) { 75 | context.data.set({ map: geojsonFlatten(context.data.get('map')) }); 76 | }; 77 | 78 | module.exports.polyline = function (context) { 79 | const input = prompt('Enter your polyline'); 80 | try { 81 | const decoded = polyline.toGeoJSON(input); 82 | context.data.set({ map: decoded }); 83 | } catch (e) { 84 | alert('Sorry, we were unable to decode that polyline'); 85 | } 86 | }; 87 | 88 | module.exports.wkxBase64 = function (context) { 89 | const input = prompt('Enter your Base64 encoded WKB/EWKB'); 90 | try { 91 | const decoded = wkx.Geometry.parse(Buffer.from(input, 'base64')); 92 | context.data.set({ map: decoded.toGeoJSON() }); 93 | zoomextent(context); 94 | } catch (e) { 95 | console.error(e); 96 | alert('Sorry, we were unable to decode that Base64 encoded WKX data'); 97 | } 98 | }; 99 | 100 | module.exports.wkxHex = function (context) { 101 | const input = prompt('Enter your Hex encoded WKB/EWKB'); 102 | try { 103 | const decoded = wkx.Geometry.parse(Buffer.from(input, 'hex')); 104 | context.data.set({ map: decoded.toGeoJSON() }); 105 | zoomextent(context); 106 | } catch (e) { 107 | console.error(e); 108 | alert('Sorry, we were unable to decode that Hex encoded WKX data'); 109 | } 110 | }; 111 | 112 | module.exports.wkxString = function (context) { 113 | const input = prompt('Enter your WKT/EWKT String'); 114 | try { 115 | const decoded = wkx.Geometry.parse(input); 116 | context.data.set({ map: decoded.toGeoJSON() }); 117 | zoomextent(context); 118 | } catch (e) { 119 | console.error(e); 120 | alert('Sorry, we were unable to decode that WKT data'); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/source/github.js: -------------------------------------------------------------------------------- 1 | module.exports.save = save; 2 | module.exports.load = load; 3 | module.exports.loadRaw = loadRaw; 4 | 5 | const config = require('../config.js')(location.hostname); 6 | const githubBase = config.GithubAPI 7 | ? config.GithubAPI + '/api/v3' 8 | : 'https://api.github.com'; 9 | 10 | function save(context, callback) { 11 | const source = context.data.get('source'), 12 | meta = context.data.get('meta'), 13 | newpath = context.data.get('newpath'), 14 | name = (meta && meta.name) || 'map.geojson', 15 | map = context.data.get('map'); 16 | 17 | if (navigator.appVersion.indexOf('MSIE 9') !== -1 || !window.XMLHttpRequest) { 18 | return alert( 19 | 'Sorry, saving and sharing is not supported in IE9 and lower. ' + 20 | 'Please use a modern browser to enjoy the full featureset of geojson.io' 21 | ); 22 | } 23 | 24 | if (!localStorage.github_token) { 25 | return alert('You need to log in with GitHub to commit changes'); 26 | } 27 | 28 | context.repo.details(onrepo); 29 | 30 | function onrepo(err, repo) { 31 | let commitMessage; 32 | let endpoint; 33 | let method = 'POST'; 34 | let data = {}; 35 | const files = {}; 36 | 37 | if (!err && repo.permissions.push) { 38 | commitMessage = context.commitMessage || prompt('Commit message:'); 39 | if (!commitMessage) return; 40 | 41 | endpoint = source.url; 42 | method = 'PUT'; 43 | data = { 44 | message: commitMessage, 45 | branch: meta.branch, 46 | content: btoa( 47 | encodeURIComponent(JSON.stringify(map, null, 2)).replace( 48 | /%([0-9A-F]{2})/g, 49 | (match, p1) => { 50 | return String.fromCharCode('0x' + p1); 51 | } 52 | ) 53 | ) 54 | }; 55 | 56 | // creating a file 57 | if (newpath) { 58 | data.path = newpath; 59 | context.data.set({ newpath: null }); 60 | } 61 | 62 | // updating a file 63 | if (source.sha) { 64 | data.sha = source.sha; 65 | } 66 | } else { 67 | endpoint = githubBase + '/gists'; 68 | files[name] = { content: JSON.stringify(map, null, 2) }; 69 | data = { files: files }; 70 | } 71 | 72 | context.user 73 | .signXHR(d3.json(endpoint)) 74 | .on('load', (data) => { 75 | callback(null, data); 76 | }) 77 | .on('error', (err) => { 78 | let message; 79 | const url = /(http:\/\/\S*)/g; 80 | 81 | try { 82 | message = JSON.parse(err.responseText).message.replace( 83 | url, 84 | '$&' 85 | ); 86 | } catch (e) { 87 | message = 'Sorry, an error occurred.'; 88 | } 89 | 90 | callback(message); 91 | }) 92 | .send(method, JSON.stringify(data)); 93 | } 94 | } 95 | 96 | function load(parts, context, callback) { 97 | context.user 98 | .signXHR(d3.json(fileUrl(parts))) 99 | .on('load', onLoad) 100 | .on('error', onError) 101 | .get(); 102 | 103 | function onLoad(file) { 104 | callback(null, file); 105 | } 106 | function onError(err) { 107 | callback(err, null); 108 | } 109 | } 110 | 111 | function loadRaw(parts, sha, context, callback) { 112 | context.user 113 | .signXHR(d3.text(shaUrl(parts, sha))) 114 | .on('load', onLoad) 115 | .on('error', onError) 116 | .header('Accept', 'application/vnd.github.raw') 117 | .get(); 118 | 119 | function onLoad(file) { 120 | callback(null, file); 121 | } 122 | function onError(err) { 123 | callback(err, null); 124 | } 125 | } 126 | 127 | function fileUrl(parts) { 128 | return ( 129 | githubBase + 130 | '/repos/' + 131 | parts.user + 132 | '/' + 133 | parts.repo + 134 | '/contents/' + 135 | parts.path + 136 | '?ref=' + 137 | parts.branch 138 | ); 139 | } 140 | 141 | function shaUrl(parts, sha) { 142 | return ( 143 | githubBase + '/repos/' + parts.user + '/' + parts.repo + '/git/blobs/' + sha 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /src/ui/draw/rectangle.js: -------------------------------------------------------------------------------- 1 | // from https://github.com/thegisdev/mapbox-gl-draw-rectangle-mode 2 | const doubleClickZoom = { 3 | enable: (ctx) => { 4 | setTimeout(() => { 5 | // First check we've got a map and some context. 6 | if ( 7 | !ctx.map || 8 | !ctx.map.doubleClickZoom || 9 | !ctx._ctx || 10 | !ctx._ctx.store || 11 | !ctx._ctx.store.getInitialConfigValue 12 | ) 13 | return; 14 | // Now check initial state wasn't false (we leave it disabled if so) 15 | if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return; 16 | ctx.map.doubleClickZoom.enable(); 17 | }, 0); 18 | }, 19 | disable(ctx) { 20 | setTimeout(() => { 21 | if (!ctx.map || !ctx.map.doubleClickZoom) return; 22 | // Always disable here, as it's necessary in some cases. 23 | ctx.map.doubleClickZoom.disable(); 24 | }, 0); 25 | } 26 | }; 27 | 28 | const DrawRectangle = { 29 | // When the mode starts this function will be called. 30 | onSetup: function () { 31 | const rectangle = this.newFeature({ 32 | type: 'Feature', 33 | properties: {}, 34 | geometry: { 35 | type: 'Polygon', 36 | coordinates: [[]] 37 | } 38 | }); 39 | this.addFeature(rectangle); 40 | this.clearSelectedFeatures(); 41 | doubleClickZoom.disable(this); 42 | this.updateUIClasses({ mouse: 'add' }); 43 | this.setActionableState({ 44 | trash: true 45 | }); 46 | return { 47 | rectangle 48 | }; 49 | }, 50 | // support mobile taps 51 | onTap: function (state, e) { 52 | // emulate 'move mouse' to update feature coords 53 | if (state.startPoint) this.onMouseMove(state, e); 54 | // emulate onClick 55 | this.onClick(state, e); 56 | }, 57 | // Whenever a user clicks on the map, Draw will call `onClick` 58 | onClick: function (state, e) { 59 | // if state.startPoint exist, means its second click 60 | // change to simple_select mode 61 | if ( 62 | state.startPoint && 63 | state.startPoint[0] !== e.lngLat.lng && 64 | state.startPoint[1] !== e.lngLat.lat 65 | ) { 66 | this.updateUIClasses({ mouse: 'pointer' }); 67 | state.endPoint = [e.lngLat.lng, e.lngLat.lat]; 68 | this.changeMode('simple_select', { featuresId: state.rectangle.id }); 69 | } 70 | // on first click, save clicked point coords as starting for rectangle 71 | const startPoint = [e.lngLat.lng, e.lngLat.lat]; 72 | state.startPoint = startPoint; 73 | }, 74 | onMouseMove: function (state, e) { 75 | // if startPoint, update the feature coordinates, using the bounding box concept 76 | // we are simply using the startingPoint coordinates and the current Mouse Position 77 | // coordinates to calculate the bounding box on the fly, which will be our rectangle 78 | if (state.startPoint) { 79 | state.rectangle.updateCoordinate( 80 | '0.0', 81 | state.startPoint[0], 82 | state.startPoint[1] 83 | ); // minX, minY - the starting point 84 | state.rectangle.updateCoordinate( 85 | '0.1', 86 | e.lngLat.lng, 87 | state.startPoint[1] 88 | ); // maxX, minY 89 | state.rectangle.updateCoordinate('0.2', e.lngLat.lng, e.lngLat.lat); // maxX, maxY 90 | state.rectangle.updateCoordinate( 91 | '0.3', 92 | state.startPoint[0], 93 | e.lngLat.lat 94 | ); // minX,maxY 95 | state.rectangle.updateCoordinate( 96 | '0.4', 97 | state.startPoint[0], 98 | state.startPoint[1] 99 | ); // minX,minY - ending point (equals to starting point) 100 | } 101 | }, 102 | // Whenever a user clicks on a key while focused on the map, it will be sent here 103 | onKeyUp: function (state, e) { 104 | if (e.keyCode === 27) return this.changeMode('simple_select'); 105 | }, 106 | onStop: function (state) { 107 | doubleClickZoom.enable(this); 108 | this.updateUIClasses({ mouse: 'none' }); 109 | this.activateUIButton(); 110 | 111 | // check to see if we've deleted this feature 112 | if (this.getFeature(state.rectangle.id) === undefined) return; 113 | 114 | // remove last added coordinate 115 | state.rectangle.removeCoordinate('0.4'); 116 | if (state.rectangle.isValid()) { 117 | this.map.fire('draw.create', { 118 | features: [state.rectangle.toGeoJSON()] 119 | }); 120 | } else { 121 | this.deleteFeature([state.rectangle.id], { silent: true }); 122 | this.changeMode('simple_select', {}, { silent: true }); 123 | } 124 | }, 125 | toDisplayFeatures: function (state, geojson, display) { 126 | const isActivePolygon = geojson.properties.id === state.rectangle.id; 127 | geojson.properties.active = isActivePolygon ? 'true' : 'false'; 128 | if (!isActivePolygon) return display(geojson); 129 | 130 | // Only render the rectangular polygon if it has the starting point 131 | if (!state.startPoint) return; 132 | return display(geojson); 133 | }, 134 | onTrash: function (state) { 135 | this.deleteFeature([state.rectangle.id], { silent: true }); 136 | this.changeMode('simple_select'); 137 | } 138 | }; 139 | 140 | module.exports = DrawRectangle; 141 | -------------------------------------------------------------------------------- /lib/d3-tooltip.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | 3 | var bootstrap = (typeof exports.bootstrap === "object") ? 4 | exports.bootstrap : 5 | (exports.bootstrap = {}); 6 | 7 | bootstrap.tooltip = function() { 8 | 9 | var tooltip = function(selection) { 10 | selection.each(setup); 11 | }, 12 | animation = d3.functor(false), 13 | html = d3.functor(false), 14 | title = function() { 15 | var title = this.getAttribute("data-original-title"); 16 | if (title) { 17 | return title; 18 | } else { 19 | title = this.getAttribute("title"); 20 | this.removeAttribute("title"); 21 | this.setAttribute("data-original-title", title); 22 | } 23 | return title; 24 | }, 25 | over = "mouseenter.tooltip", 26 | out = "mouseleave.tooltip", 27 | placements = "top left bottom right".split(" "), 28 | placement = d3.functor("top"); 29 | 30 | tooltip.title = function(_) { 31 | if (arguments.length) { 32 | title = d3.functor(_); 33 | return tooltip; 34 | } else { 35 | return title; 36 | } 37 | }; 38 | 39 | tooltip.html = function(_) { 40 | if (arguments.length) { 41 | html = d3.functor(_); 42 | return tooltip; 43 | } else { 44 | return html; 45 | } 46 | }; 47 | 48 | tooltip.placement = function(_) { 49 | if (arguments.length) { 50 | placement = d3.functor(_); 51 | return tooltip; 52 | } else { 53 | return placement; 54 | } 55 | }; 56 | 57 | tooltip.show = function(selection) { 58 | selection.each(show); 59 | }; 60 | 61 | tooltip.hide = function(selection) { 62 | selection.each(hide); 63 | }; 64 | 65 | tooltip.toggle = function(selection) { 66 | selection.each(toggle); 67 | }; 68 | 69 | tooltip.destroy = function(selection) { 70 | selection 71 | .on(over, null) 72 | .on(out, null) 73 | .attr("title", function() { 74 | return this.getAttribute("data-original-title") || this.getAttribute("title"); 75 | }) 76 | .attr("data-original-title", null) 77 | .select(".tooltip") 78 | .remove(); 79 | }; 80 | 81 | function setup() { 82 | var root = d3.select(this), 83 | animate = animation.apply(this, arguments), 84 | tip = root.append("div") 85 | .attr("class", "tooltip"); 86 | 87 | if (animate) { 88 | tip.classed("fade", true); 89 | } 90 | 91 | // TODO "inside" checks? 92 | 93 | tip.append("div") 94 | .attr("class", "tooltip-arrow"); 95 | tip.append("div") 96 | .attr("class", "tooltip-inner"); 97 | 98 | var place = placement.apply(this, arguments); 99 | tip.classed(place, true); 100 | 101 | root.on(over, show); 102 | root.on(out, hide); 103 | } 104 | 105 | function show() { 106 | var root = d3.select(this), 107 | content = title.apply(this, arguments), 108 | tip = root.select(".tooltip") 109 | .classed("in", true), 110 | markup = html.apply(this, arguments), 111 | innercontent = tip.select(".tooltip-inner")[markup ? "html" : "text"](content), 112 | place = placement.apply(this, arguments), 113 | pos = root.style("position"), 114 | outer = getPosition(root.node()), 115 | inner = getPosition(tip.node()), 116 | style; 117 | 118 | if (pos === "absolute" || pos === "relative") { 119 | outer.x = outer.y = 0; 120 | } 121 | 122 | var style; 123 | switch (place) { 124 | case "top": 125 | style = {x: outer.x + (outer.w - inner.w) / 2, y: outer.y - inner.h}; 126 | break; 127 | case "right": 128 | style = {x: outer.x + outer.w, y: outer.y + (outer.h - inner.h) / 2}; 129 | break; 130 | case "left": 131 | style = {x: outer.x - inner.w, y: outer.y + (outer.h - inner.h) / 2}; 132 | break; 133 | case "bottom": 134 | style = {x: Math.max(0, outer.x + (outer.w - inner.w) / 2), y: outer.y + outer.h}; 135 | break; 136 | } 137 | 138 | tip.style(style ? 139 | {left: ~~style.x + "px", top: ~~style.y + "px"} : 140 | {left: null, top: null}); 141 | 142 | this.tooltipVisible = true; 143 | } 144 | 145 | function hide() { 146 | d3.select(this).select(".tooltip") 147 | .classed("in", false); 148 | 149 | this.tooltipVisible = false; 150 | } 151 | 152 | function toggle() { 153 | if (this.tooltipVisible) { 154 | hide.apply(this, arguments); 155 | } else { 156 | show.apply(this, arguments); 157 | } 158 | } 159 | 160 | return tooltip; 161 | }; 162 | 163 | function getPosition(node) { 164 | var mode = d3.select(node).style('position'); 165 | if (mode === 'absolute' || mode === 'static') { 166 | return { 167 | x: node.offsetLeft, 168 | y: node.offsetTop, 169 | w: node.offsetWidth, 170 | h: node.offsetHeight 171 | }; 172 | } else { 173 | return { 174 | x: 0, 175 | y: 0, 176 | w: node.offsetWidth, 177 | h: node.offsetHeight 178 | }; 179 | } 180 | } 181 | 182 | })(this); 183 | -------------------------------------------------------------------------------- /lib/blob.js: -------------------------------------------------------------------------------- 1 | /* Blob.js 2 | * A Blob implementation. 3 | * 2013-06-20 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * By Devin Samarin, https://github.com/eboyjr 7 | * License: X11/MIT 8 | * See LICENSE.md 9 | */ 10 | 11 | /*global self, unescape */ 12 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 13 | plusplus: true */ 14 | 15 | /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ 16 | 17 | if (typeof Blob !== "function" || typeof URL === "undefined") 18 | if (typeof Blob === "function" && typeof webkitURL !== "undefined") self.URL = webkitURL; 19 | else var Blob = (function (view) { 20 | "use strict"; 21 | 22 | var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) { 23 | var 24 | get_class = function(object) { 25 | return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; 26 | } 27 | , FakeBlobBuilder = function BlobBuilder() { 28 | this.data = []; 29 | } 30 | , FakeBlob = function Blob(data, type, encoding) { 31 | this.data = data; 32 | this.size = data.length; 33 | this.type = type; 34 | this.encoding = encoding; 35 | } 36 | , FBB_proto = FakeBlobBuilder.prototype 37 | , FB_proto = FakeBlob.prototype 38 | , FileReaderSync = view.FileReaderSync 39 | , FileException = function(type) { 40 | this.code = this[this.name = type]; 41 | } 42 | , file_ex_codes = ( 43 | "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " 44 | + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" 45 | ).split(" ") 46 | , file_ex_code = file_ex_codes.length 47 | , real_URL = view.URL || view.webkitURL || view 48 | , real_create_object_URL = real_URL.createObjectURL 49 | , real_revoke_object_URL = real_URL.revokeObjectURL 50 | , URL = real_URL 51 | , btoa = view.btoa 52 | , atob = view.atob 53 | 54 | , ArrayBuffer = view.ArrayBuffer 55 | , Uint8Array = view.Uint8Array 56 | ; 57 | FakeBlob.fake = FB_proto.fake = true; 58 | while (file_ex_code--) { 59 | FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; 60 | } 61 | if (!real_URL.createObjectURL) { 62 | URL = view.URL = {}; 63 | } 64 | URL.createObjectURL = function(blob) { 65 | var 66 | type = blob.type 67 | , data_URI_header 68 | ; 69 | if (type === null) { 70 | type = "application/octet-stream"; 71 | } 72 | if (blob instanceof FakeBlob) { 73 | data_URI_header = "data:" + type; 74 | if (blob.encoding === "base64") { 75 | return data_URI_header + ";base64," + blob.data; 76 | } else if (blob.encoding === "URI") { 77 | return data_URI_header + "," + decodeURIComponent(blob.data); 78 | } if (btoa) { 79 | return data_URI_header + ";base64," + btoa(blob.data); 80 | } else { 81 | return data_URI_header + "," + encodeURIComponent(blob.data); 82 | } 83 | } else if (real_create_object_URL) { 84 | return real_create_object_URL.call(real_URL, blob); 85 | } 86 | }; 87 | URL.revokeObjectURL = function(object_URL) { 88 | if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { 89 | real_revoke_object_URL.call(real_URL, object_URL); 90 | } 91 | }; 92 | FBB_proto.append = function(data/*, endings*/) { 93 | var bb = this.data; 94 | // decode data to a binary string 95 | if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { 96 | var 97 | str = "" 98 | , buf = new Uint8Array(data) 99 | , i = 0 100 | , buf_len = buf.length 101 | ; 102 | for (; i < buf_len; i++) { 103 | str += String.fromCharCode(buf[i]); 104 | } 105 | bb.push(str); 106 | } else if (get_class(data) === "Blob" || get_class(data) === "File") { 107 | if (FileReaderSync) { 108 | var fr = new FileReaderSync; 109 | bb.push(fr.readAsBinaryString(data)); 110 | } else { 111 | // async FileReader won't work as BlobBuilder is sync 112 | throw new FileException("NOT_READABLE_ERR"); 113 | } 114 | } else if (data instanceof FakeBlob) { 115 | if (data.encoding === "base64" && atob) { 116 | bb.push(atob(data.data)); 117 | } else if (data.encoding === "URI") { 118 | bb.push(decodeURIComponent(data.data)); 119 | } else if (data.encoding === "raw") { 120 | bb.push(data.data); 121 | } 122 | } else { 123 | if (typeof data !== "string") { 124 | data += ""; // convert unsupported types to strings 125 | } 126 | // decode UTF-16 to binary string 127 | bb.push(unescape(encodeURIComponent(data))); 128 | } 129 | }; 130 | FBB_proto.getBlob = function(type) { 131 | if (!arguments.length) { 132 | type = null; 133 | } 134 | return new FakeBlob(this.data.join(""), type, "raw"); 135 | }; 136 | FBB_proto.toString = function() { 137 | return "[object BlobBuilder]"; 138 | }; 139 | FB_proto.slice = function(start, end, type) { 140 | var args = arguments.length; 141 | if (args < 3) { 142 | type = null; 143 | } 144 | return new FakeBlob( 145 | this.data.slice(start, args > 1 ? end : this.data.length) 146 | , type 147 | , this.encoding 148 | ); 149 | }; 150 | FB_proto.toString = function() { 151 | return "[object Blob]"; 152 | }; 153 | return FakeBlobBuilder; 154 | }(view)); 155 | 156 | return function Blob(blobParts, options) { 157 | var type = options ? (options.type || "") : ""; 158 | var builder = new BlobBuilder(); 159 | if (blobParts) { 160 | for (var i = 0, len = blobParts.length; i < len; i++) { 161 | builder.append(blobParts[i]); 162 | } 163 | } 164 | return builder.getBlob(type); 165 | }; 166 | }(self)); 167 | -------------------------------------------------------------------------------- /src/panel/json.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { createPopper } = require('@popperjs/core'); 3 | const geojsonNormalize = require('@mapbox/geojson-normalize'); 4 | 5 | const CodeMirror = require('codemirror/lib/codemirror'); 6 | require('codemirror/addon/fold/foldcode'); 7 | require('codemirror/addon/fold/foldgutter'); 8 | require('codemirror/addon/fold/brace-fold'); 9 | require('codemirror/addon/edit/matchbrackets'); 10 | require('codemirror/mode/javascript/javascript'); 11 | 12 | const validate = require('../lib/validate'); 13 | const zoomextent = require('../lib/zoomextent'); 14 | const saver = require('../ui/saver.js'); 15 | const flash = require('../ui/flash'); 16 | 17 | module.exports = function (context) { 18 | CodeMirror.keyMap.tabSpace = { 19 | Tab: function (cm) { 20 | const spaces = new Array(cm.getOption('indentUnit') + 1).join(' '); 21 | cm.replaceSelection(spaces, 'end', '+input'); 22 | }, 23 | 'Ctrl-S': saveAction, 24 | 'Cmd-S': saveAction, 25 | fallthrough: ['default'] 26 | }; 27 | 28 | function saveAction() { 29 | saver(context); 30 | return false; 31 | } 32 | 33 | function render(selection) { 34 | const textarea = selection.html('').append('textarea'); 35 | 36 | // copy button tooltip 37 | const tooltip = selection 38 | .append('div') 39 | .attr('id', 'tooltip') 40 | .attr( 41 | 'class', 42 | 'opacity-0 text-white font-medium text-xs rounded text-left py-1 px-2 bg-mb-gray-dark transition-opacity duration-100' 43 | ) 44 | .attr('role', 'tooltip') 45 | .text('Copied!'); 46 | 47 | // tooltip arrow 48 | tooltip 49 | .append('div') 50 | .attr('id', 'arrow') 51 | .attr( 52 | 'class', 53 | '-right-1 top-0 translate-y-2 absolute w-2 h-2 bg-transparent before:opacity-0 before:transition-opacity before:duration-100 group-hover:before:opacity-100 before:absolute before:w-2 before:h-2 before:rotate-45 before:bg-mb-gray-dark' 54 | ) 55 | .attr('data-popper-arrow', ''); 56 | 57 | // adds a copy button 58 | const buttonContainer = selection 59 | .append('div') 60 | .attr( 61 | 'class', 62 | 'mapboxgl-ctrl mapboxgl-ctrl-group absolute right-5 top-5 opacity-0 group-hover:opacity-100 transition-opacity duration-100' 63 | ); 64 | 65 | const editor = CodeMirror.fromTextArea(textarea.node(), { 66 | mode: { name: 'javascript', json: true }, 67 | matchBrackets: true, 68 | tabSize: 2, 69 | gutters: ['error', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 70 | theme: 'eclipse', 71 | autofocus: window === window.top, 72 | keyMap: 'tabSpace', 73 | lineNumbers: true, 74 | foldGutter: true 75 | }); 76 | 77 | const button = buttonContainer 78 | .append('button') 79 | .attr('id', 'copy-button') 80 | .attr('title', 'Copy'); 81 | 82 | const buttonIcon = button 83 | .append('span') 84 | .attr('class', 'fa-solid fa-copy text-gray-500'); 85 | 86 | button.on('click', () => { 87 | // copy to clipboard 88 | navigator.clipboard.writeText(editor.getValue()); 89 | 90 | // set the button to a green checkmark 91 | buttonIcon 92 | .classed('fa-copy', false) 93 | .attr('class', 'fa-solid fa-check text-green-600'); 94 | // show tooltip 95 | tooltip.classed('group-hover:opacity-100', true); 96 | setTimeout(() => { 97 | buttonIcon.attr('class', 'fa-solid fa-copy text-gray-500'); 98 | // hide tooltip 99 | tooltip.classed('group-hover:opacity-100', false); 100 | }, 3000); 101 | }); 102 | 103 | const copyButtonEl = document.querySelector('#copy-button'); 104 | const tooltipEl = document.querySelector('#tooltip'); 105 | 106 | createPopper(copyButtonEl, tooltipEl, { 107 | placement: 'left', 108 | modifiers: [ 109 | { 110 | name: 'offset', 111 | options: { 112 | offset: [0, 8] 113 | } 114 | } 115 | ] 116 | }); 117 | 118 | // blur the editor so map keybindings will work on initial load 119 | editor.display.input.blur(); 120 | 121 | editor.foldCode(CodeMirror.Pos(0, 0)); 122 | editor.matchBrackets(); 123 | 124 | editor.on('beforeChange', (cm, change) => { 125 | if (change.origin === 'paste') { 126 | try { 127 | const newText = JSON.stringify(JSON.parse(change.text[0]), null, 2); 128 | change.update(null, null, newText.split('\n')); 129 | } catch (e) { 130 | console.log('error pretty-printing pasted geojson', e); 131 | } 132 | } 133 | }); 134 | 135 | editor.on('change', validate(changeValidated)); 136 | 137 | function changeValidated(err, data, zoom) { 138 | if (!err) { 139 | let updateSource = 'json'; 140 | const originalType = data.type; 141 | // normalize into a FeatureCollection 142 | if ( 143 | [ 144 | 'Feature', 145 | 'GeometryCollection', 146 | 'Point', 147 | 'LineString', 148 | 'Polygon', 149 | 'MultiPoint', 150 | 'MultiLineString', 151 | 'MultiPolygon' 152 | ].includes(data.type) 153 | ) { 154 | data = geojsonNormalize(data); 155 | updateSource = 'geojsonNormalize'; 156 | 157 | flash( 158 | context.container, 159 | `The imported ${originalType} was normalized into a FeatureCollection` 160 | ); 161 | } 162 | 163 | // don't set data unless it has actually changed 164 | if (!_.isEqual(data, context.data.get('map'))) { 165 | context.data.set({ map: data }, updateSource); 166 | if (zoom) zoomextent(context); 167 | } 168 | } 169 | } 170 | 171 | context.dispatch.on('change.json', (event) => { 172 | if (event.source !== 'json') { 173 | const scrollInfo = editor.getScrollInfo(); 174 | editor.setValue(JSON.stringify(context.data.get('map'), null, 2)); 175 | editor.scrollTo(scrollInfo.left, scrollInfo.top); 176 | } 177 | }); 178 | 179 | editor.setValue(JSON.stringify(context.data.get('map'), null, 2)); 180 | } 181 | 182 | render.off = function () { 183 | context.dispatch.on('change.json', null); 184 | }; 185 | 186 | return render; 187 | }; 188 | -------------------------------------------------------------------------------- /src/lib/popup.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context) { 2 | return function (e, id) { 3 | const sel = d3.select(e.target._content); 4 | 5 | sel.selectAll('.cancel').on('click', clickClose); 6 | 7 | sel.selectAll('form').on('submit', saveFeature); 8 | 9 | sel.selectAll('.add').on('click', addRow); 10 | 11 | sel 12 | .selectAll('.add-simplestyle-properties-button') 13 | .on('click', addSimplestyleProperties); 14 | 15 | sel.selectAll('.delete-invert').on('click', removeFeature); 16 | 17 | function clickClose() { 18 | e.target._onClose(); 19 | } 20 | 21 | function removeFeature() { 22 | const data = context.data.get('map'); 23 | data.features.splice(id, 1); 24 | 25 | context.data.set({ map: data }, 'popup'); 26 | 27 | // hide the popup 28 | e.target._onClose(); 29 | } 30 | 31 | function losslessNumber(x) { 32 | const fl = parseFloat(x); 33 | if (fl.toString() === x) return fl; 34 | else return x; 35 | } 36 | 37 | function saveFeature() { 38 | const obj = {}; 39 | const table = sel.select('table.marker-properties'); 40 | table.selectAll('tr').each(collectRow); 41 | function collectRow() { 42 | if (d3.select(this).selectAll('input')[0][0].value) { 43 | obj[d3.select(this).selectAll('input')[0][0].value] = losslessNumber( 44 | d3.select(this).selectAll('input')[0][1].value 45 | ); 46 | } 47 | } 48 | 49 | const data = context.data.get('map'); 50 | const feature = data.features[id]; 51 | feature.properties = obj; 52 | context.data.set({ map: data }, 'popup'); 53 | // hide the popup 54 | e.target._onClose(); 55 | } 56 | 57 | function addRow() { 58 | const tr = sel.select('table.marker-properties tbody').append('tr'); 59 | 60 | tr.append('th').append('input').attr('type', 'text'); 61 | 62 | tr.append('td').append('input').attr('type', 'text'); 63 | } 64 | 65 | function addSimplestyleProperties() { 66 | // hide the button 67 | sel 68 | .selectAll('.add-simplestyle-properties-button') 69 | .style('display', 'none'); 70 | 71 | const data = context.data.get('map'); 72 | const feature = data.features[id]; 73 | const { properties, geometry } = feature; 74 | 75 | if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { 76 | if (!('marker-color' in properties)) { 77 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 78 | tr.append('th') 79 | .append('input') 80 | .attr('type', 'text') 81 | .attr('value', 'marker-color'); 82 | tr.append('td') 83 | .append('input') 84 | .attr('type', 'color') 85 | .attr('value', '#7E7E7E'); 86 | } 87 | 88 | if (!('marker-size' in properties)) { 89 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 90 | tr.append('th') 91 | .append('input') 92 | .attr('type', 'text') 93 | .attr('value', 'marker-size'); 94 | const td = tr.append('td'); 95 | td.append('input') 96 | .attr('type', 'text') 97 | .attr('value', 'medium') 98 | .attr('list', 'marker-size'); 99 | const datalist = td.append('datalist').attr('id', 'marker-size'); 100 | datalist.append('option').attr('value', 'small'); 101 | datalist.append('option').attr('value', 'medium'); 102 | datalist.append('option').attr('value', 'large'); 103 | } 104 | } 105 | if ( 106 | geometry.type === 'LineString' || 107 | geometry.type === 'MultiLineString' || 108 | geometry.type === 'Polygon' || 109 | geometry.type === 'MultiPolygon' 110 | ) { 111 | if (!('stroke' in properties)) { 112 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 113 | tr.append('th') 114 | .append('input') 115 | .attr('type', 'text') 116 | .attr('value', 'stroke'); 117 | tr.append('td') 118 | .append('input') 119 | .attr('type', 'color') 120 | .attr('value', '#555555'); 121 | } 122 | if (!('stroke-width' in properties)) { 123 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 124 | tr.append('th') 125 | .append('input') 126 | .attr('type', 'text') 127 | .attr('value', 'stroke-width'); 128 | tr.append('td') 129 | .append('input') 130 | .attr('type', 'number') 131 | .attr('min', '0') 132 | .attr('step', '0.1') 133 | .attr('value', '2'); 134 | } 135 | if (!('stroke-opacity' in properties)) { 136 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 137 | tr.append('th') 138 | .append('input') 139 | .attr('type', 'text') 140 | .attr('value', 'stroke-opacity'); 141 | tr.append('td') 142 | .append('input') 143 | .attr('type', 'number') 144 | .attr('min', '0') 145 | .attr('max', '1') 146 | .attr('step', '0.1') 147 | .attr('value', '1'); 148 | } 149 | } 150 | if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') { 151 | if (!('fill' in properties)) { 152 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 153 | tr.append('th') 154 | .append('input') 155 | .attr('type', 'text') 156 | .attr('value', 'fill'); 157 | tr.append('td') 158 | .append('input') 159 | .attr('type', 'color') 160 | .attr('value', '#555555'); 161 | } 162 | if (!('fill-opacity' in properties)) { 163 | const tr = sel.select('table.marker-properties tbody').insert('tr'); 164 | tr.append('th') 165 | .append('input') 166 | .attr('type', 'text') 167 | .attr('value', 'fill-opacity'); 168 | tr.append('td') 169 | .append('input') 170 | .attr('type', 'number') 171 | .attr('min', '0') 172 | .attr('max', '1') 173 | .attr('step', '0.1') 174 | .attr('value', '0.5'); 175 | } 176 | } 177 | } 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /lib/d3.keybinding.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is licensed under the MIT license. 3 | * 4 | * Copyright © 2013, iD authors. 5 | * 6 | * Portions copyright © 2011, Keith Cirkel 7 | * See https://github.com/keithamus/jwerty 8 | * 9 | */ 10 | d3.keybinding = function(namespace) { 11 | var bindings = []; 12 | 13 | function matches(binding, event) { 14 | for (var p in binding.event) { 15 | if (event[p] != binding.event[p]) 16 | return false; 17 | } 18 | 19 | return (!binding.capture) === (event.eventPhase !== Event.CAPTURING_PHASE); 20 | } 21 | 22 | function capture() { 23 | for (var i = 0; i < bindings.length; i++) { 24 | var binding = bindings[i]; 25 | if (matches(binding, d3.event)) { 26 | binding.callback(); 27 | } 28 | } 29 | } 30 | 31 | function bubble() { 32 | var tagName = d3.select(d3.event.target).node().tagName; 33 | if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') { 34 | return; 35 | } 36 | capture(); 37 | } 38 | 39 | function keybinding(selection) { 40 | selection = selection || d3.select(document); 41 | selection.on('keydown.capture' + namespace, capture, true); 42 | selection.on('keydown.bubble' + namespace, bubble, false); 43 | return keybinding; 44 | } 45 | 46 | keybinding.off = function(selection) { 47 | selection = selection || d3.select(document); 48 | selection.on('keydown.capture' + namespace, null); 49 | selection.on('keydown.bubble' + namespace, null); 50 | return keybinding; 51 | }; 52 | 53 | keybinding.on = function(code, callback, capture) { 54 | var binding = { 55 | event: { 56 | keyCode: 0, 57 | shiftKey: false, 58 | ctrlKey: false, 59 | altKey: false, 60 | metaKey: false 61 | }, 62 | capture: capture, 63 | callback: callback 64 | }; 65 | 66 | code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); 67 | 68 | for (var i = 0; i < code.length; i++) { 69 | // Normalise matching errors 70 | if (code[i] === '++') code[i] = '+'; 71 | 72 | if (code[i] in d3.keybinding.modifierCodes) { 73 | binding.event[d3.keybinding.modifierProperties[d3.keybinding.modifierCodes[code[i]]]] = true; 74 | } else if (code[i] in d3.keybinding.keyCodes) { 75 | binding.event.keyCode = d3.keybinding.keyCodes[code[i]]; 76 | } 77 | } 78 | 79 | bindings.push(binding); 80 | 81 | return keybinding; 82 | }; 83 | 84 | return keybinding; 85 | }; 86 | 87 | (function () { 88 | d3.keybinding.modifierCodes = { 89 | // Shift key, ⇧ 90 | '⇧': 16, shift: 16, 91 | // CTRL key, on Mac: ⌃ 92 | '⌃': 17, ctrl: 17, 93 | // ALT key, on Mac: ⌥ (Alt) 94 | '⌥': 18, alt: 18, option: 18, 95 | // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super) 96 | '⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91 97 | }; 98 | 99 | d3.keybinding.modifierProperties = { 100 | 16: 'shiftKey', 101 | 17: 'ctrlKey', 102 | 18: 'altKey', 103 | 91: 'metaKey' 104 | }; 105 | 106 | d3.keybinding.keyCodes = { 107 | // Backspace key, on Mac: ⌫ (Backspace) 108 | '⌫': 8, backspace: 8, 109 | // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥ 110 | '⇥': 9, '⇆': 9, tab: 9, 111 | // Return key, ↩ 112 | '↩': 13, 'return': 13, enter: 13, '⌅': 13, 113 | // Pause/Break key 114 | 'pause': 19, 'pause-break': 19, 115 | // Caps Lock key, ⇪ 116 | '⇪': 20, caps: 20, 'caps-lock': 20, 117 | // Escape key, on Mac: ⎋, on Windows: Esc 118 | '⎋': 27, escape: 27, esc: 27, 119 | // Space key 120 | space: 32, 121 | // Page-Up key, or pgup, on Mac: ↖ 122 | '↖': 33, pgup: 33, 'page-up': 33, 123 | // Page-Down key, or pgdown, on Mac: ↘ 124 | '↘': 34, pgdown: 34, 'page-down': 34, 125 | // END key, on Mac: ⇟ 126 | '⇟': 35, end: 35, 127 | // HOME key, on Mac: ⇞ 128 | '⇞': 36, home: 36, 129 | // Insert key, or ins 130 | ins: 45, insert: 45, 131 | // Delete key, on Mac: ⌦ (Delete) 132 | '⌦': 46, del: 46, 'delete': 46, 133 | // Left Arrow Key, or ← 134 | '←': 37, left: 37, 'arrow-left': 37, 135 | // Up Arrow Key, or ↑ 136 | '↑': 38, up: 38, 'arrow-up': 38, 137 | // Right Arrow Key, or → 138 | '→': 39, right: 39, 'arrow-right': 39, 139 | // Up Arrow Key, or ↓ 140 | '↓': 40, down: 40, 'arrow-down': 40, 141 | // odities, printing characters that come out wrong: 142 | // Num-Multiply, or * 143 | '*': 106, star: 106, asterisk: 106, multiply: 106, 144 | // Num-Plus or + 145 | '+': 107, 'plus': 107, 146 | // Num-Subtract, or - 147 | '-': 109, subtract: 109, 148 | // Semicolon 149 | ';': 186, semicolon:186, 150 | // = or equals 151 | '=': 187, 'equals': 187, 152 | // Comma, or , 153 | ',': 188, comma: 188, 154 | 'dash': 189, //??? 155 | // Period, or ., or full-stop 156 | '.': 190, period: 190, 'full-stop': 190, 157 | // Slash, or /, or forward-slash 158 | '/': 191, slash: 191, 'forward-slash': 191, 159 | // Tick, or `, or back-quote 160 | '`': 192, tick: 192, 'back-quote': 192, 161 | // Open bracket, or [ 162 | '[': 219, 'open-bracket': 219, 163 | // Back slash, or \ 164 | '\\': 220, 'back-slash': 220, 165 | // Close backet, or ] 166 | ']': 221, 'close-bracket': 221, 167 | // Apostrophe, or Quote, or ' 168 | '\'': 222, quote: 222, apostrophe: 222 169 | }; 170 | 171 | // NUMPAD 0-9 172 | var i = 95, n = 0; 173 | while (++i < 106) { 174 | d3.keybinding.keyCodes['num-' + n] = i; 175 | ++n; 176 | } 177 | 178 | // 0-9 179 | i = 47; n = 0; 180 | while (++i < 58) { 181 | d3.keybinding.keyCodes[n] = i; 182 | ++n; 183 | } 184 | 185 | // F1-F25 186 | i = 111; n = 1; 187 | while (++i < 136) { 188 | d3.keybinding.keyCodes['f' + n] = i; 189 | ++n; 190 | } 191 | 192 | // a-z 193 | i = 64; 194 | while (++i < 91) { 195 | d3.keybinding.keyCodes[String.fromCharCode(i).toLowerCase()] = i; 196 | } 197 | })(); 198 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2022-11-16 4 | * show geojsonhint warnings without rejecting geojson #786 5 | 6 | ## 2022-11-15 7 | 8 | * handle null properties when rendering MultiPoint #783 9 | * Mobile Responsive Layout and CSS updates (#779) 10 | 11 | ## 2022-11-10 12 | 13 | * add image and twitter card meta tags, update title (#774) 14 | 15 | ## 2022-11-09 16 | 17 | * restore simplestyle ui in popups, handle marker-size style property (#766) 18 | * Update linting and formatting (#764) 19 | 20 | ## 2022-11-07 21 | 22 | * install mapboxgl, mapbox-gl-draw, mapbox-gl-geocoder, and turf from node_modules (#761) 23 | * require codemirror from json.js (#762) 24 | 25 | ## 2022-11-03 26 | 27 | * add keybindings for draw mode changes and trash (#755) 28 | 29 | ## 2022-11-02 30 | 31 | * add circle drawing mode, add title tooltips for draw buttons (#748) 32 | 33 | ## 2022-10-31 34 | 35 | * remove cmd-s keybinding (#750) 36 | * upgrade fontawesome, add copy button to json editor (#753) 37 | * Persist projection and style (#752) 38 | * fix style load check (#751) 39 | 40 | ## 2022-10-27 41 | 42 | * prevent editor data update when data is unchanged (#734) 43 | * move codemirror to package.json dependencies (#731) 44 | 45 | ## 2022-10-25 46 | 47 | * Fix a bug that was checking for map load in an unreliable way before adding data from externally loaded geojson (#733) 48 | 49 | ## 2022-10-24 50 | * prevent line or polygon popup if marker was clicked (#730) 51 | * Fix the handling of points in MultiPoint and GeometryCollection geometries (#736) 52 | 53 | ## 2022-10-21 54 | 55 | * Update the default map view, `fix mapDefault() check` (#727) 56 | * Improve hot-reloading setup (#717) 57 | * Restore user-added raster layer (#715) 58 | * Add a better error message when WebGL is not enabled (#723) 59 | * Fix a bug where marker popups were read-only (#726) 60 | 61 | ## 2022-10-20 62 | 63 | * Restore most simplestyle properties (#718) 64 | * Restore OSM raster tiles as a default basemap option (#720) 65 | 66 | ## 2022-10-17 67 | 68 | * Add Codemirror Folding (#704) 69 | 70 | * The following updates were included in (#703) 71 | * Remove authentication UI and references to auth/saving. 72 | * Remove the browser CLI and references to it in the help text and readme. 73 | * Simplestyle styling is no longer supported, and default population of simplestyle properties is removed. All features in the working dataset will now have the same style 74 | * Upgrade browserify and other dependencies, allowing for a working build with newer versions of node.js (v14). 75 | * Add some vs code settings to automate running make on save, for a more streamlined development process. 76 | * Uses mapbox-gj.js for map rendering, including the new globe projection. 77 | * Adds mapbox-gl-draw with a rectangle mode and a custom edit UI/UX to be similar to the edit button in leaflet-draw 78 | * Adds a projection toggle UI (visible at zoom 6 and below) for switching between globe (default) and mercator 79 | * Replaces the existing layer switch with more mapbox core style options (Streets, Satellite Streets, Outdoors, Light, Dark) 80 | * Formats geojson on paste (useful when pasting single-line geojson from a scripted output somewhere, if the pasted string is valid JSON it will be automatically pretty-printed in the editor) 81 | 82 | ## 2022-10-05 83 | 84 | * Add a top navbar with link to Mapbox signup page 85 | 86 | ## 2018-03-01 87 | 88 | * OpenCycleMap requires an API key and the layer is disabled by default (#577, #604). 89 | 90 | ## 2014-10-13 91 | 92 | * Updated `shp-write`: shapefile writing should work again. 93 | 94 | ## 2014-09-10 95 | 96 | * Updated `geojsonhint`: Feature `id` properties are now properly validated. 97 | 98 | ## 2014-09-09 99 | 100 | * Added Flatten task to Meta menu with `geojson-flatten` 101 | 102 | ## 2014-08-07 103 | 104 | * Bugfixes 105 | * Github commit message now correct 106 | * Subsequent commits fixed 107 | 108 | ## 2014-08-01 109 | 110 | * Added Meta menu with Clear and Random Points tasks 111 | * Added background to top bar 112 | * Removed Dockerfile, Vagrantfile, and other gunk. 113 | 114 | ## 2014-07-30 115 | 116 | * Support loading large Gist files. 117 | 118 | ## 2014-05-23 119 | 120 | * Fix non-anonymous gist editing. 121 | 122 | ## 2014-05-16 123 | 124 | * Moves input and output UIs to modals so that the right sidebar isn't overloaded 125 | * Visual cleanup: fewer icons, more map 126 | * Removes geocoding UI 127 | 128 | ## 2013-12-10 129 | 130 | * Support for downloading points as DSV 131 | 132 | ## 2013-11-25 133 | 134 | * Added ability to rename GeoJSON properties in the table view 135 | 136 | ## 2013-11-18 137 | 138 | * Fix coordinate order in popups 139 | 140 | ## 2013-10-11 141 | 142 | ![KML Import](http://i.imgur.com/f0L156A.gif) 143 | 144 | * [KML](https://developers.google.com/kml/documentation/) export support 145 | with [tokml](https://github.com/mapbox/tokml) 146 | 147 | ## 2013-10-07 148 | 149 | * Fix iframe embeds 150 | * Popups now show (latitude, longitude) for points or total length for lines. 151 | 152 | ## 2013-10-04 153 | 154 | ![URL Loading](http://i.imgur.com/nfvLdjd.gif) 155 | 156 | * Support for `data=text/x-url,` argument to load GeoJSON files on the web via [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 157 | * [URL API Documentation](https://github.com/mapbox/geojson.io/blob/gh-pages/API.md) 158 | * Console API Documentation & Console API 159 | 160 | ## 2013-10-01 161 | 162 | * Shapefile export (beta) 163 | 164 | ![Shapefile Export](http://i.imgur.com/5zpt1d3.gif) 165 | 166 | ## 2013-9-30 167 | 168 | * Use MapBox markers to expose style options 169 | * Fix bug with retina monitors and github file previews 170 | * Hit `Cmd+O` to quickly import a new file 171 | 172 | ## 2013-09-25 173 | 174 | * Numbers in GeoJSON properties are coerced to be numeric when appropriate 175 | 176 | ## 2013-09-24 177 | 178 | * Drag and drop of multiple files is supported - files in folders are unfortunately 179 | not supported due to core JS/DOM limitations. 180 | * Help panel, first resurgence of in-UI documentation and first listing of 181 | keyboard shortcuts 182 | * Easter-egg status TopoJSON output 183 | * Zoom to copy-pasted new features 184 | 185 | ## 2013-09-11 186 | 187 | * Fixed mobile/read-only mode 188 | 189 | ## 2013-09-09 190 | 191 | ![](http://i.imgur.com/QgPQkVT.gif) 192 | 193 | * First release of [geojsonio-cli](github.com/mapbox/geojsonio-cli) and matching 194 | functionality on the web side 195 | * [OpenStreetMap](http://www.openstreetmap.org/) format support - import 196 | [OSM XML](http://wiki.openstreetmap.org/wiki/OSM_XML) files onto the map with 197 | [osm-and-geojson](https://npmjs.org/package/osm-and-geojson). 198 | -------------------------------------------------------------------------------- /src/ui/draw/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | id: 'gl-draw-polygon-fill-inactive', 4 | type: 'fill', 5 | filter: [ 6 | 'all', 7 | ['==', 'active', 'false'], 8 | ['==', '$type', 'Polygon'], 9 | ['!=', 'mode', 'static'] 10 | ], 11 | paint: { 12 | 'fill-color': '#3bb2d0', 13 | 'fill-outline-color': '#3bb2d0', 14 | 'fill-opacity': 0.1 15 | } 16 | }, 17 | { 18 | id: 'gl-draw-polygon-fill-active', 19 | type: 'fill', 20 | filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']], 21 | paint: { 22 | 'fill-color': '#fbb03b', 23 | 'fill-outline-color': '#fbb03b', 24 | 'fill-opacity': 0.1 25 | } 26 | }, 27 | { 28 | id: 'gl-draw-polygon-midpoint', 29 | type: 'circle', 30 | filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']], 31 | paint: { 32 | 'circle-radius': 3, 33 | 'circle-color': '#fbb03b' 34 | } 35 | }, 36 | { 37 | id: 'gl-draw-polygon-stroke-inactive', 38 | type: 'line', 39 | filter: [ 40 | 'all', 41 | ['==', 'active', 'false'], 42 | ['==', '$type', 'Polygon'], 43 | ['!=', 'mode', 'static'] 44 | ], 45 | layout: { 46 | 'line-cap': 'round', 47 | 'line-join': 'round' 48 | }, 49 | paint: { 50 | 'line-color': '#3bb2d0', 51 | 'line-width': 2 52 | } 53 | }, 54 | { 55 | id: 'gl-draw-polygon-stroke-active', 56 | type: 'line', 57 | filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']], 58 | layout: { 59 | 'line-cap': 'round', 60 | 'line-join': 'round' 61 | }, 62 | paint: { 63 | 'line-color': '#fbb03b', 64 | 'line-dasharray': [0.2, 2], 65 | 'line-width': 2 66 | } 67 | }, 68 | { 69 | id: 'gl-draw-line-inactive', 70 | type: 'line', 71 | filter: [ 72 | 'all', 73 | ['==', 'active', 'false'], 74 | ['==', '$type', 'LineString'], 75 | ['!=', 'mode', 'static'] 76 | ], 77 | layout: { 78 | 'line-cap': 'round', 79 | 'line-join': 'round' 80 | }, 81 | paint: { 82 | 'line-color': '#3bb2d0', 83 | 'line-width': 2 84 | } 85 | }, 86 | { 87 | id: 'gl-draw-line-active', 88 | type: 'line', 89 | filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']], 90 | layout: { 91 | 'line-cap': 'round', 92 | 'line-join': 'round' 93 | }, 94 | paint: { 95 | 'line-color': '#fbb03b', 96 | 'line-dasharray': [0.2, 2], 97 | 'line-width': 2 98 | } 99 | }, 100 | { 101 | id: 'gl-draw-polygon-and-line-vertex-stroke-inactive', 102 | type: 'circle', 103 | filter: [ 104 | 'all', 105 | ['==', 'meta', 'vertex'], 106 | ['==', '$type', 'Point'], 107 | ['!=', 'mode', 'static'] 108 | ], 109 | paint: { 110 | 'circle-radius': 5, 111 | 'circle-color': '#fff' 112 | } 113 | }, 114 | { 115 | id: 'gl-draw-polygon-and-line-vertex-inactive', 116 | type: 'circle', 117 | filter: [ 118 | 'all', 119 | ['==', 'meta', 'vertex'], 120 | ['==', '$type', 'Point'], 121 | ['!=', 'mode', 'static'] 122 | ], 123 | paint: { 124 | 'circle-radius': 3, 125 | 'circle-color': '#fbb03b' 126 | } 127 | }, 128 | { 129 | id: 'gl-draw-point-point-stroke-inactive', 130 | type: 'circle', 131 | filter: [ 132 | 'all', 133 | ['==', 'active', 'false'], 134 | ['==', '$type', 'Point'], 135 | ['==', 'meta', 'feature'], 136 | ['!=', 'mode', 'static'] 137 | ], 138 | paint: { 139 | 'circle-radius': 5, 140 | 'circle-opacity': 1, 141 | 'circle-color': '#fff' 142 | } 143 | }, 144 | { 145 | id: 'gl-draw-point-inactive', 146 | type: 'circle', 147 | filter: [ 148 | 'all', 149 | ['==', 'active', 'false'], 150 | ['==', '$type', 'Point'], 151 | ['==', 'meta', 'feature'], 152 | ['!=', 'mode', 'static'] 153 | ], 154 | paint: { 155 | 'circle-radius': 3, 156 | 'circle-color': '#3bb2d0' 157 | } 158 | }, 159 | { 160 | id: 'gl-draw-point-stroke-active', 161 | type: 'circle', 162 | filter: [ 163 | 'all', 164 | ['==', '$type', 'Point'], 165 | ['==', 'active', 'true'], 166 | ['!=', 'meta', 'midpoint'] 167 | ], 168 | paint: { 169 | 'circle-radius': 7, 170 | 'circle-color': '#fff' 171 | } 172 | }, 173 | { 174 | id: 'gl-draw-point-active', 175 | type: 'circle', 176 | filter: [ 177 | 'all', 178 | ['==', '$type', 'Point'], 179 | ['!=', 'meta', 'midpoint'], 180 | ['==', 'active', 'true'] 181 | ], 182 | paint: { 183 | 'circle-radius': 5, 184 | 'circle-color': '#fbb03b' 185 | } 186 | }, 187 | { 188 | id: 'gl-draw-polygon-fill-static', 189 | type: 'fill', 190 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 191 | paint: { 192 | 'fill-color': '#404040', 193 | 'fill-outline-color': '#404040', 194 | 'fill-opacity': 0.1 195 | } 196 | }, 197 | { 198 | id: 'gl-draw-polygon-stroke-static', 199 | type: 'line', 200 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 201 | layout: { 202 | 'line-cap': 'round', 203 | 'line-join': 'round' 204 | }, 205 | paint: { 206 | 'line-color': '#404040', 207 | 'line-width': 2 208 | } 209 | }, 210 | { 211 | id: 'gl-draw-line-static', 212 | type: 'line', 213 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']], 214 | layout: { 215 | 'line-cap': 'round', 216 | 'line-join': 'round' 217 | }, 218 | paint: { 219 | 'line-color': '#404040', 220 | 'line-width': 2 221 | } 222 | }, 223 | { 224 | id: 'gl-draw-point-static', 225 | type: 'circle', 226 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Point']], 227 | paint: { 228 | 'circle-radius': 5, 229 | 'circle-color': '#404040' 230 | } 231 | }, 232 | { 233 | id: 'gl-draw-symbol', 234 | type: 'symbol', 235 | layout: { 236 | 'text-line-height': 1.1, 237 | 'text-size': 15, 238 | 'text-font': ['DIN Pro Medium', 'Arial Unicode MS Regular'], 239 | 'text-anchor': 'left', 240 | 'text-justify': 'left', 241 | 'text-offset': [0.8, 0.8], 242 | 'text-field': ['get', 'radius'], 243 | 'text-max-width': 7 244 | }, 245 | paint: { 246 | 'text-color': 'hsl(0, 0%, 95%)', 247 | 'text-halo-color': 'hsl(0, 5%, 0%)', 248 | 'text-halo-width': 1, 249 | 'text-halo-blur': 1 250 | }, 251 | filter: ['==', 'meta', 'currentPosition'] 252 | } 253 | ]; 254 | -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: base64.js,v 2.12 2013/05/06 07:54:20 dankogai Exp dankogai $ 3 | * 4 | * Licensed under the MIT license. 5 | * http://opensource.org/licenses/mit-license 6 | * 7 | * References: 8 | * http://en.wikipedia.org/wiki/Base64 9 | */ 10 | 11 | (function(global) { 12 | 'use strict'; 13 | if (global.Base64) return; 14 | var version = "2.1.2"; 15 | // if node.js, we use Buffer 16 | var buffer; 17 | if (typeof module !== 'undefined' && module.exports) { 18 | buffer = require('buffer').Buffer; 19 | } 20 | // constants 21 | var b64chars 22 | = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 23 | var b64tab = function(bin) { 24 | var t = {}; 25 | for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i; 26 | return t; 27 | }(b64chars); 28 | var fromCharCode = String.fromCharCode; 29 | // encoder stuff 30 | var cb_utob = function(c) { 31 | if (c.length < 2) { 32 | var cc = c.charCodeAt(0); 33 | return cc < 0x80 ? c 34 | : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6)) 35 | + fromCharCode(0x80 | (cc & 0x3f))) 36 | : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) 37 | + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) 38 | + fromCharCode(0x80 | ( cc & 0x3f))); 39 | } else { 40 | var cc = 0x10000 41 | + (c.charCodeAt(0) - 0xD800) * 0x400 42 | + (c.charCodeAt(1) - 0xDC00); 43 | return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) 44 | + fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) 45 | + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) 46 | + fromCharCode(0x80 | ( cc & 0x3f))); 47 | } 48 | }; 49 | var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; 50 | var utob = function(u) { 51 | return u.replace(re_utob, cb_utob); 52 | }; 53 | var cb_encode = function(ccc) { 54 | var padlen = [0, 2, 1][ccc.length % 3], 55 | ord = ccc.charCodeAt(0) << 16 56 | | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) 57 | | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)), 58 | chars = [ 59 | b64chars.charAt( ord >>> 18), 60 | b64chars.charAt((ord >>> 12) & 63), 61 | padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), 62 | padlen >= 1 ? '=' : b64chars.charAt(ord & 63) 63 | ]; 64 | return chars.join(''); 65 | }; 66 | var btoa = global.btoa || function(b) { 67 | return b.replace(/[\s\S]{1,3}/g, cb_encode); 68 | }; 69 | var _encode = buffer 70 | ? function (u) { return (new buffer(u)).toString('base64') } 71 | : function (u) { return btoa(utob(u)) } 72 | ; 73 | var encode = function(u, urisafe) { 74 | return !urisafe 75 | ? _encode(u) 76 | : _encode(u).replace(/[+\/]/g, function(m0) { 77 | return m0 == '+' ? '-' : '_'; 78 | }).replace(/=/g, ''); 79 | }; 80 | var encodeURI = function(u) { return encode(u, true) }; 81 | // decoder stuff 82 | var re_btou = new RegExp([ 83 | '[\xC0-\xDF][\x80-\xBF]', 84 | '[\xE0-\xEF][\x80-\xBF]{2}', 85 | '[\xF0-\xF7][\x80-\xBF]{3}' 86 | ].join('|'), 'g'); 87 | var cb_btou = function(cccc) { 88 | switch(cccc.length) { 89 | case 4: 90 | var cp = ((0x07 & cccc.charCodeAt(0)) << 18) 91 | | ((0x3f & cccc.charCodeAt(1)) << 12) 92 | | ((0x3f & cccc.charCodeAt(2)) << 6) 93 | | (0x3f & cccc.charCodeAt(3)), 94 | offset = cp - 0x10000; 95 | return (fromCharCode((offset >>> 10) + 0xD800) 96 | + fromCharCode((offset & 0x3FF) + 0xDC00)); 97 | case 3: 98 | return fromCharCode( 99 | ((0x0f & cccc.charCodeAt(0)) << 12) 100 | | ((0x3f & cccc.charCodeAt(1)) << 6) 101 | | (0x3f & cccc.charCodeAt(2)) 102 | ); 103 | default: 104 | return fromCharCode( 105 | ((0x1f & cccc.charCodeAt(0)) << 6) 106 | | (0x3f & cccc.charCodeAt(1)) 107 | ); 108 | } 109 | }; 110 | var btou = function(b) { 111 | return b.replace(re_btou, cb_btou); 112 | }; 113 | var cb_decode = function(cccc) { 114 | var len = cccc.length, 115 | padlen = len % 4, 116 | n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) 117 | | (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) 118 | | (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) 119 | | (len > 3 ? b64tab[cccc.charAt(3)] : 0), 120 | chars = [ 121 | fromCharCode( n >>> 16), 122 | fromCharCode((n >>> 8) & 0xff), 123 | fromCharCode( n & 0xff) 124 | ]; 125 | chars.length -= [0, 0, 2, 1][padlen]; 126 | return chars.join(''); 127 | }; 128 | var atob = global.atob || function(a){ 129 | return a.replace(/[\s\S]{1,4}/g, cb_decode); 130 | }; 131 | var _decode = buffer 132 | ? function(a) { return (new buffer(a, 'base64')).toString() } 133 | : function(a) { return btou(atob(a)) }; 134 | var decode = function(a){ 135 | return _decode( 136 | a.replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' }) 137 | .replace(/[^A-Za-z0-9\+\/]/g, '') 138 | ); 139 | }; 140 | // export Base64 141 | global.Base64 = { 142 | VERSION: version, 143 | atob: atob, 144 | btoa: btoa, 145 | fromBase64: decode, 146 | toBase64: encode, 147 | utob: utob, 148 | encode: encode, 149 | encodeURI: encodeURI, 150 | btou: btou, 151 | decode: decode 152 | }; 153 | // if ES5 is available, make Base64.extendString() available 154 | if (typeof Object.defineProperty === 'function') { 155 | var noEnum = function(v){ 156 | return {value:v,enumerable:false,writable:true,configurable:true}; 157 | }; 158 | global.Base64.extendString = function () { 159 | Object.defineProperty( 160 | String.prototype, 'fromBase64', noEnum(function () { 161 | return decode(this) 162 | })); 163 | Object.defineProperty( 164 | String.prototype, 'toBase64', noEnum(function (urisafe) { 165 | return encode(this, urisafe) 166 | })); 167 | Object.defineProperty( 168 | String.prototype, 'toBase64URI', noEnum(function () { 169 | return encode(this, true) 170 | })); 171 | }; 172 | } 173 | // that's it! 174 | })(this); 175 | -------------------------------------------------------------------------------- /src/lib/readfile.js: -------------------------------------------------------------------------------- 1 | const topojson = require('topojson-client'), 2 | toGeoJSON = require('@tmcw/togeojson'), 3 | gtfs2geojson = require('gtfs2geojson'), 4 | csv2geojson = require('csv2geojson'), 5 | osmtogeojson = require('osmtogeojson'), 6 | polytogeojson = require('polytogeojson'), 7 | geojsonNormalize = require('@mapbox/geojson-normalize'); 8 | 9 | 10 | module.exports.readDrop = readDrop; 11 | module.exports.readAsText = readAsText; 12 | module.exports.readFile = readFile; 13 | 14 | function readDrop(callback) { 15 | return function () { 16 | const results = []; 17 | const errors = []; 18 | if ( 19 | d3.event.dataTransfer && 20 | d3.event.dataTransfer && 21 | d3.event.dataTransfer.files && 22 | d3.event.dataTransfer.files.length 23 | ) { 24 | d3.event.stopPropagation(); 25 | d3.event.preventDefault(); 26 | let remaining = d3.event.dataTransfer.files.length; 27 | [].forEach.call(d3.event.dataTransfer.files, (f) => { 28 | readAsText(f, (err, text) => { 29 | if (err) { 30 | errors.push(err); 31 | if (!--remaining) finish(errors, results); 32 | } else { 33 | readFile(f, text, (err, res, war) => { 34 | if (err) errors.push(err); 35 | if (res) results.push(res); 36 | if (war) results.push(war); 37 | if (!--remaining) finish(errors, results, war); 38 | }); 39 | } 40 | }); 41 | }); 42 | } else { 43 | return callback({ 44 | message: 'No files were dropped' 45 | }); 46 | } 47 | 48 | function finish(errors, results, war) { 49 | // if no conversions suceeded, return the first error 50 | if (!results.length && errors.length) 51 | return callback(errors[0], null, war); 52 | // otherwise combine results 53 | return callback( 54 | null, 55 | { 56 | type: 'FeatureCollection', 57 | features: results.reduce((memo, r) => { 58 | r = geojsonNormalize(r); 59 | if (r) { 60 | memo = memo.concat(r.features); 61 | } 62 | return memo; 63 | }, []) 64 | }, 65 | war 66 | ); 67 | } 68 | }; 69 | } 70 | 71 | function readAsText(f, callback) { 72 | try { 73 | const reader = new FileReader(); 74 | reader.readAsText(f); 75 | reader.onload = function (e) { 76 | if (e.target && e.target.result) callback(null, e.target.result); 77 | else 78 | callback({ 79 | message: 'Dropped file could not be loaded' 80 | }); 81 | }; 82 | reader.onerror = function () { 83 | callback({ 84 | message: 'Dropped file was unreadable' 85 | }); 86 | }; 87 | } catch (e) { 88 | callback({ 89 | message: 'Dropped file was unreadable' 90 | }); 91 | } 92 | } 93 | 94 | function readFile(f, text, callback) { 95 | const fileType = detectType(f, text); 96 | 97 | if (!fileType) { 98 | return callback({ 99 | message: 'Could not detect file type' 100 | }); 101 | } else if (fileType === 'kml') { 102 | const kmldom = toDom(text); 103 | if (!kmldom) { 104 | return callback({ 105 | message: 'Invalid KML file: not valid XML' 106 | }); 107 | } 108 | let warning; 109 | if (kmldom.getElementsByTagName('NetworkLink').length) { 110 | warning = { 111 | message: 112 | 'The KML file you uploaded included NetworkLinks: some content may not display. ' + 113 | 'Please export and upload KML without NetworkLinks for optimal performance' 114 | }; 115 | } 116 | callback(null, toGeoJSON.kml(kmldom), warning); 117 | } else if (fileType === 'xml') { 118 | const xmldom = toDom(text); 119 | if (!xmldom) { 120 | return callback({ 121 | message: 'Invalid XML file: not valid XML' 122 | }); 123 | } 124 | const result = osmtogeojson.toGeojson(xmldom); 125 | // only keep object tags as properties 126 | result.features.forEach((feature) => { 127 | feature.properties = feature.properties.tags; 128 | }); 129 | callback(null, result); 130 | } else if (fileType === 'gpx') { 131 | callback(null, toGeoJSON.gpx(toDom(text))); 132 | } else if (fileType === 'geojson') { 133 | try { 134 | const gj = JSON.parse(text); 135 | if (gj && gj.type === 'Topology' && gj.objects) { 136 | const collection = { type: 'FeatureCollection', features: [] }; 137 | for (const o in gj.objects) { 138 | const ft = topojson.feature(gj, gj.objects[o]); 139 | if (ft.features) 140 | collection.features = collection.features.concat(ft.features); 141 | else collection.features = collection.features.concat([ft]); 142 | } 143 | return callback(null, collection); 144 | } else { 145 | return callback(null, gj); 146 | } 147 | } catch (err) { 148 | alert('Invalid JSON file: ' + err); 149 | return; 150 | } 151 | } else if (fileType === 'dsv') { 152 | csv2geojson.csv2geojson( 153 | text, 154 | { 155 | delimiter: 'auto' 156 | }, 157 | (err, result) => { 158 | if (err) { 159 | return callback({ 160 | type: 'geocode', 161 | result: result, 162 | raw: text 163 | }); 164 | } else { 165 | return callback(null, result); 166 | } 167 | } 168 | ); 169 | } else if (fileType === 'gtfs-shapes') { 170 | try { 171 | return callback(null, gtfs2geojson.lines(text)); 172 | } catch (e) { 173 | return callback({ message: 'Invalid GTFS shapes.txt file' }); 174 | } 175 | } else if (fileType === 'gtfs-stops') { 176 | try { 177 | return callback(null, gtfs2geojson.stops(text)); 178 | } catch (e) { 179 | return callback({ message: 'Invalid GTFS stops.txt file' }); 180 | } 181 | } else if (fileType === 'poly') { 182 | callback(null, polytogeojson(text)); 183 | } 184 | 185 | function toDom(x) { 186 | return new DOMParser().parseFromString(x, 'text/xml'); 187 | } 188 | 189 | function detectType(f, text) { 190 | const filename = f.name ? f.name.toLowerCase() : ''; 191 | function ext(_) { 192 | return filename.indexOf(_) !== -1; 193 | } 194 | if (f.type === 'application/vnd.google-earth.kml+xml' || ext('.kml')) { 195 | return 'kml'; 196 | } 197 | if (ext('.gpx')) return 'gpx'; 198 | if (ext('.geojson') || ext('.json') || ext('.topojson')) return 'geojson'; 199 | if (f.type === 'text/csv' || ext('.csv') || ext('.tsv') || ext('.dsv')) { 200 | return 'dsv'; 201 | } 202 | if (ext('.xml') || ext('.osm')) return 'xml'; 203 | if (ext('.poly')) return 'poly'; 204 | if (text && text.indexOf('shape_id,shape_pt_lat,shape_pt_lon') !== -1) { 205 | return 'gtfs-shapes'; 206 | } 207 | if ( 208 | text && 209 | text.indexOf( 210 | 'stop_id,stop_code,stop_name,stop_desc,stop_lat,stop_lon' 211 | ) !== -1 212 | ) { 213 | return 'gtfs-stops'; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | ::-webkit-scrollbar { width:5px; height:10px; } 51 | ::-webkit-scrollbar-track { background:transparent; } 52 | ::-webkit-scrollbar-thumb { background:rgba(0,0,0,0.2); } 53 | 54 | /* base */ 55 | 56 | body#geojsonio-body { 57 | margin:0; padding:0; 58 | font-family: 'Open Sans', 'Helvetica Neue', sans-serif; 59 | font-size:15px; 60 | line-height:20px; 61 | color:#222; 62 | -webkit-font-smoothing:antialiased; 63 | } 64 | 65 | :focus { outline:0; } 66 | 67 | a { 68 | color:#2980b9; 69 | text-decoration:none; 70 | } 71 | 72 | a:hover { 73 | color:#199CF4; 74 | } 75 | 76 | p { 77 | padding:10px 0; 78 | line-height:200%; 79 | } 80 | 81 | small { font-size: 11px;} 82 | 83 | /* Columns 84 | ------------------------------------------------------- */ 85 | 86 | .col0 { float:left; width:04.1666%; } 87 | .col1 { float:left; width:08.3333%; } 88 | .col2 { float:left; width:16.6666%; } 89 | .col3 { float:left; width:25.0000%; } 90 | .col4 { float:left; width:33.3333%; } 91 | .col5 { float:left; width:41.6666%; } 92 | .col6 { float:left; width:50.0000%; } 93 | .col7 { float:left; width:58.3333%; } 94 | .col8 { float:left; width:66.6666%; } 95 | .col9 { float:left; width:75.0000%; } 96 | .col10 { float:left; width:83.3333%; } 97 | .col11 { float:left; width:91.6666%; } 98 | .col12 { float:left; width:100.0000%; } 99 | .margin0 { margin-left: 04.1666%; } 100 | .margin1 { margin-left: 08.3333%; } 101 | .margin2 { margin-left: 16.6666%; } 102 | .margin3 { margin-left: 25.0000%; } 103 | .margin4 { margin-left: 33.3333%; } 104 | .margin5 { margin-left: 41.6666%; } 105 | .margin6 { margin-left: 50.0000%; } 106 | .margin7 { margin-left: 58.3333%; } 107 | .margin8 { margin-left: 66.6666%; } 108 | .margin9 { margin-left: 75.0000%; } 109 | .margin10 { margin-left: 83.3333%; } 110 | .margin11 { margin-left: 91.6666%; } 111 | .margin12 { margin-left: 100.0000%; } 112 | 113 | /* Padding 114 | ------------------------------------------------------- */ 115 | 116 | .pad0 { padding:5px; } 117 | .pad0y { padding-top:5px; padding-bottom:5px; } 118 | .pad0x { padding-right:5px; padding-left:5px; } 119 | 120 | .pad1 { padding:10px; } 121 | .pad2 { padding:20px; } 122 | .pad4 { padding:40px; } 123 | 124 | .pad1x { padding-left: 10px; padding-right: 10px;} 125 | .pad2x { padding-left: 20px; padding-right: 20px;} 126 | .pad4x { padding-left: 40px; padding-right: 40px;} 127 | 128 | .pad1y { padding-top: 10px; padding-bottom: 10px;} 129 | .pad2y { padding-top: 20px; padding-bottom: 20px;} 130 | .pad4y { padding-top: 40px; padding-bottom: 40px;} 131 | 132 | /* Margin 133 | ------------------------------------------------------- */ 134 | 135 | .space-bottom0 { margin-bottom: 5px;} 136 | .space-bottom1 { margin-bottom:10px; } 137 | .space-bottom2 { margin-bottom:20px; } 138 | .space-bottom4 { margin-bottom:40px; } 139 | .space-top0 { margin-top: 5px; } 140 | .space-top1 { margin-top: 10px; } 141 | .space-top2 { margin-top: 20px; } 142 | .space-top4 { margin-top: 40px;} 143 | 144 | /* Z-index 145 | ------------------------------------------------------- */ 146 | 147 | .z1 { z-index:1; } 148 | .z10 { z-index:10; } 149 | .z100 { z-index:100; } 150 | 151 | /* Absolute containers 152 | ------------------------------------------------------- */ 153 | .pin-top, 154 | .pin-right, 155 | .pin-bottom, 156 | .pin-left, 157 | .pin-topleft, 158 | .pin-topright, 159 | .pin-bottomleft, 160 | .pin-bottomright { 161 | position:absolute; 162 | } 163 | .pin-bottom { right:0; bottom:0; left:0; } 164 | .pin-top { top:0; right:0; left:0; } 165 | .pin-left { top:0; bottom:0; left:0; } 166 | .pin-right { top:0; right:0; bottom:0; } 167 | .pin-bottomright { bottom:0; right:0; } 168 | .pin-bottomleft { bottom:0; left:0; } 169 | .pin-topright { top:0; right:0; } 170 | .pin-topleft { top:0; left:0; } 171 | 172 | /* Keylines 173 | ------------------------------------------------------- */ 174 | .keyline-all { border:1px solid #ccc; } 175 | .keyline-top { border-top:1px solid #ccc; } 176 | .keyline-right { border-right:1px solid #ccc; } 177 | .keyline-bottom { border-bottom:1px solid #ccc; } 178 | .keyline-left { border-left:1px solid #ccc; } 179 | 180 | /* Markup free clearing 181 | Details: http://www.positioniseverything.net/easyclearing.html 182 | ------------------------------------------------------- */ 183 | 184 | .clearfix:after { 185 | content: ""; 186 | display: block; 187 | height: 0; 188 | clear: both; 189 | visibility: hidden; 190 | } 191 | 192 | .clearfix { display: inline-block; } 193 | .hide { display:none; } 194 | .fl { float:left; } 195 | .fr { float:right; } 196 | .center { text-align:center; } 197 | .text-right { text-align: right;} 198 | 199 | /* Elements */ 200 | table tr td input, 201 | table tr th input { 202 | border:none; 203 | } 204 | 205 | table thead { 206 | background:#f7f7f7; 207 | border:1px solid #ccc; 208 | } 209 | 210 | table td { 211 | border:1px solid #ccc; 212 | } 213 | 214 | table thead tr th { 215 | font-weight:bold; 216 | } 217 | 218 | table thead tr th, 219 | table tr th input, 220 | table tbody tr td input { 221 | text-align:left; 222 | font:inherit; 223 | padding:5px; 224 | width:150px; 225 | box-sizing:border-box; 226 | margin:0; 227 | } 228 | 229 | input[type=text] { 230 | font:inherit; 231 | } 232 | 233 | /* Loading overlay 234 | ------------------------------------------------------- */ 235 | .loading:before { 236 | content:''; 237 | display:block; 238 | position:absolute; 239 | left:0px; 240 | top:0px; 241 | width:100%; 242 | height:100%; 243 | background:rgba(128,128,128,0.25); 244 | z-index:10; 245 | } 246 | 247 | .loading:after { 248 | content:''; 249 | display:block; 250 | position:absolute; 251 | left:50%; 252 | top:50%; 253 | margin:-20px 0px 0px -20px; 254 | width:40px; 255 | height:40px; 256 | border-radius:50%; 257 | opacity:0.25; 258 | background:#444 url(spinner.gif) 50% 50% no-repeat; 259 | } 260 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | geojson.io | powered by Mapbox 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 74 | 82 | 83 | 84 | 85 |
86 |
87 |
88 | geojson.io 89 |
90 |
91 |
92 |
93 | powered by 94 |
95 |
100 |
101 | 107 |
108 |
109 |
110 |
111 | 118 | 119 | 120 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/core/data.js: -------------------------------------------------------------------------------- 1 | const clone = require('clone'), 2 | xtend = require('xtend'), 3 | config = require('../config.js')(location.hostname), 4 | source = { 5 | gist: require('../source/gist'), 6 | github: require('../source/github'), 7 | local: require('../source/local') 8 | }; 9 | 10 | function _getData() { 11 | return { 12 | map: { 13 | type: 'FeatureCollection', 14 | features: [] 15 | }, 16 | dirty: false, 17 | source: null, 18 | meta: null, 19 | type: 'local', 20 | mapStyleLoaded: false 21 | }; 22 | } 23 | 24 | module.exports = function (context) { 25 | const _data = _getData(); 26 | 27 | function mapFile(gist) { 28 | let f; 29 | let content; 30 | 31 | for (f in gist.files) { 32 | content = gist.files[f].content; 33 | if (f.indexOf('.geojson') !== -1 && content) { 34 | return f; 35 | } 36 | } 37 | 38 | for (f in gist.files) { 39 | content = gist.files[f].content; 40 | if (f.indexOf('.json') !== -1 && content) { 41 | return f; 42 | } 43 | } 44 | } 45 | 46 | const data = {}; 47 | 48 | data.hasFeatures = function () { 49 | return !!(_data.map && _data.map.features && _data.map.features.length); 50 | }; 51 | 52 | data.set = function (obj, src) { 53 | for (const k in obj) { 54 | _data[k] = typeof obj[k] === 'object' ? clone(obj[k], false) : obj[k]; 55 | } 56 | if (obj.dirty !== false) data.dirty = true; 57 | context.dispatch.change({ 58 | obj: obj, 59 | source: src 60 | }); 61 | return data; 62 | }; 63 | 64 | data.clear = function () { 65 | data.set(_getData()); 66 | }; 67 | 68 | data.mergeFeatures = function (features, src) { 69 | function coerceNum(feature) { 70 | const props = feature.properties, 71 | keys = Object.keys(props), 72 | length = keys.length; 73 | 74 | for (let i = 0; i < length; i++) { 75 | const key = keys[i]; 76 | const value = props[key]; 77 | feature.properties[key] = losslessNumber(value); 78 | } 79 | 80 | return feature; 81 | } 82 | 83 | function losslessNumber(x) { 84 | const fl = parseFloat(x); 85 | if (fl.toString() === x) return fl; 86 | else return x; 87 | } 88 | 89 | _data.map.features = (_data.map.features || []).concat( 90 | features.map(coerceNum) 91 | ); 92 | return data.set({ map: _data.map }, src); 93 | }; 94 | 95 | data.get = function (k) { 96 | return _data[k]; 97 | }; 98 | 99 | data.all = function () { 100 | return clone(_data, false); 101 | }; 102 | 103 | data.fetch = function (q, cb) { 104 | const type = q.id.split(':')[0]; 105 | 106 | switch (type) { 107 | case 'gist': { 108 | const id = q.id.split(':')[1].split('/')[1]; 109 | 110 | // From: https://api.github.com/gists/dfa850f66f61ddc58bbf 111 | // Gists > 1 MB will have truncated set to true. Request 112 | // the raw URL in those cases. 113 | source.gist.load(id, context, (err, d) => { 114 | if (err) return cb(err, d); 115 | 116 | const file = mapFile(d); 117 | // Test for .json or .geojson found 118 | if (typeof file === 'undefined') return cb(err, d); 119 | 120 | const f = d.files[file]; 121 | if (f.truncated === true) { 122 | source.gist.loadRaw(f.raw_url, context, (err, content) => { 123 | if (err) return cb(err); 124 | return cb( 125 | err, 126 | xtend(d, { file: f.filename, content: JSON.parse(content) }) 127 | ); 128 | }); 129 | } else { 130 | return cb( 131 | err, 132 | xtend(d, { file: f.filename, content: JSON.parse(f.content) }) 133 | ); 134 | } 135 | }); 136 | 137 | break; 138 | } 139 | case 'github': { 140 | const url = q.id.split('/'); 141 | const parts = { 142 | user: url[0].split(':')[1], 143 | repo: url[1], 144 | branch: url[3], 145 | path: (url.slice(4) || []).join('/') 146 | }; 147 | 148 | source.github.load(parts, context, (err, meta) => { 149 | if (err) return cb(err); 150 | return source.github.loadRaw( 151 | parts, 152 | meta.sha, 153 | context, 154 | (err, file) => { 155 | try { 156 | return cb(err, xtend(meta, { content: JSON.parse(file) })); 157 | } catch (e) { 158 | // this was not a github file 159 | history.replaceState( 160 | '', 161 | document.title, 162 | window.location.pathname 163 | ); 164 | return cb(e); 165 | } 166 | } 167 | ); 168 | }); 169 | 170 | break; 171 | } 172 | } 173 | }; 174 | 175 | data.parse = function (d) { 176 | const endpoint = config.GithubAPI || 'https://github.com/'; 177 | let login, repo, branch, path, chunked; 178 | 179 | if (d.files) d.type = 'gist'; 180 | let type = d.length ? d[d.length - 1].type : d.type; 181 | if (d.commit) type = 'commit'; 182 | switch (type) { 183 | case 'commit': { 184 | data.set({ 185 | source: d.content 186 | }); 187 | break; 188 | } 189 | case 'local': { 190 | data.set({ 191 | type: 'local', 192 | map: d.content, 193 | path: d.path 194 | }); 195 | break; 196 | } 197 | case 'blob': { 198 | login = d[0].login; 199 | repo = d[1].name; 200 | branch = d[2].name; 201 | path = d 202 | .slice(3) 203 | .map((p) => { 204 | return p.path; 205 | }) 206 | .join('/'); 207 | 208 | data.set({ 209 | type: 'github', 210 | source: d, 211 | meta: { 212 | login: login, 213 | repo: repo, 214 | branch: branch, 215 | name: d.path 216 | }, 217 | path: path, 218 | route: 'github:' + [login, repo, 'blob', branch, path].join('/'), 219 | url: [endpoint, login, repo, 'blob', branch, path].join('/') 220 | }); 221 | if (d.content) data.set({ map: d.content }); 222 | break; 223 | } 224 | case 'file': { 225 | chunked = d.html_url.split('/'); 226 | login = chunked[3]; 227 | repo = chunked[4]; 228 | branch = chunked[6]; 229 | 230 | data.set({ 231 | type: 'github', 232 | source: d, 233 | meta: { 234 | login: login, 235 | repo: repo, 236 | branch: branch, 237 | name: d.name, 238 | sha: d.sha 239 | }, 240 | map: d.content, 241 | path: d.path, 242 | route: 'github:' + [login, repo, 'blob', branch, d.path].join('/'), 243 | url: d.html_url 244 | }); 245 | break; 246 | } 247 | case 'gist': { 248 | login = (d.owner && d.owner.login) || 'anonymous'; 249 | path = [login, d.id].join('/'); 250 | 251 | const name = mapFile(d); 252 | 253 | try { 254 | if (d.files[name].content) 255 | data.set({ map: JSON.parse(d.files[name].content) }); 256 | } catch (e) { 257 | console.error(e); 258 | } 259 | data.set({ 260 | type: 'gist', 261 | source: d, 262 | meta: { 263 | login: login, 264 | name: name 265 | }, 266 | path: path, 267 | route: 'gist:' + path, 268 | url: d.html_url 269 | }); 270 | break; 271 | } 272 | } 273 | }; 274 | 275 | data.save = function (cb) { 276 | const type = context.data.get('type'); 277 | if (type === 'github') { 278 | source.github.save(context, cb); 279 | } else if (type === 'gist') { 280 | source.gist.save(context, cb); 281 | } else if (type === 'local') { 282 | if (context.data.path) { 283 | source.local.save(context, cb); 284 | } else { 285 | source.gist.save(context, cb); 286 | } 287 | } else { 288 | source.gist.save(context, cb); 289 | } 290 | }; 291 | 292 | return data; 293 | }; 294 | -------------------------------------------------------------------------------- /img/polygon.svg: -------------------------------------------------------------------------------- 1 | image/svg+xml -------------------------------------------------------------------------------- /css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre.CodeMirror-line, 17 | .CodeMirror pre.CodeMirror-line-like { 18 | padding: 0 4px; /* Horizontal padding of content */ 19 | } 20 | 21 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 22 | background-color: white; /* The little square between H and V scrollbars */ 23 | } 24 | 25 | /* GUTTER */ 26 | 27 | .CodeMirror-gutters { 28 | border-right: 1px solid #ddd; 29 | background-color: #f7f7f7; 30 | white-space: nowrap; 31 | } 32 | .CodeMirror-linenumbers {} 33 | .CodeMirror-linenumber { 34 | padding: 0 3px 0 5px; 35 | min-width: 20px; 36 | text-align: right; 37 | color: #999; 38 | white-space: nowrap; 39 | } 40 | 41 | .CodeMirror-guttermarker { color: black; } 42 | .CodeMirror-guttermarker-subtle { color: #999; } 43 | 44 | /* CURSOR */ 45 | 46 | .CodeMirror-cursor { 47 | border-left: 1px solid black; 48 | border-right: none; 49 | width: 0; 50 | } 51 | /* Shown when moving in bi-directional text */ 52 | .CodeMirror div.CodeMirror-secondarycursor { 53 | border-left: 1px solid silver; 54 | } 55 | .cm-fat-cursor .CodeMirror-cursor { 56 | width: auto; 57 | border: 0 !important; 58 | background: #7e7; 59 | } 60 | .cm-fat-cursor div.CodeMirror-cursors { 61 | z-index: 1; 62 | } 63 | .cm-fat-cursor .CodeMirror-line::selection, 64 | .cm-fat-cursor .CodeMirror-line > span::selection, 65 | .cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; } 66 | .cm-fat-cursor .CodeMirror-line::-moz-selection, 67 | .cm-fat-cursor .CodeMirror-line > span::-moz-selection, 68 | .cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; } 69 | .cm-fat-cursor { caret-color: transparent; } 70 | @-moz-keyframes blink { 71 | 0% {} 72 | 50% { background-color: transparent; } 73 | 100% {} 74 | } 75 | @-webkit-keyframes blink { 76 | 0% {} 77 | 50% { background-color: transparent; } 78 | 100% {} 79 | } 80 | @keyframes blink { 81 | 0% {} 82 | 50% { background-color: transparent; } 83 | 100% {} 84 | } 85 | 86 | /* Can style cursor different in overwrite (non-insert) mode */ 87 | .CodeMirror-overwrite .CodeMirror-cursor {} 88 | 89 | .cm-tab { display: inline-block; text-decoration: inherit; } 90 | 91 | .CodeMirror-rulers { 92 | position: absolute; 93 | left: 0; right: 0; top: -50px; bottom: 0; 94 | overflow: hidden; 95 | } 96 | .CodeMirror-ruler { 97 | border-left: 1px solid #ccc; 98 | top: 0; bottom: 0; 99 | position: absolute; 100 | } 101 | 102 | /* DEFAULT THEME */ 103 | 104 | .cm-s-default .cm-header {color: blue;} 105 | .cm-s-default .cm-quote {color: #090;} 106 | .cm-negative {color: #d44;} 107 | .cm-positive {color: #292;} 108 | .cm-header, .cm-strong {font-weight: bold;} 109 | .cm-em {font-style: italic;} 110 | .cm-link {text-decoration: underline;} 111 | .cm-strikethrough {text-decoration: line-through;} 112 | 113 | .cm-s-default .cm-keyword {color: #708;} 114 | .cm-s-default .cm-atom {color: #219;} 115 | .cm-s-default .cm-number {color: #164;} 116 | .cm-s-default .cm-def {color: #00f;} 117 | .cm-s-default .cm-variable, 118 | .cm-s-default .cm-punctuation, 119 | .cm-s-default .cm-property, 120 | .cm-s-default .cm-operator {} 121 | .cm-s-default .cm-variable-2 {color: #05a;} 122 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 123 | .cm-s-default .cm-comment {color: #a50;} 124 | .cm-s-default .cm-string {color: #a11;} 125 | .cm-s-default .cm-string-2 {color: #f50;} 126 | .cm-s-default .cm-meta {color: #555;} 127 | .cm-s-default .cm-qualifier {color: #555;} 128 | .cm-s-default .cm-builtin {color: #30a;} 129 | .cm-s-default .cm-bracket {color: #997;} 130 | .cm-s-default .cm-tag {color: #170;} 131 | .cm-s-default .cm-attribute {color: #00c;} 132 | .cm-s-default .cm-hr {color: #999;} 133 | .cm-s-default .cm-link {color: #00c;} 134 | 135 | .cm-s-default .cm-error {color: #f00;} 136 | .cm-invalidchar {color: #f00;} 137 | 138 | .CodeMirror-composing { border-bottom: 2px solid; } 139 | 140 | /* Default styles for common addons */ 141 | 142 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 143 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 144 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 145 | .CodeMirror-activeline-background {background: #e8f2ff;} 146 | 147 | /* STOP */ 148 | 149 | /* The rest of this file contains styles related to the mechanics of 150 | the editor. You probably shouldn't touch them. */ 151 | 152 | .CodeMirror { 153 | position: relative; 154 | overflow: hidden; 155 | background: white; 156 | } 157 | 158 | .CodeMirror-scroll { 159 | overflow: scroll !important; /* Things will break if this is overridden */ 160 | /* 50px is the magic margin used to hide the element's real scrollbars */ 161 | /* See overflow: hidden in .CodeMirror */ 162 | margin-bottom: -50px; margin-right: -50px; 163 | padding-bottom: 50px; 164 | height: 100%; 165 | outline: none; /* Prevent dragging from highlighting the element */ 166 | position: relative; 167 | z-index: 0; 168 | } 169 | .CodeMirror-sizer { 170 | position: relative; 171 | border-right: 50px solid transparent; 172 | } 173 | 174 | /* The fake, visible scrollbars. Used to force redraw during scrolling 175 | before actual scrolling happens, thus preventing shaking and 176 | flickering artifacts. */ 177 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 178 | position: absolute; 179 | z-index: 6; 180 | display: none; 181 | outline: none; 182 | } 183 | .CodeMirror-vscrollbar { 184 | right: 0; top: 0; 185 | overflow-x: hidden; 186 | overflow-y: scroll; 187 | } 188 | .CodeMirror-hscrollbar { 189 | bottom: 0; left: 0; 190 | overflow-y: hidden; 191 | overflow-x: scroll; 192 | } 193 | .CodeMirror-scrollbar-filler { 194 | right: 0; bottom: 0; 195 | } 196 | .CodeMirror-gutter-filler { 197 | left: 0; bottom: 0; 198 | } 199 | 200 | .CodeMirror-gutters { 201 | position: absolute; left: 0; top: 0; 202 | min-height: 100%; 203 | z-index: 3; 204 | } 205 | .CodeMirror-gutter { 206 | white-space: normal; 207 | height: 100%; 208 | display: inline-block; 209 | vertical-align: top; 210 | margin-bottom: -50px; 211 | } 212 | .CodeMirror-gutter-wrapper { 213 | position: absolute; 214 | z-index: 4; 215 | background: none !important; 216 | border: none !important; 217 | } 218 | .CodeMirror-gutter-background { 219 | position: absolute; 220 | top: 0; bottom: 0; 221 | z-index: 4; 222 | } 223 | .CodeMirror-gutter-elt { 224 | position: absolute; 225 | cursor: default; 226 | z-index: 4; 227 | } 228 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 229 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 230 | 231 | .CodeMirror-lines { 232 | cursor: text; 233 | min-height: 1px; /* prevents collapsing before first draw */ 234 | } 235 | .CodeMirror pre.CodeMirror-line, 236 | .CodeMirror pre.CodeMirror-line-like { 237 | /* Reset some styles that the rest of the page might have set */ 238 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 239 | border-width: 0; 240 | background: transparent; 241 | font-family: inherit; 242 | font-size: inherit; 243 | margin: 0; 244 | white-space: pre; 245 | word-wrap: normal; 246 | line-height: inherit; 247 | color: inherit; 248 | z-index: 2; 249 | position: relative; 250 | overflow: visible; 251 | -webkit-tap-highlight-color: transparent; 252 | -webkit-font-variant-ligatures: contextual; 253 | font-variant-ligatures: contextual; 254 | } 255 | .CodeMirror-wrap pre.CodeMirror-line, 256 | .CodeMirror-wrap pre.CodeMirror-line-like { 257 | word-wrap: break-word; 258 | white-space: pre-wrap; 259 | word-break: normal; 260 | } 261 | 262 | .CodeMirror-linebackground { 263 | position: absolute; 264 | left: 0; right: 0; top: 0; bottom: 0; 265 | z-index: 0; 266 | } 267 | 268 | .CodeMirror-linewidget { 269 | position: relative; 270 | z-index: 2; 271 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 272 | } 273 | 274 | .CodeMirror-widget {} 275 | 276 | .CodeMirror-rtl pre { direction: rtl; } 277 | 278 | .CodeMirror-code { 279 | outline: none; 280 | } 281 | 282 | /* Force content-box sizing for the elements where we expect it */ 283 | .CodeMirror-scroll, 284 | .CodeMirror-sizer, 285 | .CodeMirror-gutter, 286 | .CodeMirror-gutters, 287 | .CodeMirror-linenumber { 288 | -moz-box-sizing: content-box; 289 | box-sizing: content-box; 290 | } 291 | 292 | .CodeMirror-measure { 293 | position: absolute; 294 | width: 100%; 295 | height: 0; 296 | overflow: hidden; 297 | visibility: hidden; 298 | } 299 | 300 | .CodeMirror-cursor { 301 | position: absolute; 302 | pointer-events: none; 303 | } 304 | .CodeMirror-measure pre { position: static; } 305 | 306 | div.CodeMirror-cursors { 307 | visibility: hidden; 308 | position: relative; 309 | z-index: 3; 310 | } 311 | div.CodeMirror-dragcursors { 312 | visibility: visible; 313 | } 314 | 315 | .CodeMirror-focused div.CodeMirror-cursors { 316 | visibility: visible; 317 | } 318 | 319 | .CodeMirror-selected { background: #d9d9d9; } 320 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 321 | .CodeMirror-crosshair { cursor: crosshair; } 322 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 323 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 324 | 325 | .cm-searching { 326 | background-color: #ffa; 327 | background-color: rgba(255, 255, 0, .4); 328 | } 329 | 330 | /* Used to force a border model for a node */ 331 | .cm-force-border { padding-right: .1px; } 332 | 333 | @media print { 334 | /* Hide the cursor when printing */ 335 | .CodeMirror div.CodeMirror-cursors { 336 | visibility: hidden; 337 | } 338 | } 339 | 340 | /* See issue #2901 */ 341 | .cm-tab-wrap-hack:after { content: ''; } 342 | 343 | /* Help users use markselection to safely style text background */ 344 | span.CodeMirror-selectedtext { background: none; } 345 | .CodeMirror-foldmarker { 346 | color: blue; 347 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 348 | font-family: arial; 349 | line-height: .3; 350 | cursor: pointer; 351 | } 352 | .CodeMirror-foldgutter { 353 | width: .7em; 354 | } 355 | .CodeMirror-foldgutter-open, 356 | .CodeMirror-foldgutter-folded { 357 | cursor: pointer; 358 | } 359 | .CodeMirror-foldgutter-open:after { 360 | content: "\25BE"; 361 | } 362 | .CodeMirror-foldgutter-folded:after { 363 | content: "\25B8"; 364 | } 365 | --------------------------------------------------------------------------------