├── .nvmrc
├── app
├── graphics
│ ├── layout
│ │ ├── .gitkeep
│ │ ├── chevron-down.svg
│ │ ├── arrow-right--white.svg
│ │ ├── xmark.svg
│ │ ├── arrow-right.svg
│ │ ├── arrow-left--white.svg
│ │ ├── arrow-left.svg
│ │ ├── icon-visibility.svg
│ │ ├── icon-line.svg
│ │ ├── icon-visibility2.svg
│ │ ├── icon-addline.svg
│ │ ├── icon-trash.svg
│ │ └── icon-cut.svg
│ ├── meta
│ │ └── .gitkeep
│ └── content
│ │ └── .gitkeep
├── robots.txt
├── scripts
│ ├── config
│ │ ├── production.js
│ │ ├── staging.js
│ │ └── base.js
│ ├── util
│ │ ├── app.js
│ │ ├── line.js
│ │ ├── auto-save.js
│ │ └── compress-changes.js
│ ├── components
│ │ ├── app
│ │ │ ├── header.js
│ │ │ ├── footer.js
│ │ │ ├── not-found.js
│ │ │ └── index.js
│ │ ├── map
│ │ │ ├── utils
│ │ │ │ └── constants.js
│ │ │ ├── styles
│ │ │ │ └── mapbox-draw-styles.js
│ │ │ └── index.js
│ │ ├── home.js
│ │ └── auto-save.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── save.js
│ │ ├── map.js
│ │ ├── draw.js
│ │ └── selection.js
│ ├── config.js
│ ├── main.js
│ └── actions
│ │ └── index.js
├── styles
│ ├── _utils.scss
│ ├── settings
│ │ └── _variables.scss
│ ├── _typography.scss
│ ├── _base.scss
│ ├── _modal.scss
│ ├── main.scss
│ ├── _reset.scss
│ ├── _normalize-opentype.scss
│ ├── _map.scss
│ └── _normalize.scss
├── humans.txt
└── index.html
├── .babelrc
├── .gitignore
├── circle.yml
├── .eslintrc
├── README.md
├── test
├── actions.js
├── map.js
└── compress-changes.js
├── package.json
└── gulpfile.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | 6.9
2 |
--------------------------------------------------------------------------------
/app/graphics/layout/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/graphics/meta/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/graphics/content/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
4 | Disallow:
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .tmp
3 | **/npm-debug.log
4 | .DS_Store
5 | *.swp
6 | dist
7 |
--------------------------------------------------------------------------------
/app/scripts/config/production.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | environment: 'production',
5 | baseUrl: 'http://138.197.97.15'
6 | };
7 |
--------------------------------------------------------------------------------
/app/scripts/config/staging.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | * App config overrides for staging.
4 | */
5 |
6 | module.exports = {
7 | environment: 'staging'
8 | };
9 |
--------------------------------------------------------------------------------
/app/scripts/util/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import check from 'mapbox-gl-supported';
3 | const App = {
4 | glSupport: check(),
5 | map: null
6 | };
7 | export default App;
8 |
--------------------------------------------------------------------------------
/app/scripts/util/line.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | export const firstCoord = (line) => line.geometry.coordinates[0];
3 | export const lastCoord = (line) => line.geometry.coordinates[line.geometry.coordinates.length - 1];
4 |
--------------------------------------------------------------------------------
/app/scripts/config/base.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | environment: 'development',
3 | baseUrl: 'http://138.197.97.15',
4 | existingRoadsSource: 'https://s3.amazonaws.com/vietbando/{z}/{x}/{y}.pbf',
5 | initialZoom: 14,
6 | minTileZoom: 13
7 | };
8 |
--------------------------------------------------------------------------------
/app/graphics/layout/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 6.9
4 | test:
5 | override:
6 | - npm run test
7 | deployment:
8 | production:
9 | branch: master
10 | commands:
11 | - npm run build
12 | - surge --project ./dist --domain skynet-scrub.surge.sh
13 |
--------------------------------------------------------------------------------
/app/styles/_utils.scss:
--------------------------------------------------------------------------------
1 |
2 | /* Clearfix
3 | ========================================================================== */
4 |
5 | .clearfix {
6 | &:before,
7 | &:after {
8 | content: " ";
9 | display: table;
10 | }
11 | &:after {
12 | clear: both;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/scripts/components/app/header.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 | var Header = React.createClass({
4 | displayName: 'Header',
5 | render: function () {
6 | return (
7 |
8 |
9 | );
10 | }
11 | });
12 | export default Header;
13 |
--------------------------------------------------------------------------------
/app/scripts/components/map/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const SPLIT = 'split';
2 | export const COMPLETE = 'complete';
3 | export const INCOMPLETE = 'incomplete';
4 | export const EDITED = 'in progress';
5 | export const MULTIPLE = 'multiple';
6 | export const CONTINUE = 'continue';
7 | export const INACTIVE = 'inactive';
8 |
--------------------------------------------------------------------------------
/app/scripts/components/app/footer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 |
4 | var Footer = React.createClass({
5 | displayName: 'Footer',
6 |
7 | render: function () {
8 | return (
9 |
10 |
Footer
11 |
12 | );
13 | }
14 | });
15 |
16 | export default Footer;
17 |
--------------------------------------------------------------------------------
/app/scripts/components/app/not-found.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 |
4 | var NotFound = React.createClass({
5 | displayName: 'NotFound',
6 |
7 | render: function () {
8 | return (
9 |
10 |
404
11 |
12 | );
13 | }
14 | });
15 |
16 | export default NotFound;
17 |
--------------------------------------------------------------------------------
/app/scripts/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import selection from './selection';
3 | import map from './map';
4 | import draw from './draw';
5 | import save from './save';
6 |
7 | export const reducers = {
8 | selection,
9 | map,
10 | draw,
11 | save
12 | };
13 |
14 | export default combineReducers(Object.assign({}, reducers));
15 |
--------------------------------------------------------------------------------
/app/scripts/components/home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import Map from './map';
5 | import AutoSave from './auto-save';
6 |
7 | var Home = React.createClass({
8 | displayName: 'Home',
9 |
10 | render: function () {
11 | return (
12 |
16 | );
17 | }
18 | });
19 | export default connect(state => state)(Home);
20 |
--------------------------------------------------------------------------------
/app/scripts/util/auto-save.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import store from 'store2';
3 | const KEY = 'skynet_scrub_local';
4 | const NULL = 'null';
5 | export function getLocalActions () {
6 | const stored = store.get(KEY);
7 | return stored === NULL ? false : stored;
8 | }
9 | export function destroyLocalActions () {
10 | const stored = store.get(KEY);
11 | store.set(KEY, NULL);
12 | return stored === NULL ? false : stored;
13 | }
14 | export function saveLocalActions (actions) {
15 | store.set(KEY, actions);
16 | return actions;
17 | }
18 |
--------------------------------------------------------------------------------
/app/scripts/reducers/save.js:
--------------------------------------------------------------------------------
1 | import { SAVE, LOCAL_STORAGE } from '../actions';
2 |
3 | const initial = {
4 | inflight: false,
5 | error: null,
6 | success: null,
7 | historyId: null,
8 | cached: null
9 | };
10 |
11 | const save = (state = initial, action) => {
12 | switch (action.type) {
13 | case SAVE:
14 | return Object.assign({}, state, action.data);
15 | case LOCAL_STORAGE:
16 | return Object.assign({}, state, { cached: action.data });
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default save;
23 |
--------------------------------------------------------------------------------
/app/scripts/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import assert from 'assert';
3 |
4 | var configurations = {
5 | base: require('./config/base'),
6 | staging: require('./config/staging'),
7 | production: require('./config/production')
8 | };
9 | var config = configurations.base || {};
10 |
11 | if (process.env.DS_ENV === 'staging') {
12 | config = Object.assign({}, config, configurations.staging);
13 | } else if (process.env.DS_ENV === 'production') {
14 | config = Object.assign({}, config, configurations.production);
15 | }
16 |
17 | assert(typeof config.apiRoot, 'string');
18 |
19 | module.exports = config;
20 |
--------------------------------------------------------------------------------
/app/scripts/components/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 |
5 | import Header from './header';
6 | import Footer from './footer';
7 |
8 | var App = React.createClass({
9 | displayName: 'App',
10 |
11 | propTypes: {
12 | children: React.PropTypes.object
13 | },
14 |
15 | render: function () {
16 | return (
17 |
18 |
19 |
20 | {this.props.children}
21 |
22 |
23 |
24 | );
25 | }
26 | });
27 |
28 | export default connect(state => state)(App);
29 |
--------------------------------------------------------------------------------
/app/styles/settings/_variables.scss:
--------------------------------------------------------------------------------
1 | $black: #111;
2 | $white: #FEFEFE;
3 |
4 | $light-blue: #76EBF9;
5 | $blue: #53D8E8;
6 | $darker-blue: #2CB0C0;
7 |
8 | /* Line Colors */
9 | $grey-incomplete: #D1D1D1;
10 | $green-completed: #5EED94;
11 | $yellow-inprogress: #FFD23B;
12 | $blue-existing: #3B9FFF;
13 |
14 | /* Greys */
15 | $dark-grey: #4A4A4A;
16 | $grey: #635B5B;
17 | $base-grey: #8D8484;
18 | $light-grey: #928888;
19 | $lightest-grey: #E8E8E8;
20 |
21 | $base-font-color: $grey;
22 | $base-font-family: 'Karla', 'Helvetica Neue', Helvetica, Arial, sans-serif;
23 | $base-font-style: normal;
24 | $base-font-regular: 400;
25 | $base-font-italic: 400i;
26 | $base-font-bold: 700;
--------------------------------------------------------------------------------
/app/graphics/layout/arrow-right--white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/humans.txt:
--------------------------------------------------------------------------------
1 | _____ _ _ _____ _
2 | | _ \ | | | | / ___| | |
3 | | | | |_____ _____| | ___ _ __ _ __ ___ ___ _ __ | |_ \ `--. ___ ___ __| |
4 | | | | / _ \ \ / / _ \ |/ _ \| '_ \| '_ ` _ \ / _ \ '_ \| __| `--. \/ _ \/ _ \/ _` |
5 | | |/ / __/\ V / __/ | (_) | |_) | | | | | | __/ | | | |_ /\__/ / __/ __/ (_| |
6 | |___/ \___| \_/ \___|_|\___/| .__/|_| |_| |_|\___|_| |_|\__| \____/ \___|\___|\__,_|
7 | | |
8 | |_|
9 | ====================================================================================
10 |
11 |
12 | /* Team */
13 | Ali Felskia
14 | Derek Lieu
15 | Drew Bollinger
16 | Marc Farra
17 |
--------------------------------------------------------------------------------
/app/graphics/layout/xmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/graphics/layout/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/scripts/reducers/map.js:
--------------------------------------------------------------------------------
1 | import { COMPLETE_MAP_UPDATE, UPDATE_MAP_DATA, REQUEST_TILE, TOGGLE_EXISTING_ROADS } from '../actions';
2 |
3 | const initial = {
4 | tempStore: null,
5 | requestedTiles: new Set(),
6 | showExistingRoads: false
7 | };
8 |
9 | const map = (state = initial, action) => {
10 | switch (action.type) {
11 | case COMPLETE_MAP_UPDATE:
12 | return Object.assign({}, state, { tempStore: null });
13 | case UPDATE_MAP_DATA:
14 | return Object.assign({}, state, { tempStore: action.data });
15 | case TOGGLE_EXISTING_ROADS:
16 | return Object.assign({}, state, { showExistingRoads: !state.showExistingRoads });
17 | case REQUEST_TILE:
18 | return Object.assign({}, state, {
19 | requestedTiles: new Set(state.requestedTiles).add(action.data, true)
20 | });
21 | default:
22 | return state;
23 | }
24 | };
25 |
26 | export default map;
27 |
--------------------------------------------------------------------------------
/app/graphics/layout/arrow-left--white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/styles/_typography.scss:
--------------------------------------------------------------------------------
1 | h1, h2, h3, h4, h5, h6 {
2 | color: $dark-grey;
3 | font-family: $base-font-family;
4 | font-weight: $base-font-bold;
5 | margin-bottom: .5em;
6 | }
7 |
8 | .heading--xxlarge {
9 | font-size: 3.2em;
10 | font-weight: $base-font-bold;
11 | }
12 |
13 | .heading--xlarge {
14 | font-size: 3.2em;
15 | font-weight: $base-font-bold;
16 | }
17 |
18 | .heading--large {
19 | font-size: 2.3em;
20 | font-weight: $base-font-regular;
21 | }
22 |
23 | .heading--medium {
24 | font-size: 1.58em;
25 | font-weight: $base-font-bold;
26 | }
27 |
28 | .heading--small {
29 | font-size: 1.28em;
30 | font-weight: $base-font-bold;
31 | }
32 |
33 | .heading--shared-content {
34 | display: inline-block;
35 | }
36 |
37 | .heading__wrapper--border {
38 | border-bottom: 1px solid $lightest-grey;
39 | padding-bottom: .3em;
40 | margin-bottom: 1.2em;
41 | }
42 |
43 | .with-description {
44 | margin-bottom: .3em;
45 | }
--------------------------------------------------------------------------------
/app/graphics/layout/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard"],
3 | "env": {
4 | "es6": true,
5 | "browser": true
6 | },
7 | "plugins": [
8 | "react"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 6,
12 | "ecmaFeatures": {
13 | "jsx": true
14 | }
15 | },
16 | "rules": {
17 | "semi": [2, "always"],
18 | "no-extra-semi": 2,
19 | "semi-spacing": [2, { "before": false, "after": true }],
20 | "react/jsx-no-duplicate-props": 2,
21 | "react/jsx-no-undef": 2,
22 | "react/jsx-uses-react": 2,
23 | "react/jsx-uses-vars": 2,
24 | "react/no-danger": 0,
25 | "react/no-deprecated": 2,
26 | "react/no-did-mount-set-state": 2,
27 | "react/no-did-update-set-state": 2,
28 | "react/no-direct-mutation-state": 2,
29 | "react/no-is-mounted": 2,
30 | "react/no-unknown-property": 2,
31 | "react/prop-types": 2,
32 | "react/react-in-jsx-scope": 2,
33 | "brace-style": 0
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/styles/_base.scss:
--------------------------------------------------------------------------------
1 | // IE < 10
2 | // Don't delete.
3 | .lt-ie10 {
4 | #site-canvas {
5 | display: none;
6 | }
7 | #nocando {
8 | margin: 200px auto;
9 | padding: 32px;
10 | max-width: 440px;
11 | background: #fff;
12 | h1 {
13 | font-size: 24px;
14 | line-height: 1.1;
15 | margin-bottom: 32px;
16 | }
17 | }
18 | }
19 |
20 | html,
21 | body {
22 | height: 100%;
23 | overflow: hidden;
24 | font-family: $base-font-family;
25 | font-weight: $base-font-regular;
26 | letter-spacing: .4px;
27 | font-size: 14px;
28 | }
29 |
30 | button {
31 | background: transparent;
32 | border: none;
33 | text-align: left;
34 | padding: 0;
35 | }
36 |
37 | .row {
38 | @extend .clearfix;
39 | max-width: 1280px;
40 | padding: 0 64px;
41 | }
42 |
43 | .full, .main, .app {
44 | height: inherit;
45 | }
46 |
47 | .header, .footer {
48 | position: absolute;
49 | }
50 |
51 | .header {
52 | top: 0;
53 | height: 1em;
54 | }
55 |
56 | .footer {
57 | bottom: 0;
58 | height: 1em;
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skynet scrub
2 |
3 | Contains code and unit tests for the Skynet Scrubber, a GUI for cleaning machine learning output data.
4 |
5 | # Installation
6 |
7 | Requires node v6.9. We recommend using [nvm](https://github.com/creationix/nvm) to manage node versions.
8 |
9 | ```(bash)
10 | nvm use
11 | npm install
12 | npm run start
13 | ```
14 |
15 | # Development
16 |
17 | ## On accessing global `window` methods
18 |
19 | Currently we use [tape](https://github.com/substack/tape) for unit tests. Tape is light-weight with a simple API. One consequence is that, unlike other runners that bundle Phantomjs (mocha, etc), tape does not include a browser environment to run client-side code.
20 |
21 | This isn't a problem as long as we import the native globals we need, ie `localStorage`, `navigator`, `document`, etc. from `app/scripts/util/window`. We use [js-dom](https://github.com/tmpvar/jsdom) to stub `window` and `document`, and can use [mock-browser](https://github.com/darrylwest/mock-browser) for methods that js-dom doesn't support, like storage.
22 |
23 | ## On testing and linting
24 |
25 | ```(javascript)
26 | npm run test
27 | ```
28 |
--------------------------------------------------------------------------------
/app/scripts/reducers/draw.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_DRAW_MODE, TOGGLE_VISIBILITY } from '../actions';
2 | import { COMPLETE, INCOMPLETE, EDITED, INACTIVE } from '../components/map/utils/constants';
3 | import { initialZoom, minTileZoom } from '../config/';
4 |
5 | const initial = {
6 | mode: initialZoom < minTileZoom ? INACTIVE : null,
7 | hidden: initialZoom < minTileZoom ? [COMPLETE, INCOMPLETE, EDITED] : []
8 | };
9 |
10 | const draw = (state = initial, action) => {
11 | switch (action.type) {
12 | case CHANGE_DRAW_MODE:
13 | return Object.assign({}, state, { mode: action.data });
14 | case TOGGLE_VISIBILITY:
15 | const status = action.data;
16 |
17 | if (status === 'all') {
18 | state.hidden = state.hidden.length ? [] : [COMPLETE, INCOMPLETE, EDITED];
19 | return Object.assign({}, state);
20 | } else if (state.hidden.indexOf(status) > -1) {
21 | state.hidden.splice(state.hidden.indexOf(status), 1);
22 | } else {
23 | state.hidden.push(status);
24 | }
25 | return Object.assign({}, state);
26 | default:
27 | return state;
28 | }
29 | };
30 |
31 | export default draw;
32 |
--------------------------------------------------------------------------------
/app/scripts/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import { Provider } from 'react-redux';
6 | import thunkMiddleware from 'redux-thunk';
7 | import { useScroll } from 'react-router-scroll';
8 | import createLogger from 'redux-logger';
9 | import { Router, Route, IndexRoute, hashHistory, applyRouterMiddleware } from 'react-router';
10 |
11 | import config from './config';
12 | import reducers from './reducers';
13 |
14 | import App from './components/app';
15 | import NotFound from './components/app/not-found';
16 | import Home from './components/home';
17 |
18 | const logger = createLogger({
19 | level: 'info',
20 | collapsed: true,
21 | predicate: (getState, action) => {
22 | return (config.environment !== 'production');
23 | }
24 | });
25 |
26 | const store = createStore(reducers, applyMiddleware(
27 | thunkMiddleware,
28 | logger
29 | ));
30 |
31 | console.log.apply(console, config.consoleMessage);
32 | console.log('Environment', config.environment);
33 |
34 | render((
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ), document.getElementById('site-canvas'));
44 |
--------------------------------------------------------------------------------
/app/styles/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal__cover {
2 | background-color: #999;
3 | opacity: 0.85;
4 | position: fixed;
5 | top: 0;
6 | right: 0;
7 | bottom: 0;
8 | left: 0;
9 | z-index: 200;
10 | &:after {
11 | -moz-box-shadow: inset 0 0 10em #000;
12 | -webkit-box-shadow: inset 0 0 10em #000;
13 | box-shadow: inset 0 0 10em #000;
14 | position: fixed;
15 | top: 0;
16 | left: 0;
17 | width: 100%;
18 | height: 100%;
19 | z-index: 2;
20 | content: "";
21 | }
22 | }
23 |
24 | .modal {
25 | position: fixed;
26 | text-align: center;
27 | top: 8em;
28 | width: 100%;
29 | z-index: 201;
30 | }
31 |
32 | .modal__inner {
33 | display: inline-block;
34 | background-color: white;
35 | text-align: left;
36 | padding: 3.2em 2.8em 3.5em;
37 | -webkit-border-radius: 4px;
38 | -moz-border-radius: 4px;
39 | border-radius: 4px;
40 | position: relative;
41 | p {
42 | color: $grey;
43 | margin-top: .8em;
44 | }
45 | }
46 |
47 | .icon-close {
48 | content: '';
49 | width: 10px;
50 | height: 10px;
51 | background-image: url(../graphics/layout/xmark.svg);
52 | background-size: 10px 10px;
53 | display: block;
54 | position: absolute;
55 | right: 1.8em;
56 | top: 1.5em;
57 | cursor: pointer;
58 | }
59 |
60 | .modal__files-changed {
61 | font-size: .86em;
62 | margin: 1.8em 0 3em;
63 | list-style-type: circle;
64 | padding-left: 1.5em;
65 | color: $base-grey;
66 | li {
67 | margin-bottom: .5em;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/styles/main.scss:
--------------------------------------------------------------------------------
1 | /* ==========================================================================
2 | Main
3 | ========================================================================== */
4 |
5 | /**
6 | * Following Necolas' Principles of writing consistent, idiomatic CSS:
7 | * https://github.com/necolas/idiomatic-css
8 | */
9 |
10 | /* Charset */
11 |
12 | @charset "UTF-8";
13 |
14 |
15 | /* Libraries
16 | ========================================================================== */
17 |
18 | /**
19 | * Using Bourbon for extended SASS functionality:
20 | * http://bourbon.io/
21 | */
22 |
23 | @import "bourbon";
24 |
25 | /* Reboot
26 | ========================================================================== */
27 |
28 | @import "normalize";
29 | @import "reset";
30 |
31 | /**
32 | * Using Kenneth Ormandy's OpenType features—ligatures, kerning, and
33 | * more—to Normalize.css:
34 | * https://github.com/kennethormandy/normalize-opentype.css
35 | */
36 |
37 | @import "normalize-opentype";
38 |
39 | /* Fonts
40 | ========================================================================== */
41 |
42 | @import url('https://fonts.googleapis.com/css?family=Karla:400,400i,700');
43 |
44 | /* Settings
45 | ========================================================================== */
46 | @import "settings/variables";
47 |
48 | /* Main
49 | ========================================================================== */
50 |
51 | @import "typography";
52 | @import "base";
53 | @import "map";
54 | @import "modal";
55 | @import "utils";
56 |
--------------------------------------------------------------------------------
/test/actions.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import nock from 'nock';
3 | import thunk from 'redux-thunk';
4 | import configureMockStore from 'redux-mock-store';
5 |
6 | import { baseUrl } from '../app/scripts/config/base';
7 | import { save } from '../app/scripts/actions';
8 |
9 | const middlewares = [thunk];
10 | const mockStore = configureMockStore(middlewares);
11 |
12 | test('save', function (t) {
13 | t.test('creates SAVE actions', function (st) {
14 | nock(baseUrl)
15 | .post('/commit')
16 | .reply(200, { body: 'ok' });
17 |
18 | const store = mockStore({});
19 | const selectionArray = [{
20 | historyId: 'initial',
21 | selection: [
22 | {
23 | id: '1',
24 | redo: {
25 | id: '',
26 | type: 'Feature',
27 | geometry: {},
28 | properties: { status: 'incomplete' }
29 | },
30 | undo: {
31 | id: '',
32 | type: 'Feature',
33 | geometry: {},
34 | properties: { status: 'edited' }
35 | }
36 | }
37 | ]
38 | }];
39 |
40 | const expectedActions = JSON.stringify([
41 | { type: 'SAVE', data: { inflight: true, error: null } },
42 | { type: 'SAVE', data: { historyId: 'initial', inflight: false, error: null, success: true } }
43 | ]);
44 |
45 | return store.dispatch(save(selectionArray)).then(() => {
46 | t.equal(JSON.stringify(store.getActions()), expectedActions);
47 | st.end();
48 | });
49 | });
50 |
51 | nock.cleanAll();
52 | t.end();
53 | });
54 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Skynet Scrub
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/styles/_reset.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Reset ==============================================================
3 | * Based on http://meyerweb.com/eric/tools/css/reset
4 | */
5 |
6 | html,
7 | body,
8 |
9 | /* Structures */
10 | div,
11 | span,
12 | applet,
13 | object,
14 | iframe,
15 |
16 | /* Text */
17 | h1,
18 | h2,
19 | h3,
20 | h4,
21 | h5,
22 | h6,
23 | p,
24 | blockquote,
25 | pre,
26 | a,
27 | abbr,
28 | acronym,
29 | address,
30 | big,
31 | cite,
32 | code,
33 | del,
34 | dfn,
35 | em,
36 | font,
37 | img,
38 | ins,
39 | kbd,
40 | q,
41 | s,
42 | samp,
43 | small,
44 | strike,
45 | strong,
46 | sub,
47 | sup,
48 | tt,
49 | var,
50 | b,
51 | u,
52 | i,
53 | center,
54 |
55 | /* Lists */
56 | dl,
57 | dt,
58 | dd,
59 | ol,
60 | ul,
61 | li,
62 |
63 | /* Forms */
64 | fieldset,
65 | form,
66 | input,
67 | select,
68 | textarea,
69 | label,
70 | legend,
71 |
72 | /* Tables */
73 | table,
74 | caption,
75 | tbody,
76 | tfoot,
77 | thead,
78 | tr,
79 | th,
80 | td {
81 | margin:0px;
82 | padding:0px;
83 | border:0px;
84 | outline:0px;
85 | font-size:100%;
86 | vertical-align:baseline;
87 | background:transparent;
88 | line-height:inherit;
89 | }
90 |
91 | ol,
92 | ul,
93 | .item-list ul,
94 | .item-list ul li {
95 | list-style:none;
96 | }
97 |
98 | blockquote,
99 | q { quotes:none; }
100 |
101 | blockquote:before,
102 | blockquote:after,
103 | q:before, q:after {
104 | content:'';
105 | content:none;
106 | }
107 |
108 | /* remember to define focus styles! */
109 | :focus { outline:0px; }
110 |
111 | /* remember to highlight inserts somehow! */
112 | ins { text-decoration:none; }
113 | del { text-decoration:line-through; }
--------------------------------------------------------------------------------
/app/scripts/reducers/selection.js:
--------------------------------------------------------------------------------
1 | import hat from 'hat';
2 | import { UNDO, REDO, COMPLETE_UNDO, COMPLETE_REDO, UPDATE_SELECTION, FAST_FORWARD } from '../actions';
3 |
4 | const initial = {
5 | past: [],
6 | present: { historyId: 'initial', selection: [] },
7 | future: []
8 | };
9 |
10 | const rack = hat.rack();
11 |
12 | const selection = (state = initial, action) => {
13 | const { past, present, future } = state;
14 | switch (action.type) {
15 | case UNDO:
16 | const previous = past[past.length - 1];
17 | const newPast = past.slice(0, past.length - 1);
18 | return {
19 | past: newPast,
20 | present: { selection: previous.selection, historyId: 'undo' },
21 | future
22 | };
23 | case REDO:
24 | const next = future[0];
25 | const newFuture = future.slice(1);
26 | return {
27 | past,
28 | present: { selection: next.selection, historyId: 'redo' },
29 | future: newFuture
30 | };
31 | case UPDATE_SELECTION:
32 | return {
33 | past: [ ...past, { historyId: present.historyId, selection: action.data } ],
34 | present: { historyId: rack(), selection: [] },
35 | future: []
36 | };
37 | case COMPLETE_UNDO:
38 | return {
39 | past,
40 | present: { historyId: rack(), selection: [] },
41 | future: [ present, ...future ]
42 | };
43 | case COMPLETE_REDO:
44 | return {
45 | past: [ ...past, present ],
46 | present: { historyId: rack(), selection: [] },
47 | future
48 | };
49 | case FAST_FORWARD:
50 | return {
51 | past,
52 | present: { selection: action.data, historyId: 'redo' },
53 | future: []
54 | };
55 | default:
56 | return state;
57 | }
58 | };
59 |
60 | export default selection;
61 |
--------------------------------------------------------------------------------
/app/scripts/util/compress-changes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import findIndex from 'lodash.findindex';
3 |
4 | // compress a series of changes into a single change per id
5 | export function compressChanges (selectionArray, lastHistoryId) {
6 | let index = 0;
7 | if (typeof lastHistoryId !== 'undefined') {
8 | index = findIndex(selectionArray, d => d.historyId === lastHistoryId);
9 | index = index === -1 ? 0 : index + 1;
10 | }
11 | const compressed = {};
12 | for (let i = index; i < selectionArray.length; ++i) {
13 | const selectionObj = selectionArray[i];
14 | const { selection } = selectionObj;
15 | selection.forEach(action => {
16 | const { id, redo } = action;
17 | if (!compressed[id]) {
18 | // if the action is 'new' for this ID, save a copy
19 | compressed[id] = Object.assign({}, action);
20 | } else {
21 | compressed[id].redo = redo;
22 | }
23 | });
24 | }
25 | return values(compressed).filter(action => action.undo || action.redo);
26 | }
27 |
28 | // compress a series of changes from the store into a smaller format
29 | export default function compress (selectionArray, lastHistoryId) {
30 | const compressed = compressChanges(selectionArray, lastHistoryId);
31 | const deleted = {};
32 | const edited = {};
33 | const created = {};
34 | compressed.forEach(action => {
35 | const { id, undo, redo } = action;
36 | // blank action, was created then deleted
37 | if (!undo && !redo) return;
38 | else if (!redo) deleted[id] = 1;
39 | else if (!undo) created[id] = redo;
40 | else edited[id] = redo;
41 | });
42 | return {
43 | deleted: Object.keys(deleted),
44 | edited: values(edited).concat(values(created))
45 | };
46 | }
47 |
48 | function values (obj) {
49 | return Object.keys(obj).map(key => obj[key]).filter(Boolean);
50 | }
51 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-visibility.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-line.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-visibility2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/map.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React from 'react';
3 | import test from 'tape';
4 | import mock from 'mapbox-gl-js-mock';
5 | import proxyquire from 'proxyquire';
6 | import { mount } from 'enzyme';
7 | import App from '../app/scripts/util/app';
8 | App.glSupport = true;
9 |
10 | // stub global DOM methods
11 | import jsdom from 'jsdom';
12 | const doc = jsdom.jsdom('');
13 | global.window = doc.defaultView;
14 | global.document = doc;
15 |
16 | // stub mapbox draw
17 | const draw = () => true;
18 | draw.prototype.add = () => true;
19 | draw.prototype.onAdd = () => true;
20 | draw.prototype.getSelected = () => ({ features: [{ properties: {} }] });
21 | draw.prototype.getSelectedPoints = () => ({ features: [{ properties: {} }] });
22 | draw.prototype.getMode = () => 'simple_select';
23 | draw.prototype.changeMode = () => true;
24 |
25 | const { Map } = proxyquire.noCallThru().load('../app/scripts/components/map', {
26 | '@mapbox/mapbox-gl-draw': draw,
27 | 'mapbox-gl': mock,
28 | '../../util/app': App
29 | });
30 |
31 | function setup (options) {
32 | options = options || {};
33 | const props = Object.assign({
34 | selection: {
35 | past: [],
36 | present: { historyId: 'initial' },
37 | future: []
38 | },
39 | map: {
40 | showExistingRoads: false
41 | },
42 | draw: {
43 | mode: null,
44 | hidden: []
45 | },
46 | save: {}
47 | }, options);
48 | const map = mount();
49 |
50 | return {
51 | props,
52 | map
53 | };
54 | }
55 |
56 | test('map', function (t) {
57 | const args = [];
58 | setup({ dispatch: (d) => args.push(d) });
59 | const event = {
60 | features: [
61 | {id: 1, type: 'Feature', geometry: {type: 'LineString', coordinates: [[1, 1], [2, 2]]}, properties: {}},
62 | {id: 2, type: 'Feature', geometry: {type: 'LineString', coordinates: [[1, 1], [2, 2]]}, properties: {}},
63 | {id: 3, type: 'Feature', geometry: {type: 'LineString', coordinates: [[1, 1], [2, 2]]}, properties: {}}
64 | ]
65 | };
66 |
67 | App.map.fire('draw.create', event);
68 | App.map.fire('draw.delete', event);
69 | App.map.fire('draw.selectionchange', { features: [{id: 1, type: 'LineString', geometry: {}, properties: {}}] });
70 | App.map.fire('draw.update', event);
71 |
72 | args.forEach(d => t.ok(d.type === 'UPDATE_SELECTION' || d.type === 'CHANGE_DRAW_MODE'));
73 | t.equals(args.length, 5);
74 | t.end();
75 | });
76 |
--------------------------------------------------------------------------------
/test/compress-changes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import test from 'tape';
3 | import compress, { compressChanges } from '../app/scripts/util/compress-changes';
4 |
5 | test('compression', function (t) {
6 | // modification, then deletion
7 | let payload = compress([
8 | {selection: [{id: 1, redo: 1, undo: 1}]},
9 | {selection: [{id: 1, redo: 1, undo: 1}]},
10 | {selection: [{id: 1, redo: 1, undo: 1}]},
11 | {selection: [{id: 1, redo: 1, undo: 1}]},
12 | {selection: [{id: 1, redo: 1, undo: 1}]},
13 | {selection: [{id: 1, redo: 1, undo: 1}]},
14 | {selection: [{id: 1, redo: 0, undo: 1}]}
15 | ]);
16 | t.equal(payload.deleted.length, 1);
17 | t.equal(payload.edited.length, 0);
18 |
19 | // creation, then deletion
20 | payload = compress([
21 | {selection: [{id: 1, redo: 1, undo: 0}]},
22 | {selection: [{id: 1, redo: 0, undo: 1}]}
23 | ]);
24 | t.equal(payload.deleted.length, 0);
25 | t.equal(payload.edited.length, 0);
26 |
27 | // creation, then modification
28 | payload = compress([
29 | {selection: [{id: 1, redo: 1, undo: 0}]},
30 | {selection: [{id: 1, redo: 2, undo: 1}]}
31 | ]);
32 | t.equal(payload.deleted.length, 0);
33 | t.equal(payload.edited.length, 1);
34 | t.equal(payload.edited[0], 2);
35 |
36 | // creation, modification, then deletion (but starting from after the creation)
37 | // without the historyId param, this would produce an empty payload
38 | payload = compress([
39 | {historyId: 0, selection: [{id: 1, redo: 1, undo: 0}]},
40 | {historyId: 1, selection: [{id: 1, redo: 2, undo: 1}]},
41 | {historyId: 2, selection: [{id: 1, redo: 0, undo: 1}]}
42 | ], 0);
43 | t.equal(payload.deleted.length, 1);
44 | t.equal(payload.edited.length, 0);
45 |
46 | t.end();
47 | });
48 |
49 | test('compress changes', function (t) {
50 | const selectionArray = [{
51 | historyId: 0,
52 | selection: [{ id: 'a', undo: 0, redo: 1 }, { id: 'b', undo: 1, redo: 2 }] // create a, modify b
53 | }, {
54 | historyId: 1,
55 | selection: [{ id: 'a', undo: 1, redo: 2 }] // modify a
56 | }, {
57 | historyId: 2,
58 | selection: [{ id: 'b', undo: 2, redo: 3 }] // modify b
59 | }, {
60 | historyId: 3,
61 | selection: [{ id: 'a', undo: 2, redo: 0 }, { id: 'b', undo: 3, redo: 0 }, { id: 'c', undo: 0, redo: 1 }] // delete a, delete b, create c
62 | }];
63 | let payload = compressChanges(selectionArray);
64 | t.equals(payload.find(d => d.id === 'a'), undefined); // a is created and deleted in the same change
65 | t.deepEquals(payload.find(d => d.id === 'b'), { id: 'b', undo: 1, redo: 0 });
66 | t.deepEquals(payload.find(d => d.id === 'c'), { id: 'c', undo: 0, redo: 1 });
67 |
68 | payload = compressChanges(selectionArray, 1);
69 | t.deepEquals(payload.find(d => d.id === 'a'), { id: 'a', undo: 2, redo: 0 });
70 | t.deepEquals(payload.find(d => d.id === 'b'), { id: 'b', undo: 2, redo: 0 });
71 | t.deepEquals(payload.find(d => d.id === 'c'), { id: 'c', undo: 0, redo: 1 });
72 | t.end();
73 | });
74 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-addline.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/scripts/components/auto-save.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import * as autosave from '../util/auto-save';
5 | import { compressChanges } from '../util/compress-changes';
6 | import { fastForward, updateLocalStore } from '../actions';
7 |
8 | export const AutoSave = React.createClass({
9 |
10 | componentWillMount: function () {
11 | const unsaved = autosave.getLocalActions();
12 | if (unsaved) {
13 | this.props.dispatch(updateLocalStore(unsaved));
14 | }
15 | },
16 |
17 | componentWillReceiveProps: function (newProps) {
18 | const { historyId, success } = newProps.save;
19 | const { past } = newProps.selection;
20 | if (success && autosave.getLocalActions()) {
21 | // on save success, if there's any stored items, remove them.
22 | this.forget();
23 | } else if (past.length && historyId !== past[past.length - 1].historyId) {
24 | // if we have unsaved changes, persist them to localStorage.
25 | this.store(past, historyId);
26 | }
27 | },
28 |
29 | store: function (past, historyId) {
30 | // compress the past selection array as a single action.
31 | const compressed = historyId ? compressChanges(past, historyId) : compressChanges(past);
32 | autosave.saveLocalActions(compressed);
33 | },
34 |
35 | restore: function () {
36 | const { cached } = this.props.save;
37 | this.props.dispatch(fastForward(cached));
38 | this.forget();
39 | },
40 |
41 | forget: function () {
42 | this.props.dispatch(updateLocalStore(null));
43 | autosave.destroyLocalActions();
44 | },
45 |
46 | describeAction: function (action) {
47 | if (action.undo && action.redo) return 'Modified';
48 | else if (action.undo) return 'Deleted';
49 | else return 'Created';
50 | },
51 |
52 | renderCached: function (cached) {
53 | const items = cached.map(action => (
54 | {this.describeAction(action)}: {action.id}
55 | ));
56 | return (
57 |
58 |
59 |
Save Your Changes
60 |
By leaving this page all changes below will be deleted.
61 |
62 |
63 |
64 |
65 | );
66 | },
67 |
68 | render: function () {
69 | const { cached } = this.props.save;
70 | return (
71 |
72 | { cached ?
: null }
73 | { cached ? (
74 |
75 |
76 | {this.renderCached(cached)}
77 |
78 |
79 | ) : null }
80 |
81 | );
82 | },
83 |
84 | propTypes: {
85 | dispatch: React.PropTypes.func,
86 | selection: React.PropTypes.object,
87 | save: React.PropTypes.object
88 | }
89 | });
90 |
91 | function mapStateToProps (state) {
92 | return {
93 | selection: state.selection,
94 | save: state.save
95 | };
96 | }
97 |
98 | export default connect(mapStateToProps)(AutoSave);
99 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skynet-scrub",
3 | "version": "1.0.0",
4 | "description": "A GUI for cleaning ML output",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/developmentseed/skynet-scrub"
8 | },
9 | "author": {
10 | "name": "Development Seed",
11 | "url": "https://developmentseed.org"
12 | },
13 | "license": "BSD",
14 | "bugs": {
15 | "url": "https://github.com/developmentseed/skynet-scrub/issues"
16 | },
17 | "homepage": "https://github.com/developmentseed/skynet-scrub",
18 | "scripts": {
19 | "serve": "DS_ENV=development gulp serve",
20 | "build": "NODE_ENV=production gulp",
21 | "lint": "./node_modules/.bin/eslint app/scripts/ test/ --ext .js",
22 | "start": "npm run serve",
23 | "test": "npm run lint; ./node_modules/.bin/tape -r babel-register -r babel-polyfill test/**/*.js"
24 | },
25 | "browserify": {
26 | "transform": [
27 | [
28 | "babelify",
29 | {
30 | "presets": [
31 | "es2015",
32 | "react"
33 | ]
34 | }
35 | ],
36 | "require-globify",
37 | "envify"
38 | ]
39 | },
40 | "devDependencies": {
41 | "babel": "^6.5.2",
42 | "babel-polyfill": "^6.23.0",
43 | "babel-preset-es2015": "^6.6.0",
44 | "babel-preset-react": "^6.5.0",
45 | "babel-register": "^6.23.0",
46 | "babelify": "^7.3.0",
47 | "browser-sync": "^2.12.5",
48 | "browserify": "^13.0.1",
49 | "del": "^2.2.0",
50 | "envify": "^3.4.0",
51 | "enzyme": "^2.7.1",
52 | "eslint": "^2.9.0",
53 | "eslint-config-standard": "^5.3.1",
54 | "eslint-plugin-promise": "^1.1.0",
55 | "eslint-plugin-react": "^6.9.0",
56 | "eslint-plugin-standard": "^1.3.2",
57 | "geojson-random": "git://github.com/developmentseed/geojson-random.git#d1a40194491aacc9c1f08533b0acd1cd7cc560aa",
58 | "gulp": "^3.9.1",
59 | "gulp-cache": "^0.4.4",
60 | "gulp-csso": "^2.0.0",
61 | "gulp-exit": "0.0.2",
62 | "gulp-if": "^2.0.1",
63 | "gulp-imagemin": "^3.0.1",
64 | "gulp-load-plugins": "^1.2.2",
65 | "gulp-plumber": "^1.1.0",
66 | "gulp-rev": "^7.0.0",
67 | "gulp-rev-replace": "^0.4.3",
68 | "gulp-sass": "^2.3.1",
69 | "gulp-size": "^2.1.0",
70 | "gulp-sourcemaps": "^1.6.0",
71 | "gulp-uglify": "^2.1.2",
72 | "gulp-useref": "^3.1.0",
73 | "gulp-util": "^3.0.7",
74 | "jsdom": "^9.12.0",
75 | "mapbox-gl-js-mock": "^0.28.0",
76 | "nock": "^9.0.9",
77 | "node-bourbon": "^4.2.8",
78 | "node-notifier": "^4.5.0",
79 | "node-sass": "^3.7.0",
80 | "proxyquire": "^1.7.11",
81 | "react-addons-test-utils": "^15.4.2",
82 | "redux-mock-store": "^1.2.2",
83 | "require-globify": "^1.3.0",
84 | "sinon": "^1.17.7",
85 | "surge": "^0.19.0",
86 | "tape": "^4.6.3",
87 | "vinyl-buffer": "^1.0.0",
88 | "vinyl-source-stream": "^1.1.0",
89 | "watchify": "^3.7.0"
90 | },
91 | "dependencies": {
92 | "@mapbox/mapbox-gl-draw": "^0.18.0",
93 | "@turf/helpers": "^3.10.5",
94 | "@turf/line-slice": "^3.11.7",
95 | "@turf/meta": "^4.0.0",
96 | "@turf/nearest": "^4.0.0",
97 | "classnames": "^2.2.5",
98 | "geojson-linestring-dissolve": "0.0.1",
99 | "hat": "0.0.3",
100 | "isomorphic-fetch": "^2.2.1",
101 | "lodash.findindex": "^4.6.0",
102 | "lodash.isempty": "^4.4.0",
103 | "lodash.uniq": "^4.5.0",
104 | "mapbox-gl": "0.32.1",
105 | "mapbox-gl-supported": "^1.2.0",
106 | "object-path": "^0.11.4",
107 | "react": "^15.4.2",
108 | "react-dom": "^15.4.2",
109 | "react-redux": "^5.0.2",
110 | "react-router": "^3.0.2",
111 | "react-router-scroll": "^0.4.1",
112 | "redux": "^3.6.0",
113 | "redux-logger": "^2.8.1",
114 | "redux-thunk": "^2.2.0",
115 | "store2": "^2.5.0",
116 | "tile-cover": "^3.0.1",
117 | "turf-bbox-polygon": "^3.0.12",
118 | "turf-distance": "^3.0.12"
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/graphics/layout/icon-cut.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/scripts/actions/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch';
2 | import config from '../config';
3 | import compress from '../util/compress-changes';
4 | import isEmpty from 'lodash.isempty';
5 |
6 | export const UPDATE_SELECTION = 'UPDATE_SELECTION';
7 | export const UNDO = 'UNDO';
8 | export const REDO = 'REDO';
9 | export const SAVE = 'SAVE';
10 | export const COMPLETE_UNDO = 'COMPLETE_UNDO';
11 | export const COMPLETE_REDO = 'COMPLETE_REDO';
12 | export const COMPLETE_MAP_UPDATE = 'COMPLETE_MAP_UPDATE';
13 | export const UPDATE_MAP_DATA = 'UPDATE_MAP_DATA';
14 | export const REQUEST_TILE = 'REQUEST_TILE';
15 | export const CHANGE_DRAW_MODE = 'CHANGE_DRAW_MODE';
16 | export const LOCAL_STORAGE = 'LOCAL_STORAGE';
17 | export const FAST_FORWARD = 'FAST_FORWARD';
18 | export const TOGGLE_VISIBILITY = 'TOGGLE_VISIBILITY';
19 | export const TOGGLE_EXISTING_ROADS = 'TOGGLE_EXISTING_ROADS';
20 |
21 | /**
22 | * Updates the selection store with a new array of changes
23 | * @param {Array