Skip to content

Commit 3b0c565

Browse files
committed
Add support for ValidateImage project type
1 parent 2f58bf5 commit 3b0c565

File tree

6 files changed

+377
-3
lines changed

6 files changed

+377
-3
lines changed

src/components/CompletenessProject.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { computed, inject, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch, watchEffect } from 'vue';
33
import { createGeoJsonFromTasks } from '@/utils/common';
44
import buildTasks from '@/utils/buildTasks';
5-
import type { CustomOption, OverlayTileServer, Project, TaskGroup, TutorialTileTask, TileTask, Tutorial } from '@/utils/types';
5+
import type { DefaultOption, OverlayTileServer, Project, TaskGroup, TutorialTileTask, TileTask, Tutorial } from '@/utils/types';
66
import BaseMap from './BaseMap.vue';
77
import { isDefined, isNotDefined, listToMap } from '@togglecorp/fujs';
88
import TaskProgress from '@/components/TaskProgress.vue'
@@ -17,7 +17,7 @@ import FindProjectInstructions from './FindProjectInstructions.vue';
1717
interface Props {
1818
group: TaskGroup;
1919
first: boolean;
20-
options: CustomOption[];
20+
options: DefaultOption[];
2121
project: Project;
2222
tasks: TileTask[];
2323
tutorial: Tutorial;
@@ -171,7 +171,6 @@ function handleWindowResize() {
171171
);
172172
}
173173
}, 200);
174-
175174
}
176175
177176
onMounted(() => {
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<script lang="ts" setup>
2+
3+
import { isDefined } from '@togglecorp/fujs';
4+
import { computed, inject, onMounted, ref, shallowRef, watchEffect } from 'vue';
5+
import type { Project, TaskGroup, TutorialTileTask, Tutorial, CustomOption, ImageTask } from '@/utils/types';
6+
import ValidateImageProjectTask from '@/components/ValidateImageProjectTask.vue';
7+
import OptionButtons from '@/components/OptionButtons.vue';
8+
import TaskProgress from './TaskProgress.vue';
9+
import ProjectHeader from '@/components/ProjectHeader.vue'
10+
import ProjectInfo from './ProjectInfo.vue';
11+
import createInformationPages from '@/utils/createInformationPages';
12+
import { createFallbackInformationPages } from '@/utils/domain';
13+
14+
interface Props {
15+
group: TaskGroup;
16+
first: boolean;
17+
options: CustomOption[];
18+
project: Project;
19+
tasks: ImageTask[];
20+
tutorial: Tutorial;
21+
tutorialTasks: TutorialTileTask[];
22+
}
23+
24+
const taskOffset = ref(0);
25+
// const projectInfoRef = useTemplateRef('projectInfo');
26+
27+
const props = withDefaults(defineProps<Props>(), {
28+
options: () => [
29+
{
30+
mdiIcon: 'mdi-check-bold',
31+
description: `The shape does outline a building in the image`,
32+
iconColor: '#388E3C',
33+
shortKey: 1,
34+
title: 'Yes',
35+
value: 1,
36+
},
37+
{
38+
mdiIcon: 'mdi-close-thick',
39+
description: `The shape doesn't match a building in the image`,
40+
iconColor: '#D32F2F',
41+
shortKey: 2,
42+
title: 'No',
43+
value: 0,
44+
},
45+
{
46+
mdiIcon: 'mdi-minus-thick',
47+
description: `If you're not sure or there is cloud cover / bad imagery.`,
48+
iconColor: '#616161',
49+
title: 'Not sure',
50+
shortKey: 3,
51+
value: 2,
52+
},
53+
],
54+
});
55+
56+
const logMappingStarted = inject<(projectType: string) => void>('logMappingStarted');
57+
const saveResults = inject<(results: Record<string, number>, startTime: string) => void>('saveResults');
58+
const arrowKeys = ref(true);
59+
const startTime = shallowRef<string>();
60+
61+
const emit = defineEmits<{ created: []}>();
62+
const results = ref<Record<string, number>>({});
63+
64+
watchEffect(() => {
65+
startTime.value = new Date().toISOString();
66+
});
67+
68+
69+
const currentTask = computed(() => props.tasks[taskOffset.value]);
70+
71+
onMounted(() => {
72+
logMappingStarted?.(props.project.projectType);
73+
emit('created');
74+
});
75+
76+
function addResult(newValue: number) {
77+
results.value[currentTask.value.taskId] = newValue;
78+
}
79+
80+
const isLastTask = computed(
81+
() => (props.tasks.length - 1) === taskOffset.value
82+
);
83+
const currentTaskAnswered = computed(
84+
() => isDefined(results.value[currentTask.value.taskId])
85+
)
86+
87+
function handleBack() {
88+
if (taskOffset.value > 0) {
89+
taskOffset.value = taskOffset.value - 1;
90+
}
91+
}
92+
93+
function handleForward() {
94+
if (taskOffset.value < (props.tasks.length - 1)) {
95+
taskOffset.value = taskOffset.value + 1;
96+
}
97+
}
98+
99+
</script>
100+
101+
<template>
102+
<ProjectHeader
103+
:mission="$t('projectView.youAreLookingFor', { lookFor: props.project.lookFor })"
104+
>
105+
<Project-info
106+
ref="projectInfo"
107+
:first="first"
108+
:informationPages="createInformationPages(props.tutorial, props.project, createFallbackInformationPages)"
109+
:manualUrl="project?.manualUrl"
110+
@toggle-dialog="arrowKeys = !arrowKeys"
111+
>
112+
<!-- TODO: add instruction -->
113+
<!--
114+
<template #instructions>
115+
<validate-project-instructions :mission="mission" :options="options" />
116+
</template>
117+
-->
118+
<!-- FIXME: add tutorial -->
119+
<!--
120+
<template #tutorial>
121+
<validate-project-tutorial
122+
:tutorial="tutorial"
123+
:tasks="tutorialTasks"
124+
:options="options"
125+
@tutorialComplete="projectInfoRef?.toggleDialog"
126+
/>
127+
</template>
128+
-->
129+
<!-- FIXME: add tutorial -->
130+
</Project-info>
131+
</ProjectHeader>
132+
<v-container
133+
class="ma-0 pa-0 container"
134+
>
135+
<ValidateImageProjectTask
136+
:task="currentTask"
137+
/>
138+
</v-container>
139+
<OptionButtons
140+
v-if="currentTask"
141+
:options="props.options"
142+
:result="results[currentTask.taskId]"
143+
:taskId="currentTask.taskId"
144+
@addResult="addResult"
145+
/>
146+
<v-toolbar color="white" density="compact" extension-height="20" extended>
147+
<v-spacer />
148+
<v-btn
149+
:title="$t('validateProject.moveLeft')"
150+
icon="mdi-chevron-left"
151+
color="secondary"
152+
:disabled="taskOffset <= 0"
153+
@click="handleBack"
154+
v-shortkey.once="[arrowKeys ? 'arrowleft' : '']"
155+
@shortkey="handleBack"
156+
/>
157+
<v-btn
158+
v-if="isDefined(startTime)"
159+
:title="$t('projectView.saveResults')"
160+
icon="mdi-content-save"
161+
color="primary"
162+
:disabled="!isLastTask"
163+
@click="saveResults?.(results, startTime)"
164+
/>
165+
<v-btn
166+
:title="$t('validateProject.moveRight')"
167+
icon="mdi-chevron-right"
168+
color="secondary"
169+
:disabled="!currentTaskAnswered || isLastTask"
170+
@click="handleForward"
171+
v-shortkey.once="[arrowKeys ? 'arrowright' : '']"
172+
@shortkey="handleForward"
173+
/>
174+
<v-spacer />
175+
<template #extension>
176+
<TaskProgress :progress="taskOffset + 1" :total="tasks.length" />
177+
</template>
178+
</v-toolbar>
179+
</template>
180+
181+
<style scoped>
182+
.container {
183+
height: calc(100vh - 390px);
184+
}
185+
</style>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<script lang="ts" setup>
2+
import type { ImageTask } from '@/utils/types';
3+
import { isNotDefined } from '@togglecorp/fujs';
4+
import { onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
5+
6+
interface Props {
7+
task: ImageTask;
8+
}
9+
10+
const imgRef = useTemplateRef("taskImage");
11+
const props = defineProps<Props>();
12+
13+
const bbox = ref<{ x: string, y: string, width: string, height: string} | undefined>();
14+
const debounceTimeoutRef = shallowRef();
15+
16+
function calculateBbox() {
17+
if (isNotDefined(imgRef.value?.image)) {
18+
bbox.value = undefined;
19+
return;
20+
}
21+
22+
if (isNotDefined(props.task.bbox)) {
23+
bbox.value = undefined;
24+
return;
25+
}
26+
27+
const imageWidth = props.task.width ?? imgRef.value.image.naturalWidth;
28+
const imageHeight = props.task.height ?? imgRef.value.image.naturalHeight;
29+
30+
const containerWidth = imgRef.value.image.clientWidth;
31+
const containerHeight = imgRef.value.image.clientHeight;
32+
33+
const containerAspectRatio = containerWidth / containerHeight;
34+
const imageAspectRatio = imageWidth / imageHeight;
35+
36+
const renderedHeight = imageAspectRatio > containerAspectRatio
37+
? containerWidth / imageAspectRatio
38+
: containerHeight;
39+
40+
const renderedWidth = containerAspectRatio > imageAspectRatio
41+
? containerHeight * imageAspectRatio
42+
: containerWidth;
43+
44+
const yExcess = containerHeight - renderedHeight;
45+
const xExcess = containerWidth - renderedWidth;
46+
47+
const [x1, y1, w, h] = props.task.bbox;
48+
49+
const cx = (x1 / imageWidth) * renderedWidth + xExcess / 2;
50+
const cy = (y1 / imageHeight) * renderedHeight + yExcess / 2;
51+
const cw = (w / imageWidth) * renderedWidth;
52+
const ch = (h / imageHeight) * renderedHeight;
53+
54+
bbox.value = {
55+
x: `${cx}px`,
56+
y: `${cy}px`,
57+
width: `${cw}px`,
58+
height: `${ch}px`,
59+
}
60+
}
61+
62+
watch(
63+
() => props.task,
64+
calculateBbox,
65+
);
66+
67+
function handleResize() {
68+
calculateBbox();
69+
}
70+
71+
function handleImageLoad() {
72+
setTimeout(calculateBbox, 0);
73+
}
74+
75+
function handleWindowResize() {
76+
window.clearTimeout(debounceTimeoutRef.value);
77+
debounceTimeoutRef.value = window.setTimeout(() => {
78+
calculateBbox();
79+
}, 200);
80+
}
81+
82+
onMounted(() => {
83+
window.addEventListener('resize', handleWindowResize);
84+
});
85+
86+
onUnmounted(() => {
87+
window.removeEventListener('resize', handleWindowResize);
88+
window.clearTimeout(debounceTimeoutRef.value);
89+
});
90+
91+
</script>
92+
93+
<template>
94+
<v-img
95+
class="task-image"
96+
v-if="props.task.url"
97+
:src="props.task.url"
98+
@load="handleImageLoad"
99+
:onresize="handleResize"
100+
ref="taskImage"
101+
>
102+
<svg
103+
v-if="bbox"
104+
class="bbox"
105+
view-box="0 0 100 100"
106+
>
107+
<rect
108+
:x="bbox.x"
109+
:y="bbox.y"
110+
:width="bbox.width"
111+
:height="bbox.height"
112+
/>
113+
</svg>
114+
<template v-slot:placeholder>
115+
<v-row class="fill-height ma-0" align="center" justify="center">
116+
<v-progress-circular color="primary" indeterminate />
117+
</v-row>
118+
</template>
119+
<template v-slot:error>
120+
<v-row class="fill-height ma-0 image-failed" align="center" justify="center">
121+
{{ $t('imageTile.failureMessage') }}
122+
</v-row>
123+
</template>
124+
</v-img>
125+
<div class="image-not-available" v-if="!props.task.url">
126+
{{ $t('imageTile.notAvailableMessage') }}
127+
</div>
128+
</template>
129+
130+
<style scoped>
131+
.task-image {
132+
position: relative;
133+
isolation: isolate;
134+
width: 100%;
135+
height: 100%;
136+
}
137+
.image-not-available {
138+
display: flex;
139+
align-items: center;
140+
justify-content: center;
141+
text-align: center;
142+
aspect-ratio: 1;
143+
background-color: #a1a1a1;
144+
color: rgba(255, 255, 255, 0.6);
145+
}
146+
.image-failed {
147+
color: rgba(255, 255, 255, 0.6);
148+
background-color: #a1a1a1;
149+
}
150+
.bbox {
151+
position: absolute;
152+
left: 0;
153+
top: 0;
154+
width: 100%;
155+
height: 100%;
156+
}
157+
158+
.bbox rect {
159+
stroke: #fff;
160+
stroke-width: 2;
161+
fill: #fff;
162+
fill-opacity: 0.1;
163+
transition: .2s all ease-in-out;
164+
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5));
165+
}
166+
</style>

src/config/projectTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const projectTypes = {
2727
name: 'Street',
2828
component: 'streetProject',
2929
},
30+
'10': {
31+
name: 'Validate Image',
32+
component: 'validateImageProject',
33+
},
3034
}
3135

3236
export default projectTypes

0 commit comments

Comments
 (0)