comparison client/src/components/map/fairway/Profiles.vue @ 1372:553aadd97087

new cross profile workflow (WIP) Needs fixing of some bugs and not so nice looks.
author Markus Kottlaender <markus@intevation.de>
date Tue, 27 Nov 2018 12:59:26 +0100
parents
children fa7d647f8d77
comparison
equal deleted inserted replaced
1371:5b9b8eabcd01 1372:553aadd97087
1 <template>
2 <div :class="['box ui-element rounded bg-white text-nowrap', { expanded: showProfiles }]">
3 <div style="width: 20rem">
4 <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
5 <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon>
6 Profiles
7 <font-awesome-icon
8 icon="times"
9 class="ml-auto text-muted"
10 @click="$store.commit('application/showProfiles', false)"
11 ></font-awesome-icon>
12 </h6>
13 <div class="d-flex flex-column p-3 flex-grow-1 text-left position-relative">
14 <div class="loading d-flex justify-content-center align-items-center" v-if="surveysLoading || profileLoading">
15 <font-awesome-icon icon="spinner" spin />
16 </div>
17 <small class="text-muted">Bottleneck:</small>
18 <select @click="moveToBottleneck" v-model="selectedBottleneck" class="form-control form-control-sm">
19 <option :value="null">Select Bottleneck</option>
20 <option
21 v-for="bn in bottlenecks"
22 :key="bn.properties.name"
23 :value="bn.properties.name"
24 >{{ bn.properties.name }}</option>
25 </select>
26 <div v-if="selectedBottleneck">
27 <div class="d-flex mt-2">
28 <div class="flex-fill">
29 <small class="text-muted">Sounding Result:</small>
30 <select v-model="selectedSurvey" class="form-control form-control-sm">
31 <option
32 v-for="survey in surveys"
33 :key="survey.date_info"
34 :value="survey"
35 >{{ survey.date_info }}</option>
36 </select>
37 </div>
38 <div class="flex-fill ml-3" v-if="selectedSurvey && surveys.length > 1">
39 <small class="text-muted mt-1">Compare with:</small>
40 <select v-model="additionalSurvey" class="form-control form-control-sm">
41 <option :value="null">None</option>
42 <option
43 v-for="survey in additionalSurveys"
44 :key="survey.date_info"
45 :value="survey"
46 >{{ survey.date_info }}</option>
47 </select>
48 </div>
49 </div>
50 <hr class="w-100">
51 <small class="d-flex text-left my-2" v-if="startPoint && endPoint">
52 <div class="text-nowrap mr-3">
53 <b>Start:</b>
54 <br>
55 Lat: {{ startPoint[1] }}
56 <br>
57 Lon: {{ startPoint[0] }}
58 </div>
59 <div class="text-nowrap">
60 <b>End:</b>
61 <br>
62 Lat: {{ endPoint[1] }}
63 <br>
64 Lon: {{ endPoint[0] }}
65 </div>
66 <button v-clipboard:copy="coordinatesForClipboard"
67 v-clipboard:success="onCopyCoordinates"
68 class="btn btn-info btn-sm ml-auto mt-auto">
69 <font-awesome-icon icon="copy" />
70 </button>
71 </small>
72 <div class="d-flex">
73 <div class="pr-3 w-50" v-if="startPoint && endPoint">
74 <button class="btn btn-info btn-sm w-100"
75 @click="showLabelInput = !showLabelInput">
76 <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" />
77 {{ showLabelInput ? "Cancel" : "Save" }}
78 </button>
79 </div>
80 <div :class="startPoint && endPoint ? 'w-50' : 'w-100'">
81 <button
82 class="btn btn-info btn-sm w-100"
83 @click="toggleCutTool"
84 >
85 <font-awesome-icon :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'"></font-awesome-icon>
86 {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }}
87 </button>
88 </div>
89 </div>
90 <div v-if="showLabelInput" class="mt-2">
91 <small class="text-muted">Enter label for cross profile:</small>
92 <div class="position-relative">
93 <input class="form-control form-control-sm pr-5" v-model="cutLabel" />
94 <button class="btn btn-sm btn-info position-absolute input-button-right"
95 @click="saveCut"
96 v-if="cutLabel"
97 style="top: 0; right: 0;">
98 <font-awesome-icon icon="check" />
99 </button>
100 </div>
101 </div>
102 <small class="text-muted d-block mt-2">Saved cross profiles:</small>
103 <select class="form-control form-control-sm" v-model="coordinatesSelect">
104 <option></option>
105 <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index">
106 {{ cut.label }}
107 </option>
108 </select>
109 <small class="text-muted d-block mt-2">Enter coordinates manually:</small>
110 <div class="position-relative">
111 <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" />
112 <button class="btn btn-sm btn-info position-absolute input-button-right"
113 @click="applyManualCoordinates"
114 style="top: 0; right: 0;"
115 v-if="coordinatesInputIsValid">
116 <font-awesome-icon icon="check" />
117 </button>
118 </div>
119 </div>
120 </div>
121 </div>
122 </div>
123 </template>
124
125 <style lang="sass" scoped>
126 .loading
127 background: rgba(255, 255, 255, 0.96)
128 position: absolute
129 z-index: 99
130 top: 0
131 right: 0
132 bottom: 0
133 left: 0
134
135 .input-button-right
136 border-top-right-radius: $border-radius
137 border-bottom-right-radius: $border-radius
138 border-top-left-radius: 0 !important
139 border-bottom-left-radius: 0 !important
140 </style>
141
142 <script>
143 /* This is Free Software under GNU Affero General Public License v >= 3.0
144 * without warranty, see README.md and license for details.
145 *
146 * SPDX-License-Identifier: AGPL-3.0-or-later
147 * License-Filename: LICENSES/AGPL-3.0.txt
148 *
149 * Copyright (C) 2018 by via donau
150 * – Österreichische Wasserstraßen-Gesellschaft mbH
151 * Software engineering by Intevation GmbH
152 *
153 * Author(s):
154 * Markus Kottländer <markus.kottlaender@intevation.de>
155 */
156 import { mapState, mapGetters } from "vuex";
157 import Feature from "ol/Feature";
158 import LineString from "ol/geom/LineString";
159 import { displayError, displayInfo } from "../../../lib/errors.js";
160
161 export default {
162 name: "profiles",
163 data() {
164 return {
165 coordinatesInput: "",
166 coordinatesSelect: null,
167 cutLabel: "",
168 showLabelInput: false
169 };
170 },
171 computed: {
172 ...mapGetters("map", ["getVSourceByName"]),
173 ...mapState("application", ["showProfiles"]),
174 ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
175 ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]),
176 ...mapState("fairwayprofile", [
177 "previousCuts",
178 "startPoint",
179 "endPoint",
180 "profileLoading"
181 ]),
182 selectedBottleneck: {
183 get() {
184 return this.$store.state.bottlenecks.selectedBottleneck;
185 },
186 set(name) {
187 this.loading = true;
188 this.$store.dispatch("bottlenecks/setSelectedBottleneck", name);
189 }
190 },
191 selectedSurvey: {
192 get() {
193 return this.$store.state.bottlenecks.selectedSurvey;
194 },
195 set(survey) {
196 this.$store.commit("bottlenecks/setSelectedSurvey", survey);
197 }
198 },
199 additionalSurvey: {
200 get() {
201 return this.$store.state.fairwayprofile.additionalSurvey;
202 },
203 set(survey) {
204 this.$store.commit("fairwayprofile/setAdditionalSurvey", survey);
205 }
206 },
207 additionalSurveys() {
208 return this.surveys.filter(survey => survey !== this.selectedSurvey);
209 },
210 coordinatesForClipboard() {
211 return (
212 this.startPoint[1] +
213 "," +
214 this.startPoint[0] +
215 "," +
216 this.endPoint[1] +
217 "," +
218 this.endPoint[0]
219 );
220 },
221 coordinatesInputIsValid() {
222 const coordinates = this.coordinatesInput
223 .split(",")
224 .map(coord => parseFloat(coord.trim()))
225 .filter(c => Number(c) === c);
226 return coordinates.length === 4;
227 }
228 },
229 watch: {
230 selectedBottleneck() {
231 this.$store.dispatch("fairwayprofile/previousCuts");
232 this.cutLabel =
233 this.selectedBottleneck + " (" + new Date().toISOString() + ")";
234 },
235 selectedSurvey(survey) {
236 if (survey) this.$store.dispatch("fairwayprofile/loadProfile", survey);
237 },
238 additionalSurvey(survey) {
239 if (survey) this.$store.dispatch("fairwayprofile/loadProfile", survey);
240 },
241 coordinatesSelect(newValue) {
242 if (newValue) {
243 this.applyCoordinates(newValue);
244 }
245 }
246 },
247 methods: {
248 toggleCutTool() {
249 if (this.selectedSurvey) {
250 this.cutTool.setActive(!this.cutTool.getActive());
251 this.lineTool.setActive(false);
252 this.polygonTool.setActive(false);
253 this.$store.commit("map/setCurrentMeasurement", null);
254 }
255 },
256 onCopyCoordinates() {
257 displayInfo({
258 title: "Success",
259 message: "Coordinates copied to clipboard!"
260 });
261 },
262 applyManualCoordinates() {
263 const coordinates = this.coordinatesInput
264 .split(",")
265 .map(coord => parseFloat(coord.trim()));
266 this.coordinatesSelect = null;
267 this.coordinatesInput = "";
268 this.applyCoordinates([
269 coordinates[1],
270 coordinates[0],
271 coordinates[3],
272 coordinates[2]
273 ]);
274 },
275 applyCoordinates(coordinates) {
276 // allow only numbers
277 coordinates = coordinates.filter(c => Number(c) === c);
278 if (coordinates.length === 4) {
279 // draw line on map
280 this.getVSourceByName("Cut Tool").clear();
281 const cut = new Feature({
282 geometry: new LineString([
283 [coordinates[0], coordinates[1]],
284 [coordinates[2], coordinates[3]]
285 ]).transform("EPSG:4326", "EPSG:3857")
286 });
287 this.getVSourceByName("Cut Tool").addFeature(cut);
288
289 // draw diagram
290 this.$store.dispatch("fairwayprofile/cut", cut);
291 } else {
292 displayError({
293 title: "Invalid input",
294 message:
295 "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon"
296 });
297 }
298 },
299 saveCut() {
300 const previousCuts =
301 JSON.parse(localStorage.getItem("previousCuts")) || [];
302 const newEntry = {
303 label: this.cutLabel,
304 bottleneckName: this.selectedBottleneck,
305 coordinates: [...this.startPoint, ...this.endPoint]
306 };
307 const existingEntry = previousCuts.find(cut => {
308 return JSON.stringify(cut) === JSON.stringify(newEntry);
309 });
310 if (!existingEntry) previousCuts.push(newEntry);
311 if (previousCuts.length > 100) previousCuts.shift();
312 localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
313 this.$store.dispatch("fairwayprofile/previousCuts");
314
315 this.showLabelInput = false;
316 this.cutLabel = "";
317 displayInfo({
318 title: "Coordinates saved!",
319 message:
320 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
321 });
322 },
323 moveToBottleneck() {
324 const bottleneck = this.bottlenecks.find(
325 bn => bn.properties.name === this.selectedBottleneck
326 );
327 if (!bottleneck) return;
328 this.$store.commit("map/moveMap", {
329 coordinates: bottleneck.geometry.coordinates,
330 zoom: 17,
331 preventZoomOut: true
332 });
333 }
334 }
335 };
336 </script>