Add comparison app

This commit is contained in:
Alfred Melch 2019-07-21 12:55:28 +02:00
parent 3e51f7f57b
commit 549ba95426
19 changed files with 11079 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"presets": ["@babel/preset-react"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}

10270
compare-algorithms/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
{
"name": "compare-algorithms",
"version": "0.1.0",
"description": "",
"main": "src/index.js",
"scripts": {
"build": "webpack --mode development",
"serve": "webpack serve",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "prettier --check '**/*.js'",
"format": "prettier --write '**/*.js'"
},
"author": "Alfred Melch (dev@melch.pro)",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@webpack-cli/serve": "^0.1.8",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.0.3",
"css-loader": "^3.1.0",
"file-loader": "^4.1.0",
"prettier": "^1.18.2",
"react-hot-loader": "^4.12.8",
"style-loader": "^0.23.1",
"url-loader": "^2.1.0",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2"
},
"dependencies": {
"computed-async-mobx": "^4.1.1",
"file-drop-element": "^0.2.0",
"leaflet": "^1.5.1",
"mobx": "^5.13.0",
"mobx-react": "^6.1.1",
"mobx-utils": "^5.4.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-leaflet": "^2.4.0"
}
}

View File

@ -0,0 +1,17 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header><h1>Simplify GeoJSON</h1></header>
<div id="map"></div>
<main id="root"></main>
<footer>Footer</footer>
<script src="./bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,71 @@
html,
body {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
flex-wrap: wrap;
}
.leaflet-container {
flex: 2;
min-width: 300px;
min-height: 60vh;
}
#options {
flex: 1;
min-width: 300px;
}
file-drop {
position: relative;
box-sizing: border-box;
display: block;
padding: 10px;
background-color: rgba(245, 234, 174, 0.2);
overflow: hidden;
touch-action: none;
}
file-drop::after {
content: '';
position: absolute;
display: block;
left: 2px;
top: 2px;
right: 2px;
bottom: 2px;
border: 2px dashed #fff;
background-color: rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5);
border-radius: 10px;
opacity: 0;
transform: scale(0.95);
transition: all 200ms ease-in;
transition-property: transform, opacity;
pointer-events: none;
}
.drop-valid::after {
opacity: 1;
transform: scale(1);
transition-timing-function: ease-out;
}
.drop-invalid::after {
background-color: rgba(255, 27, 27, 0.2);
border-color: rgba(255, 170, 170, 0.5);
opacity: 1;
transform: scale(1);
transition-timing-function: ease-out;
}

View File

@ -0,0 +1,60 @@
import { computed } from 'mobx'
export class Algorithm {
constructor(name, id, method) {
this.name = name
this.id = id
this.method = method
this.fields = []
}
addField(fieldDesc) {
this.fields.push(fieldDesc)
}
@computed
get params() {
this.fields.map(f => f.value)
}
async simplifyLine(coords, params) {
return this.method(coords, ...params)
}
async simplifyPolygon(coords, params) {
return await Promise.all(
coords.map(ring => this.simplifyLine(ring, params))
)
}
async simplifyGeoJSON(geojson, params) {
// clone geojson
let simplified = JSON.parse(JSON.stringify(geojson))
for (let feature of simplified.features) {
let geom = feature.geometry
let coords = geom.coordinates
switch (geom.type) {
case 'Point':
case 'MultiPoint':
break
case 'LineString':
geom['coordinates'] = await this.simplifyLine(coords, params)
break
case 'MultiLineString':
geom['coordinates'] = await Promise.all(
coords.map(line => this.simplifyLine(line, params))
)
break
case 'Polygon':
geom['coordinates'] = await this.simplifyPolygon(coords, params)
break
case 'MultiPolygon':
geom['coordinates'] = await Promise.all(
coords.map(poly => this.simplifyPolygon(poly, params))
)
}
}
return simplified
}
simplifyTopoJSON(data, params) {}
}

View File

@ -0,0 +1,32 @@
import { observable } from 'mobx'
class Field {
@observable
value
constructor(name, id, type, props, initialValue) {
this.name = name
this.id = id
this.type = type
this.props = props || {}
this.value = initialValue
}
}
export class Range extends Field {
constructor(name, id, min, max, step, value) {
super(name, id, 'range', { min, max, step }, value)
}
}
export class Checkbox extends Field {
constructor(name, id, checked) {
super(name, id, 'checkbox', {}, checked)
}
}
export class ToleranceRange extends Range {
constructor(name = 'Tolerance', id = 'tol') {
super(name, id, 0.01, 1, 0.01, 0.2)
}
}

View File

@ -0,0 +1,115 @@
import {
simplify_nth_point,
simplify_radial_distance,
simplify_perpendicular_distance,
simplify_reumann_witkam,
simplify_opheim,
simplify_lang,
simplify_douglas_peucker,
simplify_douglas_peucker_n
} from '../../../lib/psimpl-js/index.js'
import SimplifyJS from '../../../lib/simplify-js-alternative/simplify.js'
import { simplifyWasm } from '../../../lib/simplify-wasm/index.js'
import { Range, ToleranceRange, Checkbox } from './Field.js'
import { Algorithm } from './Algorithm.js'
class NthPoint extends Algorithm {
constructor() {
super('Nth Point', 'nth_point', simplify_nth_point)
this.addField(new Range('n', 'n', 1, 15, 1, 3))
}
}
class RadialDistance extends Algorithm {
constructor() {
super('RadialDistance', 'rad_dist', simplify_radial_distance)
this.addField(new ToleranceRange())
}
}
class PerpendicularDistance extends Algorithm {
constructor() {
super(
'Perpendicular Distance',
'perp_dist',
simplify_perpendicular_distance
)
this.addField(new ToleranceRange())
this.addField(new Range('Repeat', 'repeat', 1, 10, 1, 3))
}
}
class ReumannWitkam extends Algorithm {
constructor() {
super('Reuman-Witkam Algorithm', 'reumann_witkam', simplify_reumann_witkam)
this.addField(new ToleranceRange())
}
}
class Opheim extends Algorithm {
constructor() {
super('Opheim Algorithm', 'opheim', simplify_opheim)
this.addField(new ToleranceRange('Minimum Tolerance', 'minTol'))
this.addField(new ToleranceRange('Maximum Tolerance', 'maxTol'))
}
}
class Lang extends Algorithm {
constructor() {
super('Lang Algorithm', 'lang', simplify_lang)
this.addField(new ToleranceRange())
this.addField(new Range('Look ahead', 'lookAhead', 2, 100, 1, 5))
}
}
class DouglasPeucker extends Algorithm {
constructor() {
super(
'Douglas Peucker Algorithm',
'dougles_peucker',
simplify_douglas_peucker
)
this.addField(new ToleranceRange())
}
}
class DouglasPeuckerAlt extends Algorithm {
constructor() {
super(
'Douglas Peucker Alt. Algorithm',
'dougles_peucker_n',
simplify_douglas_peucker_n
)
this.addField(new Range('Count', 'count', 1, 2000, 1, 1000))
}
}
class SimplifyJSAlgo extends Algorithm {
constructor() {
super('Simplify.js', 'simplify_js', SimplifyJS)
this.addField(new ToleranceRange())
this.addField(new Checkbox('High Quality', 'highQual', false))
}
}
class SimplifyWASMAlgo extends Algorithm {
constructor() {
super('Simplify.wasm', 'simplify_wasm', simplifyWasm)
this.addField(new ToleranceRange())
this.addField(new Checkbox('High Quality', 'highQual', true))
}
}
export default [
new NthPoint(),
new RadialDistance(),
new PerpendicularDistance(),
new ReumannWitkam(),
new Opheim(),
new Lang(),
new DouglasPeucker(),
new DouglasPeuckerAlt(),
new SimplifyJSAlgo(),
new SimplifyWASMAlgo()
]

View File

@ -0,0 +1,38 @@
import 'file-drop-element'
import React from 'react'
import { observer } from 'mobx-react'
import state from '../state'
async function readFile(file) {
return new Promise(resolve => {
let reader = new FileReader()
reader.readAsText(file)
reader.onload = function() {
resolve(this.result)
}
})
}
@observer
export class DataSelector extends React.Component {
componentDidMount() {
let drop = document.getElementsByTagName('file-drop')[0]
drop.addEventListener('filedrop', e => {
let file = e.files[0]
state.originalName = file.name
readFile(file).then(text => {
state.originalText = text
})
})
}
render() {
return (
<>
<h2>Data</h2>
<file-drop accept="application/geo+json">Drop GeoJSON here!</file-drop>
</>
)
}
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import { observer } from 'mobx-react'
import state from '../state'
@observer
export class LayerControl extends React.Component {
render() {
return (
<>
<h3>Layers</h3>
<ul>
<li>
<label>
<input
name="originalActive"
type="checkbox"
checked={state.originalActive}
onChange={() => (state.originalActive = !state.originalActive)}
></input>
Original: {state.originalName}
</label>
</li>
<li>
<label>
<input
name="simplifiedActive"
type="checkbox"
checked={state.simplifiedActive}
onChange={() =>
(state.simplifiedActive = !state.simplifiedActive)
}
></input>
Simplified
</label>
</li>
</ul>
</>
)
}
}

View File

@ -0,0 +1,38 @@
import React from 'react'
import { Map, TileLayer, GeoJSON } from 'react-leaflet'
import { observer } from 'mobx-react'
import 'leaflet/dist/leaflet.css'
import state from '../state.js'
const tileLayers = {
osmDE: (
<TileLayer
url="https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
),
osmMapnik: (
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
)
}
@observer
export class MyMap extends React.Component {
render() {
return (
<Map center={[0, 0]} zoom={2}>
{tileLayers[state.tileLayer]}
{state.originalActive && (
<GeoJSON key={state.originalText} data={state.original} />
)}
{state.simplifiedActive && (
<GeoJSON key={state.simplifiedText} data={state.simplified} />
)}
</Map>
)
}
}

View File

@ -0,0 +1,9 @@
.range-input label {
display: grid;
grid-template-columns: 1fr auto;
}
.range-input input {
grid-row: 2/3;
grid-column: 1/3;
}

View File

@ -0,0 +1,67 @@
import React from 'react'
import './RangeInput.css'
export class RangeInput extends React.Component {
constructor(props) {
super(props)
this.state = {
rangeValue: props.value
? this.indexOfValue(props.value)
: Math.floor((this.length - 1) / 2)
}
this.handleChange = this.handleChange.bind(this)
}
get options() {
let { min, max, step } = this.props
let arr = []
if (step <= 0 || max < min) return []
for (min; min <= max; min += step) {
arr.push(min)
}
return arr.map(val => Math.round(val * 1000) / 1000)
}
get length() {
return this.options.length
}
get realValue() {
return this.options[this.state.rangeValue]
}
indexOfValue(value) {
let i = 0
for (let opt of this.options) {
if (opt >= value) return i
i++
}
return i
}
handleChange(e) {
this.setState({ rangeValue: e.target.value })
if (this.props.onChange) {
this.props.onChange(this.options[e.target.value])
}
}
render() {
return (
<div className="range-input">
<label>
<span>{this.props.label ? this.props.label : this.props.name}:</span>
<span>{Number(this.realValue.toFixed(2))}</span>
<input
type="range"
min={0}
max={this.length - 1}
value={this.state.rangeValue}
onChange={this.handleChange}
/>
</label>
</div>
)
}
}

View File

@ -0,0 +1,73 @@
import React from 'react'
import { observer } from 'mobx-react'
import algorithms from '../algorithms/index.js'
import { RangeInput } from './RangeInput.js'
import state from '../state.js'
const FieldComponent = ({ field }) => {
switch (field.type) {
case 'range':
let { name, id } = field
return (
<RangeInput
name={name}
label={id}
{...field.props}
value={field.value}
onChange={val => (field.value = val)}
/>
)
case 'checkbox':
return (
<>
{field.name}
<input
type="checkbox"
onChange={e => (field.value = e.target.checked)}
checked={field.value}
/>
</>
)
default:
return <span>Error: Field type {field.type} not known.</span>
}
}
const Options = ({ fields }) => {
return (
<fieldset>
{fields.map((field, i) => (
<FieldComponent key={i} field={field} />
))}
</fieldset>
)
}
@observer
export class SimplificationControl extends React.Component {
render() {
return (
<>
<h3>Simplification</h3>
<select
name="algorithm"
value={state.algorithmId}
onChange={e => (state.algorithmId = e.target.value)}
>
{algorithms.map((algo, i) => (
<option key={i} value={algo.id}>
{algo.name}
</option>
))}
</select>
{algorithms.map((algo, i) => (
<div key={i} hidden={!(algo === state.selectedAlgorithm)}>
<Options fields={algo.fields} />
</div>
))}
</>
)
}
}

View File

@ -0,0 +1,29 @@
fieldset.radio-grp {
border: none;
}
/* .radio-grp input[type='radio'] {
display: none;
} */
.radio-grp input[type='radio']:focus + label {
outline: 1px;
}
.radio-grp label {
display: block;
padding: 10px 20px;
font-size: 16px;
border-radius: 2px;
background-color: #ddd;
border: 1px solid #444;
}
.radio-grp label.selected {
background-color: rgb(175, 243, 255);
border-color: rgb(75, 174, 240);
}
.radio-grp label:hover {
background-color: rgb(144, 208, 250);
}

View File

@ -0,0 +1,47 @@
import React from 'react'
import { observer } from 'mobx-react'
import state from '../state.js'
import './TileLayerControl.css'
const RadioButton = ({ option, selected, fullName }) => {
return (
<label className={option === selected ? 'selected' : ''}>
<input
type="radio"
name="tileLayer"
value={option}
checked={option === selected}
onChange={() => {}}
/>
{fullName ? fullName : option}
</label>
)
}
@observer
export class TileLayerControl extends React.Component {
render() {
return (
<>
<h3>Background Layer</h3>
<fieldset
className="radio-grp"
onChange={e => (state.tileLayer = e.target.value)}
// onChange={console.log}
>
<RadioButton
option="osmDE"
selected={state.tileLayer}
fullName="OpenStreetMap DE"
/>
<RadioButton
option="osmMapnik"
selected={state.tileLayer}
fullName="OpenStreetMap Mapnik"
/>
</fieldset>
</>
)
}
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { TileLayerControl } from './components/TileLayerControl.js'
import { DataSelector } from './components/DataSelector.js'
import { LayerControl } from './components/LayerControl.js'
import { SimplificationControl } from './components/SimplificationControl.js'
import { MyMap } from './components/MyMap.js'
class App extends Component {
render() {
return (
<>
<MyMap />
<div id="options">
<h2>Options</h2>
<TileLayerControl />
<DataSelector />
<LayerControl />
<SimplificationControl />
</div>
</>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))

View File

@ -0,0 +1,46 @@
import { observable, computed } from 'mobx'
import { asyncComputed } from 'computed-async-mobx'
import algorithms from './algorithms/index.js'
class State {
@observable tileLayer = 'osmDE'
@observable originalActive = true
@observable simplifiedActive = true
@observable algorithmId = 'nth_point'
@observable topoJSON = false
@computed
get selectedAlgorithm() {
return algorithms.filter(algo => algo.id === this.algorithmId)[0]
}
@computed
get algoParams() {
return this.selectedAlgorithm.fields.map(f => f.value)
}
@observable originalText = '{"type": "FeatureCollection", "features": []}'
@observable originalName = ''
@computed
get original() {
return JSON.parse(this.originalText)
}
simplifiedPromise = asyncComputed(this.original, 500, async () => {
return await this.selectedAlgorithm.simplifyGeoJSON(
this.original,
this.algoParams
)
})
@computed
get simplified() {
return this.simplifiedPromise.get()
}
@computed
get simplifiedText() {
return JSON.stringify(this.simplified)
}
}
export default new State()

View File

@ -0,0 +1,46 @@
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: [/node_modules/, /data/]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: ['url-loader']
},
{
test: /\.wasm$/,
type: 'javascript/auto',
loader: 'file-loader',
options: {
name: '[name].[hash:5].[ext]'
}
}
]
},
plugins: [new CleanWebpackPlugin(), new CopyPlugin(['public'])],
devtool: 'source-map',
// to make webpack work with emscripten js files
target: 'web',
node: {
__dirname: false,
fs: 'empty',
Buffer: false,
process: false
}
}