diff --git a/backend/models/dtos/project_dto.py b/backend/models/dtos/project_dto.py
index dd855ada0..74803113a 100644
--- a/backend/models/dtos/project_dto.py
+++ b/backend/models/dtos/project_dto.py
@@ -199,7 +199,11 @@ class ProjectDTO(Model):
enforce_random_task_selection = BooleanType(
required=False, default=False, serialized_name="enforceRandomTaskSelection"
)
-
+ earliest_street_imagery = UTCDateTimeType(serialized_name="earliestStreetImagery")
+ image_capture_mode = BooleanType(
+ required=False, default=False, serialized_name="imageCaptureMode"
+ )
+ mapillary_organization_id = StringType(serialized_name="mapillaryOrganizationId")
private = BooleanType(required=True)
changeset_comment = StringType(serialized_name="changesetComment")
osmcha_filter_id = StringType(serialized_name="osmchaFilterId")
@@ -462,6 +466,11 @@ class ProjectSummary(Model):
author = StringType()
created = UTCDateTimeType()
due_date = UTCDateTimeType(serialized_name="dueDate")
+ earliest_street_imagery = UTCDateTimeType(serialized_name="earliestStreetImagery")
+ image_capture_mode = BooleanType(
+ required=False, default=False, serialized_name="imageCaptureMode"
+ )
+ mapillary_organization_id = StringType(serialized_name="mapillaryOrganizationId")
last_updated = UTCDateTimeType(serialized_name="lastUpdated")
priority = StringType(serialized_name="projectPriority")
campaigns = ListType(ModelType(CampaignDTO), default=[])
diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py
index 11845c7cf..6575f1b6e 100644
--- a/backend/models/postgis/project.py
+++ b/backend/models/postgis/project.py
@@ -146,7 +146,10 @@ class Project(db.Model):
db.String
) # Optional custom filter id for filtering on OSMCha
due_date = db.Column(db.DateTime)
+ earliest_street_imagery = db.Column(db.DateTime)
imagery = db.Column(db.String)
+ image_capture_mode = db.Column(db.Boolean, default=False)
+ mapillary_organization_id = db.Column(db.String)
josm_preset = db.Column(db.String)
id_presets = db.Column(ARRAY(db.String))
extra_id_params = db.Column(db.String)
@@ -378,6 +381,9 @@ def update(self, project_dto: ProjectDTO):
self.mapper_level = MappingLevel[project_dto.mapper_level.upper()].value
self.changeset_comment = project_dto.changeset_comment
self.due_date = project_dto.due_date
+ self.earliest_street_imagery = project_dto.earliest_street_imagery
+ self.image_capture_mode = project_dto.image_capture_mode
+ self.mapillary_organization_id = project_dto.mapillary_organization_id
self.imagery = project_dto.imagery
self.josm_preset = project_dto.josm_preset
self.id_presets = project_dto.id_presets
@@ -836,6 +842,9 @@ def get_project_summary(self, preferred_locale) -> ProjectSummary:
summary.country_tag = self.country
summary.changeset_comment = self.changeset_comment
summary.due_date = self.due_date
+ summary.earliest_street_imagery = self.earliest_street_imagery
+ summary.image_capture_mode = self.image_capture_mode
+ summary.mapillary_organization_id = self.mapillary_organization_id
summary.created = self.created
summary.last_updated = self.last_updated
summary.osmcha_filter_id = self.osmcha_filter_id
@@ -1011,6 +1020,9 @@ def _get_project_and_base_dto(self):
base_dto.changeset_comment = self.changeset_comment
base_dto.osmcha_filter_id = self.osmcha_filter_id
base_dto.due_date = self.due_date
+ base_dto.earliest_street_imagery = self.earliest_street_imagery
+ base_dto.image_capture_mode = self.image_capture_mode
+ base_dto.mapillary_organization_id = self.mapillary_organization_id
base_dto.imagery = self.imagery
base_dto.josm_preset = self.josm_preset
base_dto.id_presets = self.id_presets
diff --git a/frontend/.env.expand b/frontend/.env.expand
index 3247d4410..916ae8ead 100644
--- a/frontend/.env.expand
+++ b/frontend/.env.expand
@@ -42,3 +42,4 @@ REACT_APP_SENTRY_FRONTEND_DSN=$TM_SENTRY_FRONTEND_DSN
REACT_APP_ENVIRONMENT=$TM_ENVIRONMENT
REACT_APP_TM_DEFAULT_CHANGESET_COMMENT=$TM_DEFAULT_CHANGESET_COMMENT
REACT_APP_RAPID_EDITOR_URL=$RAPID_EDITOR_URL
+REACT_APP_MAPILLARY_TOKEN=$MAPILLARY_ACCESS_TOKEN
\ No newline at end of file
diff --git a/frontend/src/assets/img/mapillary-compass.png b/frontend/src/assets/img/mapillary-compass.png
new file mode 100644
index 000000000..a0130dcc8
Binary files /dev/null and b/frontend/src/assets/img/mapillary-compass.png differ
diff --git a/frontend/src/assets/styles/_extra.scss b/frontend/src/assets/styles/_extra.scss
index b06e47f2a..aa7f0b01d 100644
--- a/frontend/src/assets/styles/_extra.scss
+++ b/frontend/src/assets/styles/_extra.scss
@@ -383,6 +383,10 @@ a[href="https://www.mapbox.com/map-feedback/"] {
}
}
+div.react-datepicker-popper {
+ z-index: 5;
+}
+
.comment-textarea {
box-sizing: border-box;
@@ -398,4 +402,4 @@ a[href="https://www.mapbox.com/map-feedback/"] {
.sticky-top {
position: sticky !important;
top: 0;
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/editor.js b/frontend/src/components/editor.js
index 82606344c..b9337e5e5 100644
--- a/frontend/src/components/editor.js
+++ b/frontend/src/components/editor.js
@@ -5,7 +5,15 @@ import '@hotosm/id/dist/iD.css';
import { OSM_CONSUMER_KEY, OSM_CONSUMER_SECRET, OSM_SERVER_URL } from '../config';
-export default function Editor({ setDisable, comment, presets, imagery, gpxUrl }) {
+export default function Editor({
+ setDisable,
+ comment,
+ presets,
+ imagery,
+ gpxUrl,
+ earliestStreetImagery,
+ imageCaptureMode,
+}) {
const dispatch = useDispatch();
const session = useSelector((state) => state.auth.get('session'));
const iDContext = useSelector((state) => state.editor.context);
@@ -105,8 +113,25 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl }
setDisable(false);
}
});
+
+ if (imageCaptureMode) {
+ if (earliestStreetImagery) {
+ iDContext.photos().setDateFilter('fromDate', earliestStreetImagery.substr(0, 10), false);
+ }
+ window.location.href =
+ window.location.href + '&photo_overlay=mapillary,mapillary-map-features,mapillary-signs';
+ }
}
- }, [session, iDContext, setDisable, presets, locale, gpxUrl]);
+ }, [
+ session,
+ iDContext,
+ setDisable,
+ presets,
+ locale,
+ gpxUrl,
+ earliestStreetImagery,
+ imageCaptureMode,
+ ]);
return
;
}
diff --git a/frontend/src/components/projectCreate/messages.js b/frontend/src/components/projectCreate/messages.js
index 2b57df0e1..f27b534a0 100644
--- a/frontend/src/components/projectCreate/messages.js
+++ b/frontend/src/components/projectCreate/messages.js
@@ -85,6 +85,14 @@ export default defineMessages({
id: 'management.projects.create.trim_tasks.trim_to_aoi',
defaultMessage: 'Trim the tasks to define the exact Area of Interest for mapping.',
},
+ trimExcludeWater: {
+ id: 'management.projects.create.trim_tasks.trim_exclude_water',
+ defaultMessage: 'Trim the tasks to exclude water areas.',
+ },
+ trimCoverPathsRoads: {
+ id: 'management.projects.create.trim_tasks.trim_cover_paths_roads',
+ defaultMessage: 'Trim the tasks to only cover paths and roads.',
+ },
tinyTasks: {
id: 'management.projects.create.trim_tasks.tiny_tasks',
defaultMessage:
diff --git a/frontend/src/components/projectCreate/trimProject.js b/frontend/src/components/projectCreate/trimProject.js
index 5fcaec0b9..10fba5f5e 100644
--- a/frontend/src/components/projectCreate/trimProject.js
+++ b/frontend/src/components/projectCreate/trimProject.js
@@ -39,6 +39,9 @@ const removeTinyTasks = (metadata, updateMetadata) => {
export default function TrimProject({ metadata, mapObj, updateMetadata }) {
const token = useSelector((state) => state.auth.get('token'));
const [clipStatus, setClipStatus] = useState(false);
+ const [roadStatus, setRoadStatus] = useState(false);
+ const [waterStatus, setWaterStatus] = useState(false);
+
const [tinyTasksNumber, setTinyTasksNumber] = useState(0);
const trimTaskGridAsync = useAsync(trimTaskGrid);
@@ -72,6 +75,28 @@ export default function TrimProject({ metadata, mapObj, updateMetadata }) {
onChange={() => setClipStatus(!clipStatus)}
label={}
/>
+
+
+ {
+ setWaterStatus(!waterStatus);
+ }}
+ label={}
+ />
+
+
+ {
+ setRoadStatus(!roadStatus);
+ }}
+ label={}
+ />
+
+
diff --git a/frontend/src/components/projectEdit/imageryForm.js b/frontend/src/components/projectEdit/imageryForm.js
index e80e9d585..853cd4603 100644
--- a/frontend/src/components/projectEdit/imageryForm.js
+++ b/frontend/src/components/projectEdit/imageryForm.js
@@ -1,17 +1,22 @@
import React, { useContext, useState, useLayoutEffect } from 'react';
import Select from 'react-select';
import { FormattedMessage } from 'react-intl';
-
+import DatePicker from 'react-datepicker';
+import { SwitchToggle } from '../formInputs';
import messages from './messages';
import { StateContext, styleClasses } from '../../views/projectEdit';
import { Code } from '../code';
import { fetchLocalJSONAPI } from '../../network/genericJSONRequest';
import { useImageryOption, IMAGERY_OPTIONS } from '../../hooks/UseImageryOption';
+import { MAPILLARY_TOKEN, MAPILLARY_GRAPH_URL } from '../../config';
+import axios from 'axios';
export const ImageryForm = () => {
const { projectInfo, setProjectInfo } = useContext(StateContext);
const [licenses, setLicenses] = useState(null);
+ const [organization, setOrganization] = useState(null);
+
useLayoutEffect(() => {
const fetchLicenses = async () => {
fetchLocalJSONAPI('licenses/')
@@ -21,6 +26,15 @@ export const ImageryForm = () => {
fetchLicenses();
}, [setLicenses]);
+ if (projectInfo.mapillaryOrganizationId) {
+ axios
+ .get(
+ `${MAPILLARY_GRAPH_URL}${projectInfo.mapillaryOrganizationId}?access_token=${MAPILLARY_TOKEN}&fields=name`,
+ )
+ .then((resp) => setOrganization(resp.data.name))
+ .catch(() => setOrganization(null));
+ }
+
let defaultValue = null;
if (licenses !== null && projectInfo.licenseId !== null) {
defaultValue = licenses.filter((l) => l.licenseId === projectInfo.licenseId)[0];
@@ -34,6 +48,7 @@ export const ImageryForm = () => {
+
+
+
+
+
+
+
+
}
+ labelPosition="right"
+ isChecked={projectInfo.imageCaptureMode}
+ onChange={() =>
+ setProjectInfo({ ...projectInfo, imageCaptureMode: !projectInfo.imageCaptureMode })
+ }
+ />
+
+
+ {projectInfo.imageCaptureMode && (
+ <>
+
+
+
+ :
+
+ setProjectInfo({
+ ...projectInfo,
+ earliestStreetImagery: date,
+ })
+ }
+ placeholderText="DD/MM/YYYY"
+ dateFormat="dd/MM/yyyy"
+ className={styleClasses.inputClass}
+ showYearDropdown
+ scrollableYearDropdown
+ />
+
+
+
+
+
+
+
+
+
+
+
+
: {organization}
+
+
{
+ setProjectInfo({
+ ...projectInfo,
+ mapillaryOrganizationId: e.target.value,
+ });
+ }}
+ />
+
+ >
+ )}
);
};
diff --git a/frontend/src/components/projectEdit/messages.js b/frontend/src/components/projectEdit/messages.js
index f8c2b530d..7444c247f 100644
--- a/frontend/src/components/projectEdit/messages.js
+++ b/frontend/src/components/projectEdit/messages.js
@@ -593,6 +593,34 @@ export default defineMessages({
defaultMessage:
'This will remove the custom editor from the project. Are you sure you don\'t want to disable the custom editor by toggling the "Enabled" checkbox above?',
},
+ imageCaptureMode: {
+ id: 'projects.formInputs.imageCaptureMode',
+ defaultMessage: 'Image capture mode',
+ },
+ imageCaptureModeInfo: {
+ id: 'projects.formInputs.imageCaptureMode.info',
+ defaultMessage: 'Adapts Tasking Manager to street imagery capture workflow.',
+ },
+ imageryCaptureDate: {
+ id: 'projects.formInputs.imageryCaptureDate',
+ defaultMessage: 'Imagery capture date',
+ },
+ imageryCaptureDateAfter: {
+ id: 'projects.formInputs.imageryCaptureDate.after',
+ defaultMessage: 'After',
+ },
+ mapillaryOrganizationId: {
+ id: 'projects.formInputs.mapillaryOrganizationId',
+ defaultMessage: 'Mapillary organization ID',
+ },
+ mapillaryOrganizationIdInfo: {
+ id: 'projects.formInputs.mapillaryOrganizationId.info',
+ defaultMessage: '15-digit identifier to filter Mapillary contributions.',
+ },
+ mapillaryOrganizationSelected: {
+ id: 'projects.formInputs.mapillaryOrganizationId.selected',
+ defaultMessage: 'Organization',
+ },
noMappingEditor: {
id: 'projects.formInputs.noMappingEditor',
defaultMessage: 'At least one editor must be enabled for mapping',
diff --git a/frontend/src/components/rapidEditor.js b/frontend/src/components/rapidEditor.js
index 8129334fd..d492550a9 100644
--- a/frontend/src/components/rapidEditor.js
+++ b/frontend/src/components/rapidEditor.js
@@ -12,6 +12,8 @@ export default function RapidEditor({
imagery,
gpxUrl,
powerUser = false,
+ earliestStreetImagery,
+ imageCaptureMode,
}) {
const dispatch = useDispatch();
const session = useSelector((state) => state.auth.get('session'));
@@ -113,8 +115,31 @@ export default function RapidEditor({
setDisable(false);
}
});
+
+ if (imageCaptureMode) {
+ if (earliestStreetImagery) {
+ RapiDContext.photos().setDateFilter(
+ 'fromDate',
+ earliestStreetImagery.substr(0, 10),
+ false,
+ );
+ }
+
+ window.location.href =
+ window.location.href + '&photo_overlay=mapillary,mapillary-map-features,mapillary-signs';
+ }
}
- }, [session, RapiDContext, setDisable, presets, locale, gpxUrl, powerUser]);
+ }, [
+ session,
+ RapiDContext,
+ setDisable,
+ presets,
+ locale,
+ gpxUrl,
+ powerUser,
+ earliestStreetImagery,
+ imageCaptureMode,
+ ]);
return ;
}
diff --git a/frontend/src/components/taskSelection/action.js b/frontend/src/components/taskSelection/action.js
index 515655faa..e315439c3 100644
--- a/frontend/src/components/taskSelection/action.js
+++ b/frontend/src/components/taskSelection/action.js
@@ -199,6 +199,8 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
setDisable={setDisable}
comment={project.changesetComment}
presets={project.idPresets}
+ earliestStreetImagery={project.earliestStreetImagery}
+ imageCaptureMode={project.imageCaptureMode}
imagery={formatImageryUrlCallback(project.imagery)}
gpxUrl={getTaskGpxUrlCallback(project.projectId, tasksIds)}
/>
@@ -207,6 +209,8 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
setDisable={setDisable}
comment={project.changesetComment}
presets={project.idPresets}
+ earliestStreetImagery={project.earliestStreetImagery}
+ imageCaptureMode={project.imageCaptureMode}
imagery={formatImageryUrlCallback(project.imagery)}
gpxUrl={getTaskGpxUrlCallback(project.projectId, tasksIds)}
powerUser={project.rapidPowerUser}
diff --git a/frontend/src/components/taskSelection/actionSidebars.js b/frontend/src/components/taskSelection/actionSidebars.js
index 845b9f7cc..a05e4db71 100644
--- a/frontend/src/components/taskSelection/actionSidebars.js
+++ b/frontend/src/components/taskSelection/actionSidebars.js
@@ -418,8 +418,14 @@ export function CompletionTabForValidation({
+
-
+
{tasksIds.length > 3 && (
diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js
index 62e78bcef..24ab7351c 100644
--- a/frontend/src/components/taskSelection/footer.js
+++ b/frontend/src/components/taskSelection/footer.js
@@ -110,6 +110,14 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se
}
})
.catch((e) => lockFailed(windowObjectReference, e.message));
+
+ if (project.imageCaptureMode) {
+ if (navigator.userAgent.includes('Android')) {
+ navigate('mapillary://mapillary/explore');
+ } else if (navigator.userAgent.includes('iPad|iPhone|iPod')) {
+ navigate('mapillary://goto/camera');
+ }
+ }
}
if (['resumeMapping', 'resumeValidation'].includes(taskAction)) {
const urlParams = openEditor(
diff --git a/frontend/src/components/taskSelection/index.js b/frontend/src/components/taskSelection/index.js
index adae01fa2..71cf8b377 100644
--- a/frontend/src/components/taskSelection/index.js
+++ b/frontend/src/components/taskSelection/index.js
@@ -367,8 +367,10 @@ export function TaskSelection({ project, type, loading }: Object) {
taskBordersOnly={false}
priorityAreas={priorityAreas}
animateZoom={false}
+ earliestStreetImagery={project.earliestStreetImagery}
+ mapillaryOrganizationId={parseInt(project.mapillaryOrganizationId) || 0}
/>
-
+
diff --git a/frontend/src/components/taskSelection/legend.js b/frontend/src/components/taskSelection/legend.js
index 98b9a60e1..d2ef76cf1 100644
--- a/frontend/src/components/taskSelection/legend.js
+++ b/frontend/src/components/taskSelection/legend.js
@@ -5,7 +5,7 @@ import messages from './messages';
import { TaskStatus } from './taskList';
import { LockIcon, ChevronDownIcon, ChevronUpIcon } from '../svgIcons';
-export function TasksMapLegend() {
+export function TasksMapLegend({ imageCaptureMode }) {
const lineClasses = 'mv2 blue-dark f5';
const [expand, setExpand] = useState(true);
return (
@@ -34,6 +34,19 @@ export function TasksMapLegend() {
+ {imageCaptureMode && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
diff --git a/frontend/src/components/taskSelection/map.js b/frontend/src/components/taskSelection/map.js
index 0ed0b1711..e66a8c594 100644
--- a/frontend/src/components/taskSelection/map.js
+++ b/frontend/src/components/taskSelection/map.js
@@ -1,16 +1,25 @@
-import React, { useLayoutEffect, useState } from 'react';
+import React, { useEffect, useLayoutEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import bbox from '@turf/bbox';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import MapboxLanguage from '@mapbox/mapbox-gl-language';
import { FormattedMessage } from 'react-intl';
-
import WebglUnsupported from '../webglUnsupported';
import messages from './messages';
-import { MAPBOX_TOKEN, TASK_COLOURS, MAP_STYLE, MAPBOX_RTL_PLUGIN_URL } from '../../config';
+import {
+ MAPBOX_TOKEN,
+ TASK_COLOURS,
+ MAP_STYLE,
+ MAPBOX_RTL_PLUGIN_URL,
+ MAPILLARY_TOKEN,
+ MAPILLARY_GRAPH_URL,
+} from '../../config';
import lock from '../../assets/img/lock.png';
import redlock from '../../assets/img/red-lock.png';
+import axios from 'axios';
+import compassIcon from '../../assets/img/mapillary-compass.png';
+import { SwitchToggle } from '../formInputs';
let lockIcon = new Image(17, 20);
lockIcon.src = lock;
@@ -39,11 +48,14 @@ export const TasksMap = ({
animateZoom = true,
showTaskIds = false,
selected: selectedOnMap,
+ earliestStreetImagery = '1970-01-01T00:00:00.000000Z',
+ mapillaryOrganizationId,
}) => {
const mapRef = React.createRef();
const locale = useSelector((state) => state.preferences['locale']);
const authDetails = useSelector((state) => state.auth.get('userDetails'));
const [hoveredTaskId, setHoveredTaskId] = useState(null);
+ const [mapillaryShown, setMapillaryShown] = useState(false);
const [map, setMapObj] = useState(null);
@@ -164,6 +176,16 @@ export const TasksMap = ({
}
taskStatusCondition = [...taskStatusCondition, ...[locked, 'lock', '']];
+ map.addControl(
+ new mapboxgl.GeolocateControl({
+ positionOptions: {
+ enableHighAccuracy: true,
+ },
+ trackUserLocation: true,
+ showUserHeading: true,
+ }),
+ );
+
map.addLayer({
id: 'tasks-icon',
type: 'symbol',
@@ -197,6 +219,12 @@ export const TasksMap = ({
TASK_COLOURS.INVALIDATED,
'BADIMAGERY',
TASK_COLOURS.BADIMAGERY,
+ 'PENDING_IMAGE_CAPTURE',
+ TASK_COLOURS.PENDING_IMAGE_CAPTURE,
+ 'MORE_IMAGES_NEEDED',
+ TASK_COLOURS.MORE_IMAGES_NEEDED,
+ 'IMAGE_CAPTURE_DONE',
+ TASK_COLOURS.IMAGE_CAPTURE_DONE,
'rgba(0,0,0,0)',
],
'fill-opacity': 0.8,
@@ -205,6 +233,79 @@ export const TasksMap = ({
'tasks-icon',
);
+ map.addSource('mapillary', {
+ type: 'vector',
+ tiles: [
+ `https://tiles.mapillary.com/maps/vtp/mly1_public/2/{z}/{x}/{y}?access_token=${MAPILLARY_TOKEN}`,
+ ],
+ minzoom: 1,
+ maxzoom: 14,
+ });
+
+ map.addLayer({
+ id: 'mapillary-sequences',
+ type: 'line',
+ source: 'mapillary',
+ 'source-layer': 'sequence',
+ layout: {
+ 'line-join': 'round',
+ 'line-cap': 'round',
+ },
+ paint: {
+ 'line-color': '#05CB63',
+ 'line-width': 2,
+ },
+ layout: {
+ visibility: 'none',
+ },
+ });
+ map.addLayer({
+ id: 'mapillary-images',
+ type: 'circle',
+ source: 'mapillary',
+ 'source-layer': 'image',
+ paint: {
+ 'circle-color': '#05CB63',
+ 'circle-radius': 5,
+ },
+ layout: {
+ visibility: 'none',
+ },
+ });
+
+ map.loadImage(compassIcon, (error, image) => {
+ if (error) throw error;
+
+ map.addImage('compass', image);
+ });
+
+ map.addSource('point', {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [0, 0],
+ },
+ },
+ ],
+ },
+ });
+
+ map.addLayer({
+ id: 'mapillary-compass',
+ type: 'symbol',
+ source: 'point',
+ layout: {
+ 'icon-image': 'compass',
+ 'icon-size': 0.5,
+ visibility: 'none',
+ },
+ });
+
map.addLayer({
id: 'selected-tasks-border',
type: 'line',
@@ -233,6 +334,28 @@ export const TasksMap = ({
);
}
+ map.setFilter('mapillary-images', [
+ 'all',
+ ['>=', ['get', 'captured_at'], new Date(earliestStreetImagery).getTime()],
+ ]);
+
+ map.setFilter('mapillary-sequences', [
+ 'all',
+ ['>=', ['get', 'captured_at'], new Date(earliestStreetImagery).getTime()],
+ ]);
+
+ if (mapillaryOrganizationId > 0) {
+ map.setFilter('mapillary-images', [
+ 'all',
+ ['==', ['get', 'organization_id'], mapillaryOrganizationId],
+ ]);
+
+ map.setFilter('mapillary-sequences', [
+ 'all',
+ ['==', ['get', 'organization_id'], mapillaryOrganizationId],
+ ]);
+ }
+
if (map.getSource('tasks-outline') === undefined && taskBordersMap) {
map.addSource('tasks-outline', {
type: 'geojson',
@@ -383,6 +506,58 @@ export const TasksMap = ({
map.getCanvas().style.cursor = 'pointer';
}
});
+
+ const popup = new mapboxgl.Popup({
+ closeButton: true,
+ closeOnClick: false,
+ });
+
+ function displayMapPopup(e) {
+ popup.remove();
+
+ map.getCanvas().style.cursor = 'pointer';
+ let imageObj = e.features[0].properties;
+
+ axios
+ .get(
+ `${MAPILLARY_GRAPH_URL}${imageObj.id}?fields=thumb_256_url,computed_compass_angle,camera_type`,
+ {
+ headers: {
+ Authorization: `OAuth ${MAPILLARY_TOKEN}`,
+ },
+ },
+ )
+ .then((resp) => {
+ popup
+ .setLngLat([e.lngLat.lng, e.lngLat.lat])
+ .setHTML(`
`)
+ .addTo(map);
+
+ const geoJsonData = {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [e.lngLat.lng, e.lngLat.lat],
+ },
+ },
+ ],
+ };
+
+ map.getSource('point').setData(geoJsonData);
+ map.setLayoutProperty('mapillary-compass', 'visibility', 'visible');
+ map.setLayoutProperty(
+ 'mapillary-compass',
+ 'icon-rotate',
+ resp.data.computed_compass_angle,
+ );
+ });
+ }
+ map.on('mousemove', 'mapillary-images', (e) => displayMapPopup(e));
+ popup.on('close', () => map.setLayoutProperty('mapillary-compass', 'visibility', 'none'));
+
map.on('click', 'tasks-fill', onSelectTaskClick);
map.on('mouseleave', 'tasks-fill', function (e) {
// Change the cursor style as a UI indicator.
@@ -463,8 +638,21 @@ export const TasksMap = ({
authDetails.id,
showTaskIds,
zoomedTaskId,
+ earliestStreetImagery,
+ mapillaryOrganizationId,
]);
+ useEffect(() => {
+ if (map) {
+ mapillaryShown
+ ? map.setLayoutProperty('mapillary-images', 'visibility', 'visible')
+ : map.setLayoutProperty('mapillary-images', 'visibility', 'none');
+ mapillaryShown
+ ? map.setLayoutProperty('mapillary-sequences', 'visibility', 'visible')
+ : map.setLayoutProperty('mapillary-sequences', 'visibility', 'none');
+ }
+ }, [mapillaryShown]);
+
if (!mapboxgl.supported()) {
return ;
} else {
@@ -475,7 +663,20 @@ export const TasksMap = ({
)}
-
+
+
+ setMapillaryShown(!mapillaryShown)}
+ label={}
+ />
+
+
>
);
}
diff --git a/frontend/src/components/taskSelection/messages.js b/frontend/src/components/taskSelection/messages.js
index b856d7c3d..afa1c6662 100644
--- a/frontend/src/components/taskSelection/messages.js
+++ b/frontend/src/components/taskSelection/messages.js
@@ -309,6 +309,18 @@ export default defineMessages({
id: 'project.tasks.list.linkCopied',
defaultMessage: 'Task link copied to the clipboard',
},
+ taskStatus_PENDING_IMAGE_CAPTURE: {
+ id: 'project.tasks.pending_image_capture',
+ defaultMessage: 'Pending image capture',
+ },
+ taskStatus_MORE_IMAGES_NEEDED: {
+ id: 'project.tasks.more_images_needed',
+ defaultMessage: 'More images needed',
+ },
+ taskStatus_IMAGE_CAPTURE_DONE: {
+ id: 'project.tasks.image_capture_done',
+ defaultMessage: 'Image capture done',
+ },
taskStatus_PRIORITY_AREAS: {
id: 'project.tasks.priority_areas',
defaultMessage: 'Priority areas',
@@ -494,6 +506,11 @@ export default defineMessages({
defaultMessage:
'{number, plural, one {Is this task well mapped?} other {Are these tasks well mapped?}}',
},
+ validatedImageQuestion: {
+ id: 'project.tasks.action.options.validated_image_question',
+ defaultMessage:
+ '{number, plural, one {Is imagery capture complete for this task?} other {Is imagery capture complete for hese tasks?}}',
+ },
complete: {
id: 'project.tasks.action.options.complete',
defaultMessage: 'Yes',
@@ -586,6 +603,10 @@ export default defineMessages({
id: 'project.resources.changesets.task',
defaultMessage: "See task's changesets",
},
+ showMapillaryLayer: {
+ id: 'project.input.placeholder.mapillary',
+ defaultMessage: 'Show Mapillary layer',
+ },
taskOnOSMCha: {
id: 'project.tasks.activity.osmcha',
defaultMessage: 'View changesets in OSMCha',
diff --git a/frontend/src/components/teamsAndOrgs/teams.js b/frontend/src/components/teamsAndOrgs/teams.js
index c1f06dcfa..896037076 100644
--- a/frontend/src/components/teamsAndOrgs/teams.js
+++ b/frontend/src/components/teamsAndOrgs/teams.js
@@ -57,8 +57,8 @@ export function TeamsManagement({
)}
-
-
+
+
);
}
diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js
index aba999e9e..fbce8dd87 100644
--- a/frontend/src/config/index.js
+++ b/frontend/src/config/index.js
@@ -49,6 +49,11 @@ export const HOMEPAGE_VIDEO_URL = process.env.REACT_APP_HOMEPAGE_VIDEO_URL || ''
// Sentry.io DSN
export const SENTRY_FRONTEND_DSN = process.env.REACT_APP_SENTRY_FRONTEND_DSN;
+// Mapillary
+export const MAPILLARY_TOKEN = process.env.REACT_APP_MAPILLARY_TOKEN || '';
+export const MAPILLARY_GRAPH_URL =
+ process.env.REACT_APP_MAPILLARY_GRAPH_URL || 'https://graph.mapillary.com/';
+
// OSM API and Editor URLs
export const OSM_SERVER_URL =
process.env.REACT_APP_OSM_SERVER_URL || 'https://www.openstreetmap.org';
@@ -69,6 +74,9 @@ export const TASK_COLOURS = {
INVALIDATED: '#fceca4',
BADIMAGERY: '#d8dae4',
PRIORITY_AREAS: '#efd1d1',
+ PENDING_IMAGE_CAPTURE: '#f8e8fb',
+ MORE_IMAGES_NEEDED: '#d497e2',
+ IMAGE_CAPTURE_DONE: '#7fc874',
};
export const CHART_COLOURS = {
diff --git a/migrations/versions/7aba630f4265_.py b/migrations/versions/7aba630f4265_.py
new file mode 100644
index 000000000..74fb7c3d8
--- /dev/null
+++ b/migrations/versions/7aba630f4265_.py
@@ -0,0 +1,37 @@
+"""empty message
+
+Revision ID: 7aba630f4265
+Revises: 8a6419f289aa
+Create Date: 2022-08-08 12:54:31.656978
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = "7aba630f4265"
+down_revision = "8a6419f289aa"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column(
+ "projects", sa.Column("earliest_street_imagery", sa.DateTime(), nullable=True)
+ )
+ op.add_column(
+ "projects", sa.Column("image_capture_mode", sa.Boolean(), nullable=True)
+ )
+ op.add_column(
+ "projects", sa.Column("mapillary_organization_id", sa.String(), nullable=True)
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column("projects", "mapillary_organization_id")
+ op.drop_column("projects", "image_capture_mode")
+ op.drop_column("projects", "earliest_street_imagery")
+ # ### end Alembic commands ###