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 ###