├── .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 |
10 |
11 |
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 |
28 | Save
29 |
30 |
31 | Cancel
32 |
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 |
48 |
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 |
45 | The data you create and modify in geojson.io doesn't
46 | acquire any additional license: if it's secret and copyrighted, it will remain
47 | that way - it doesn't have to be public or open source.
48 |
49 |
--------------------------------------------------------------------------------
/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 | [](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fgeojson.io?ref=badge_shield)
2 |
3 | # geojson.io
4 |
5 | 
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 | [](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 |
52 | Mapbox Studio is a free online browser application for
53 | designing maps with awesome style flexibility.
54 | QGIS is an open source, free desktop application for
55 | doing GIS analysis on data.
56 | Leaflet is an open source map framework
57 | for embedding maps in webpages.
58 |
59 |
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 |
64 | ogre , an online tool for shapefile to geojson conversion
65 | shpescape , another online tool that supports conversion
66 | Arc2Earth , an ArcGIS plugin that exports GeoJSON
67 | esri2open , a python script that converts ESRI Feature classes to GeoJSON
68 |
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 | 
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 | 
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 | 
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 | 
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 |
90 |
91 |
92 |
93 | powered by
94 |
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 |
--------------------------------------------------------------------------------