Commit 8898faeb authored by Barnabas Kendall's avatar Barnabas Kendall

picnic.css + bootstrap icons

file open, feature select
parent 09a8bae8
Pipeline #157893966 passed with stage
in 0 seconds
......@@ -6,7 +6,9 @@
"build": "vite build"
},
"dependencies": {
"bootstrap-icons": "^1.0.0-alpha4",
"ol": "^6.3.1",
"picnic": "^6.5.2",
"vue": "^3.0.0-beta.15",
"vuex": "^4.0.0-beta.2"
},
......
<template>
<div class="wrapper">
<the-header class="header"></the-header>
<the-sidebar class="sidebar"></the-sidebar>
<the-map class="content"></the-map>
<the-header
class="header"
@openFile="file => $refs.map.openFile(file)"
@saveFile="$refs.map.saveFile()"
@clearMap="clearMap()"
/>
<the-sidebar
class="sidebar"
@saveFeature="feature => $refs.map.saveFile(feature)"
@zoomFeature="feature => $refs.map.zoomFeature(feature)"
>
<div>
<button
:disabled="isDrawDisabled"
@click="$refs.map.isDrawing = true">
<img src="./assets/icons/pencil.svg">
Draw
</button>
</div>
</the-sidebar>
<the-map
class="content"
ref="map"
@ready="isMapReady = true"
/>
</div>
</template>
......@@ -10,6 +32,7 @@
import TheHeader from './components/TheHeader.vue'
import TheSidebar from './components/TheSidebar.vue'
import TheMap from './components/TheMap.vue'
import { mapMutations } from 'vuex'
export default {
name: 'App',
......@@ -17,6 +40,21 @@ export default {
TheMap,
TheSidebar,
TheHeader,
},
data () {
return { isMapReady: false }
},
computed: {
isDrawDisabled () {
return !this.isMapReady || !this.$refs.map || this.$refs.map.isDrawing
}
},
methods: {
clearMap () {
this.$refs.map.source.clear()
this.clearFeatures()
},
...mapMutations(['clearFeatures'])
}
}
</script>
......@@ -25,8 +63,8 @@ export default {
min-height: 100vh;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 3fr;
grid-template-rows: 48px auto;
grid-template-columns: 300px auto;
grid-template-rows: 45px auto;
grid-gap: 8px;
}
......
<svg class="bi bi-arrows-fullscreen" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M1.464 10.536a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3.5a.5.5 0 0 1-.5-.5v-3.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M5.964 10a.5.5 0 0 1 0 .707l-4.146 4.147a.5.5 0 0 1-.707-.708L5.257 10a.5.5 0 0 1 .707 0zm8.854-8.854a.5.5 0 0 1 0 .708L10.672 6a.5.5 0 0 1-.708-.707l4.147-4.147a.5.5 0 0 1 .707 0z"/>
<path fill-rule="evenodd" d="M10.5 1.5A.5.5 0 0 1 11 1h3.5a.5.5 0 0 1 .5.5V5a.5.5 0 0 1-1 0V2h-3a.5.5 0 0 1-.5-.5zm4 9a.5.5 0 0 0-.5.5v3h-3a.5.5 0 0 0 0 1h3.5a.5.5 0 0 0 .5-.5V11a.5.5 0 0 0-.5-.5z"/>
<path fill-rule="evenodd" d="M10 9.964a.5.5 0 0 0 0 .708l4.146 4.146a.5.5 0 0 0 .708-.707l-4.147-4.147a.5.5 0 0 0-.707 0zM1.182 1.146a.5.5 0 0 0 0 .708L5.328 6a.5.5 0 0 0 .708-.707L1.889 1.146a.5.5 0 0 0-.707 0z"/>
<path fill-rule="evenodd" d="M5.5 1.5A.5.5 0 0 0 5 1H1.5a.5.5 0 0 0-.5.5V5a.5.5 0 0 0 1 0V2h3a.5.5 0 0 0 .5-.5z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-download" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 8a.5.5 0 0 1 .5.5V12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8.5a.5.5 0 0 1 1 0V12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5A.5.5 0 0 1 .5 8z"/>
<path fill-rule="evenodd" d="M5 7.5a.5.5 0 0 1 .707 0L8 9.793 10.293 7.5a.5.5 0 1 1 .707.707l-2.646 2.647a.5.5 0 0 1-.708 0L5 8.207A.5.5 0 0 1 5 7.5z"/>
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0v-8A.5.5 0 0 1 8 1z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-pencil-square" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456l-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-pencil" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M11.293 1.293a1 1 0 0 1 1.414 0l2 2a1 1 0 0 1 0 1.414l-9 9a1 1 0 0 1-.39.242l-3 1a1 1 0 0 1-1.266-1.265l1-3a1 1 0 0 1 .242-.391l9-9zM12 2l2 2-9 9-3 1 1-3 9-9z"/>
<path fill-rule="evenodd" d="M12.146 6.354l-2.5-2.5.708-.708 2.5 2.5-.707.708zM3 10v.5a.5.5 0 0 0 .5.5H4v.5a.5.5 0 0 0 .5.5H5v.5a.5.5 0 0 0 .5.5H6v-1.5a.5.5 0 0 0-.5-.5H5v-.5a.5.5 0 0 0-.5-.5H3z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-trash" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
\ No newline at end of file
<template>
<header>
<h1>Map Editor</h1>
<nav>
<h1 class="brand">Map Editor</h1>
<div class="menu">
<button
class="pseudo"
@click="$refs.inputFile.click()"
>
Open
</button>
<button
class="pseudo"
@click="$emit('saveFile')"
>
Save
</button>
<button
class="pseudo"
@click="$emit('clearMap')"
>
Clear
</button>
</div>
</nav>
<input type="file" ref="inputFile" @change="onFileChange">
</header>
</template>
<script>
export default {
name: 'TheHeader'
name: 'TheHeader',
methods: {
async onFileChange (e) {
const file = e.target.files[0]
if (file) this.$emit('openFile', file)
}
}
}
</script>
<style scoped>
......@@ -14,4 +44,8 @@ export default {
margin: 0;
font-size: x-large;
}
input {
display: none;
}
</style>
......@@ -4,8 +4,8 @@
<script>
import 'ol/ol.css'
import { createMap, getFeatureProperties } from './map'
import { mapMutations } from 'vuex'
import { createMap, geoJSONToFeatures, getFeatureProperties } from './map'
import { mapMutations, mapState } from 'vuex'
export default {
name: 'TheMap',
......@@ -22,25 +22,72 @@ export default {
interactions.select.on('select', this.onSelect)
this.isDrawing = true
this.$emit('ready')
},
computed: {
...mapState(['selectedIds'])
},
computed: {},
methods: {
async openFile (file) {
try {
const json = JSON.parse(await file.text())
const features = geoJSONToFeatures(json)
this.source.addFeatures(features)
this.zoomExtent()
} catch (err) {
console.log('openFile error', err)
}
},
async saveFile (feature) {
if (feature) {
} else {
}
},
onSelect (e) {
if (!this.isDrawing) {
const selectedIds = e.target.getFeatures().getArray().map(f => f.getId())
this.setSelectedIds(selectedIds)
const ids = e.target.getFeatures().getArray().map(f => f.getId())
this.setSelectedIds(ids)
}
},
onAddFeature ({ feature }) {
this.addFeature(getFeatureProperties(feature))
this.isDrawing = false
},
updateSelection () {
// sync map selection with vuex selected Ids if necessary
const selectedCount = Object.entries(this.selectedIds).filter(e => e[1]).length
const currentSelection = this.interactions.select.getFeatures()
if (selectedCount !== currentSelection.getLength()) {
currentSelection.clear()
this.source.forEachFeature(f => {
if (this.selectedIds[f.getId()]) currentSelection.push(f)
})
}
},
zoomExtent () {
this.map.getView().fit(this.source.getExtent())
},
zoomFeature (feature) {
const mapFeature = this.source.getFeatureById(feature.id)
if(mapFeature) {
this.map.getView().fit(mapFeature.getGeometry().getExtent())
}
},
...mapMutations(['addFeature', 'setSelectedIds'])
},
watch: {
isDrawing: function (value) {
this.interactions.drawPolygon.setActive(value === true)
this.interactions.modify.setActive(value !== true)
},
selectedIds: {
handler: function () {
this.updateSelection()
},
deep: true
}
}
}
......
<template>
<aside>
<div>
<slot></slot>
<div class="scroll-y">
<ul class="features">
<li :class="getFeatureClass(feature)" v-for="feature in features" :key="feature.id">
<span v-for="(value, key) in feature">
<strong>{{key}}:</strong> {{value}}
</span>
<li
:class="getFeatureClass(feature)"
v-for="feature in features"
:key="feature.id">
<label @click.prevent="toggleSelectedId(feature.id)">
<input
type="checkbox"
:checked="selectedIds[feature.id]">
<span class="checkable">{{getFeatureName(feature)}}</span>
</label>
<div class="feature-buttons" v-if="selectedIds[feature.id]">
<button
class="pseudo tooltip-bottom"
data-tooltip="Zoom"
@click="$emit('zoomFeature', feature)"
>
<img src="../assets/icons/arrows-fullscreen.svg">
</button>
<button
class="pseudo tooltip-bottom"
data-tooltip="Edit"
@click="$emit('editFeature', feature)"
>
<img src="../assets/icons/pencil-square.svg">
</button>
<button
class="pseudo tooltip-bottom"
data-tooltip="Save"
@click="$emit('saveFeature', feature)"
>
<img src="../assets/icons/download.svg">
</button>
<button
class="warning pseudo tooltip-bottom"
data-tooltip="Delete"
@click="$emit('deleteFeature', feature)"
>
<img src="../assets/icons/trash.svg">
</button>
</div>
</li>
</ul>
</div>
</aside>
</template>
<script>
import { mapState } from 'vuex'
import { mapMutations, mapState } from 'vuex'
export default {
name: 'TheSidebar',
......@@ -22,9 +59,21 @@ export default {
methods: {
getFeatureClass (feature) {
return {
feature: true,
selected: this.selectedIds[feature.id]
}
}
},
getFeatureName (feature) {
const { id, name, ...other } = feature
if (name) return name
const entries = Object.entries(other)
.filter(e => typeof e[1] === 'string')
.sort((a, b) => a[0].localeCompare(b[0]))
return entries.length > 0 ? entries.map(e => `${e[0]}: ${e[1]}`)
.join('; ') : `(new feature ${id})`
},
...mapMutations(['toggleSelectedId'])
}
}
</script>
......@@ -33,10 +82,12 @@ export default {
position: relative;
}
aside > div {
.scroll-y {
position: absolute;
top: 0;
top: 48px;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
......@@ -46,20 +97,19 @@ export default {
margin: 0;
}
ul li {
li.feature {
border: 2px solid transparent;
border-radius: 2px;
padding: 4px;
}
li:hover {
background: #ccc;
}
li.selected {
background: #2c3e50;
color: #fff;
li.feature.selected {
border: 2px solid #0074D9;
border-radius: 2px;
}
ul span:not(:last-child):after {
content: '; ';
.feature-buttons {
display: flex;
justify-content: space-between;
}
</style>
......@@ -4,7 +4,8 @@ import { OSM, Vector as VectorSource } from 'ol/source'
import { DragAndDrop, Draw, Modify, Select, Snap } from 'ol/interaction'
import { GeoJSON } from 'ol/format'
const geoJSONFormat = new GeoJSON()
const GEOJSON_OPTS = { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }
const geoJSONFormat = new GeoJSON(GEOJSON_OPTS)
export function createMap (target) {
const source = new VectorSource()
......@@ -48,10 +49,23 @@ export function getFeatureProperties (feature) {
return { id, ...props }
}
export function featureToGeoJSON (feature) {
export function geoJSONToFeatures (json, props) {
const features = geoJSONFormat.readFeaturesFromObject(json, GEOJSON_OPTS)
if (props) features.forEach(f => f.setProperties(props))
return features
}
export function featureToGeoJSON (feature, props) {
if (props) feature.setProperties(props)
return geoJSONFormat.writeFeatureObject(feature, { decimals: 7 })
}
export function featuresToGeoJSON (features) {
export function featuresToGeoJSON (features, propMap) {
if (propMap) {
features.forEach(f => {
const id = f.getId()
if (propMap[id]) f.setProperties(propMap[id])
})
}
return geoJSONFormat.writeFeaturesObject(features, { decimals: 7 })
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
min-height:100vh;
}
button img {
height: 1.25em;
width: 1.25em;
vertical-align: middle;
}
import { createApp } from 'vue'
import { store } from './store'
import App from './App.vue'
import 'picnic/picnic.min.css'
import './index.css'
createApp(App)
......
......@@ -25,6 +25,13 @@ export const store = createStore({
Object.keys(state.selectedIds).forEach(id => newIds[id] = false)
if (Array.isArray(ids)) ids.forEach(id => newIds[id] = true)
state.selectedIds = newIds
},
toggleSelectedId (state, id) {
state.selectedIds[id] = !state.selectedIds[id]
},
clearFeatures (state) {
state.selectedIds = {}
state.features = []
}
}
})
......@@ -477,6 +477,11 @@ [email protected]^3.1.1:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
[email protected]^1.0.0-alpha4:
version "1.0.0-alpha4"
resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.0.0-alpha4.tgz#b3eb7ca170696892bce19bf085601d4157831d4e"
integrity sha512-UcpSUPsvUiW7ueBQfXZSgknJv/rj060dglhWIRPjkLjUWa32jMWqsLXO8tXY2od4Ew6cuh0BJ3f8VOhQPVY4mA==
[email protected]^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
......@@ -1673,6 +1678,11 @@ [email protected]:
ieee754 "^1.1.12"
resolve-protobuf-schema "^2.1.0"
[email protected]^6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/picnic/-/picnic-6.5.2.tgz#3c307b1556eeed3da4fe63a18d9f1ef77690ecec"
integrity sha512-USH4JQiJVxrdW5vmFd5Ko1LSncCv6pzHj2Fujule64PZssB6VqZ569mfB4XDDs8dKN/KiezQ+Vvnft4/VLX7mA==
[email protected]^2.0.4, [email protected]^2.0.5, [email protected]^2.2.1, [email protected]^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment