Commit c7a23d9c authored by Barnabas Kendall's avatar Barnabas Kendall

copy feature

parent b16f069c
Pipeline #158384384 passed with stage
in 0 seconds
......@@ -8,23 +8,15 @@
/>
<the-sidebar
class="sidebar"
@saveFeature="feature => $refs.map.saveFile(feature)"
@zoomFeature="feature => $refs.map.zoomFeature(feature)"
@removeFeature="feature => $refs.map.removeSourceFeature(feature)"
@copyFeature="f => $refs.map.copyFeature(f)"
@removeFeature="f => $refs.map.removeSourceFeature(f)"
@saveFeature="f => $refs.map.saveFile(f)"
@zoomFeature="f => $refs.map.zoomFeature(f)"
>
<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>
......@@ -42,14 +34,6 @@ export default {
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()
......
<svg class="bi bi-file-earmark" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1h5v1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6h1v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2z"/>
<path d="M9 4.5V1l5 5h-3.5A1.5 1.5 0 0 1 9 4.5z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-files" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H3z"/>
<path d="M5 0h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1H3a2 2 0 0 1 2-2z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-hand-index" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.75 1a.75.75 0 0 0-.75.75V9a.5.5 0 0 1-1 0v-.89l-1.003.2a.5.5 0 0 0-.399.546l.345 3.105a1.5 1.5 0 0 0 .243.666l1.433 2.15a.5.5 0 0 0 .416.223h6.385a.5.5 0 0 0 .434-.252l1.395-2.442a2.5 2.5 0 0 0 .317-.991l.272-2.715a1 1 0 0 0-.995-1.1H13.5v1a.5.5 0 0 1-1 0V7.154a4.208 4.208 0 0 0-.2-.26c-.187-.222-.368-.383-.486-.43-.124-.05-.392-.063-.708-.039a4.844 4.844 0 0 0-.106.01V8a.5.5 0 0 1-1 0V5.986c0-.167-.073-.272-.15-.314a1.657 1.657 0 0 0-.448-.182c-.179-.035-.5-.04-.816-.027l-.086.004V8a.5.5 0 0 1-1 0V1.75A.75.75 0 0 0 6.75 1zM8.5 4.466V1.75a1.75 1.75 0 0 0-3.5 0v5.34l-1.199.24a1.5 1.5 0 0 0-1.197 1.636l.345 3.106a2.5 2.5 0 0 0 .405 1.11l1.433 2.15A1.5 1.5 0 0 0 6.035 16h6.385a1.5 1.5 0 0 0 1.302-.756l1.395-2.441a3.5 3.5 0 0 0 .444-1.389l.272-2.715a2 2 0 0 0-1.99-2.199h-.582a5.184 5.184 0 0 0-.195-.248c-.191-.229-.51-.568-.88-.716-.364-.146-.846-.132-1.158-.108l-.132.012a1.26 1.26 0 0 0-.56-.642 2.634 2.634 0 0 0-.738-.288c-.31-.062-.739-.058-1.05-.046l-.048.002zm2.094 2.025z"/>
</svg>
\ No newline at end of file
<svg class="bi bi-upload" 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 8zM5 4.854a.5.5 0 0 0 .707 0L8 2.56l2.293 2.293A.5.5 0 1 0 11 4.146L8.354 1.5a.5.5 0 0 0-.708 0L5 4.146a.5.5 0 0 0 0 .708z"/>
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0v-8A.5.5 0 0 1 8 2z"/>
</svg>
\ No newline at end of file
......@@ -2,24 +2,24 @@
<header>
<nav>
<h1 class="brand">
<img src="../assets/icons/map.svg" />
<img src="../assets/logo.svg" alt="logo"/>
Map Editor
</h1>
<div class="menu">
<button
class="pseudo"
class="pseudo icon upload"
@click="$refs.inputFile.click()"
>
Open
Upload
</button>
<button
class="pseudo"
class="pseudo icon download"
@click="$emit('saveFile')"
>
Save
Download
</button>
<button
class="pseudo"
class="pseudo icon file-earmark"
@click="$emit('clearMap')"
>
Clear
......@@ -50,7 +50,7 @@ export default {
h1 img {
height: 1.25em;
margin-right:8px;
margin-right: 8px;
}
input {
......
......@@ -5,6 +5,7 @@
<script>
import 'ol/ol.css'
import {
cloneMapFeature,
createMap,
downloadJsonFile,
featuresToGeoJSON,
......@@ -16,9 +17,6 @@ import { mapMutations, mapState } from 'vuex'
export default {
name: 'TheMap',
data () {
return { isDrawing: false }
},
mounted () {
const { map, source, interactions } = createMap(this.$el)
this.map = map
......@@ -28,34 +26,29 @@ export default {
source.on('addfeature', this.onAddFeature)
interactions.select.on('select', this.onSelect)
this.isDrawing = true
this.$emit('ready')
},
computed: {
...mapState(['selectedIds', 'features'])
...mapState(['selectedIds', 'features', 'isDrawing'])
},
methods: {
copyFeature (feature) {
const mapFeature = this.source.getFeatureById(feature.id)
if (mapFeature) {
const copy = cloneMapFeature(mapFeature, feature)
this.source.addFeature(copy)
}
},
async openFile (file) {
try {
const json = JSON.parse(await file.text())
const features = geoJSONToFeatures(json)
const features = geoJSONToFeatures(json, { fileName: file.name })
this.source.addFeatures(features)
this.zoomExtent()
} catch (err) {
console.log('openFile error', err)
}
},
saveFile (feature) {
if (feature) {
const mapFeature = this.source.getFeatureById(feature.id)
if (!mapFeature) return
const json = featureToGeoJSON(mapFeature, feature)
downloadJsonFile(json, `feature_${feature.id}.json`)
} else {
const json = featuresToGeoJSON(this.source.getFeatures(), this.features)
downloadJsonFile(json, 'features.json')
}
},
onSelect (e) {
if (!this.isDrawing) {
const ids = e.target.getFeatures().getArray().map(f => f.getId())
......@@ -64,7 +57,7 @@ export default {
},
onAddFeature ({ feature }) {
this.addFeature(getDataFeature(feature))
this.isDrawing = false
this.setIsDrawing(false)
},
removeSourceFeature (feature) {
const mapFeature = this.source.getFeatureById(feature.id)
......@@ -73,6 +66,17 @@ export default {
this.source.removeFeature(mapFeature)
}
},
saveFile (feature) {
if (feature) {
const mapFeature = this.source.getFeatureById(feature.id)
if (!mapFeature) return
const json = featureToGeoJSON(mapFeature, feature)
downloadJsonFile(json, feature.properties.fileName || `feature_${feature.id}.json`)
} else {
const json = featuresToGeoJSON(this.source.getFeatures(), this.features)
downloadJsonFile(json, 'features.json')
}
},
updateSelection () {
// sync map selection with vuex selected Ids if necessary
const selectedCount = Object.entries(this.selectedIds).filter(e => e[1]).length
......@@ -93,7 +97,7 @@ export default {
this.map.getView().fit(mapFeature.getGeometry().getExtent())
}
},
...mapMutations(['addFeature', 'removeFeature', 'setSelectedIds'])
...mapMutations(['addFeature', 'removeFeature', 'setSelectedIds', 'setIsDrawing'])
},
watch: {
isDrawing: function (value) {
......
<template>
<aside>
<slot></slot>
<feature-dialog ref="featureDialog" />
<div>
<button
class="pseudo icon"
:class="[isDrawing ? 'pencil' : 'hand-index']"
@click="setIsDrawing(!isDrawing)"
>
<span data-tooltip="Toggle drawing or editing mode">
{{isDrawing ? 'Drawing' : 'Editing'}}
</span>
</button>
</div>
<feature-dialog ref="featureDialog"/>
<div class="scroll-y">
<ul class="features">
<li
......@@ -20,7 +30,7 @@
data-tooltip="Zoom"
@click="$emit('zoomFeature', feature)"
>
<img src="../assets/icons/arrows-fullscreen.svg" alt="zoom out">
<img src="../assets/icons/arrows-fullscreen.svg" alt="Zoom">
</button>
<button
class="pseudo tooltip-bottom"
......@@ -36,6 +46,13 @@
>
<img src="../assets/icons/download.svg" alt="save">
</button>
<button
class="pseudo tooltip-bottom"
data-tooltip="Copy"
@click="$emit('copyFeature', feature)"
>
<img src="../assets/icons/files.svg" alt="copy">
</button>
<button
class="warning pseudo tooltip-bottom"
data-tooltip="Delete"
......@@ -57,7 +74,7 @@ export default {
name: 'TheSidebar',
components: { FeatureDialog },
computed: {
...mapState(['features', 'selectedIds'])
...mapState(['features', 'selectedIds', 'isDrawing'])
},
methods: {
getFeatureClass (feature) {
......@@ -70,13 +87,13 @@ export default {
const { id, properties } = feature
if (properties && properties.name) return properties.name
const entries = Object.entries(properties)
.filter(e => typeof e[1] === 'string')
.filter(e => e[0] !== 'fileName' && typeof e[1] === 'string')
.sort((a, b) => a[0].localeCompare(b[0]))
return entries.length > 0 ? entries.map(e => `${e[0]}: ${e[1]}`)
return entries.length > 0 ? entries.map(([name, value]) => `${name}: ${value}`)
.join('; ') : `(new feature ${id})`
},
...mapMutations(['toggleSelectedId'])
...mapMutations(['toggleSelectedId', 'setIsDrawing'])
}
}
</script>
......@@ -87,7 +104,7 @@ export default {
.scroll-y {
position: absolute;
top: 48px;
top: 72px;
bottom: 0;
left: 0;
right: 0;
......
......@@ -29,6 +29,7 @@ export function createMap (target) {
Object.values(interactions).forEach(i => map.addInteraction(i))
interactions.dragAndDrop.on('addfeatures', function (e) {
e.features.forEach(f => f.setProperties({ 'fileName': e.file.name }))
source.addFeatures(e.features)
map.getView().fit(source.getExtent())
})
......@@ -39,6 +40,13 @@ export function createMap (target) {
return { map, source, interactions }
}
export function cloneMapFeature (mapFeature, dataFeature) {
const json = featureToGeoJSON(mapFeature)
const [copy] = geoJSONToFeatures(json, dataFeature.properties)
copy.setId(getUid(copy))
return copy
}
export function getDataFeature (mapFeature) {
let id = mapFeature.getId()
if (!id) {
......
#app {
min-height:100vh;
min-height: 100vh;
}
button img {
......@@ -7,3 +7,53 @@ button img {
width: 1.25em;
vertical-align: middle;
}
button.icon::before {
vertical-align: middle;
margin-right: 4px;
}
.icon::before {
display: inline-block;
content: "";
background-repeat: no-repeat;
background-size: contain;
width: 1.25rem;
height: 1.25rem;
}
.icon.arrows-fullscreen::before {
background-image: url('./assets/icons/arrows-fullscreen.svg')
}
.icon.download::before {
background-image: url('./assets/icons/download.svg')
}
.icon.file-earmark::before {
background-image: url('./assets/icons/file-earmark.svg')
}
.icon.files::before {
background-image: url('./assets/icons/files.svg')
}
.icon.hand-index::before {
background-image: url('./assets/icons/hand-index.svg')
}
.icon.pencil::before {
background-image: url('./assets/icons/pencil.svg')
}
.icon.pencil-square::before {
background-image: url('./assets/icons/pencil-square.svg')
}
.icon.trash::before {
background-image: url('./assets/icons/trash.svg')
}
.icon.upload::before {
background-image: url('./assets/icons/upload.svg')
}
......@@ -3,6 +3,7 @@ import { createStore } from 'vuex'
export const store = createStore({
state () {
return {
isDrawing: false,
selectedIds: {},
features: []
}
......@@ -12,11 +13,9 @@ export const store = createStore({
state.features.push(Object.assign({}, feature))
if (feature.id) state.selectedIds = { ...state.selectedIds, [feature.id]: false }
},
updateFeature (state, feature) {
const idx = state.features.findIndex(f => f.id === feature.id)
if (idx < 0) return false
state.features[idx] = Object.assign({}, feature)
return true
clearFeatures (state) {
state.selectedIds = {}
state.features = []
},
removeFeature (state, feature) {
const idx = state.features.findIndex(f => f.id === feature.id)
......@@ -24,6 +23,9 @@ export const store = createStore({
state.features.splice(idx, 1)
return true
},
setIsDrawing (state, isDrawing) {
state.isDrawing = isDrawing
},
setSelectedIds (state, ids) {
const newIds = {}
Object.keys(state.selectedIds).forEach(id => newIds[id] = false)
......@@ -33,9 +35,11 @@ export const store = createStore({
toggleSelectedId (state, id) {
state.selectedIds[id] = !state.selectedIds[id]
},
clearFeatures (state) {
state.selectedIds = {}
state.features = []
updateFeature (state, feature) {
const idx = state.features.findIndex(f => f.id === feature.id)
if (idx < 0) return false
state.features[idx] = Object.assign({}, feature)
return true
}
}
})
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