diff --git a/example.env b/example.env index 51c2e37818..9afc83f9d2 100644 --- a/example.env +++ b/example.env @@ -204,3 +204,8 @@ TM_DEFAULT_LOCALE=en # Sentry.io DSN Config (optional) # TM_SENTRY_BACKEND_DSN=https://foo.ingest.sentry.io/1234567 # TM_SENTRY_FRONTEND_DSN=https://bar.ingest.sentry.io/8901234 + + +EXPORT TOOL Integration with 0(Disable) and 1(Enable) and S3 URL for Export Tool +#EXPORT_TOOL_S3_URL=https://foorawdataapi.s3.amazonaws.com +#ENABLE_EXPORT_TOOL=0 \ No newline at end of file diff --git a/frontend/.env.expand b/frontend/.env.expand index 29585a9c42..58cf2498ab 100644 --- a/frontend/.env.expand +++ b/frontend/.env.expand @@ -43,3 +43,5 @@ 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_EXPORT_TOOL_S3_URL=$EXPORT_TOOL_S3_URL +REACT_APP_ENABLE_EXPORT_TOOL=$ENABLE_EXPORT_TOOL diff --git a/frontend/src/assets/styles/index.scss b/frontend/src/assets/styles/index.scss index bab0e3f339..fa5ce80aa6 100644 --- a/frontend/src/assets/styles/index.scss +++ b/frontend/src/assets/styles/index.scss @@ -11,3 +11,17 @@ @import 'learn'; @import 'notifications'; @import 'contributions'; + +.fade-in { + opacity: 0; + transition: opacity 0.5s ease-in; +} + +.fade-in.active { + opacity: 1; +} + +.categorycard:hover > svg *, +.categorycard:hover { + fill: #d73f3f; +} diff --git a/frontend/src/components/projectDetail/downloadOsmData.js b/frontend/src/components/projectDetail/downloadOsmData.js new file mode 100644 index 0000000000..aea68bace2 --- /dev/null +++ b/frontend/src/components/projectDetail/downloadOsmData.js @@ -0,0 +1,231 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { RoadIcon, HomeIcon, WavesIcon, TaskIcon, DownloadIcon } from '../svgIcons'; +import FileFormatCard from './fileFormatCard'; +import Popup from 'reactjs-popup'; +import { EXPORT_TOOL_S3_URL } from '../../config'; +import messages from './messages'; +import { FormattedMessage } from 'react-intl'; + +export const TITLED_ICONS = [ + { + Icon: RoadIcon, + title: 'roads', + value: 'ROADS', + featuretype: ['lines'], + formats: ['geojson', 'shp', 'kml'], + }, + { + Icon: HomeIcon, + title: 'buildings', + value: 'BUILDINGS', + featuretype: ['polygons'], + formats: ['geojson', 'shp', 'kml'], + }, + { + Icon: WavesIcon, + title: 'waterways', + value: 'WATERWAYS', + featuretype: ['lines', 'polygons'], + formats: ['geojson', 'shp', 'kml'], + }, + { + Icon: TaskIcon, + title: 'landuse', + value: 'LAND_USE', + featuretype: ['points', 'polygons'], + formats: ['geojson', 'shp', 'kml'], + }, +]; + +/** + * Renders a list of download options for OSM data based on the project mapping types. + * + * @param {Array} projectMappingTypes - The mapping types of the project. + * @return {JSX.Element} - The JSX element containing the download options. + */ + +export const DownloadOsmData = ({ projectMappingTypes, project }) => { + const [showPopup, setShowPopup] = useState(false); + const [isDownloadingState, setIsDownloadingState] = useState(null); + const [selectedCategoryFormat, setSelectedCategoryFormat] = useState(null); + + const datasetConfig = { + dataset_prefix: `hotosm_project_${project.projectId}`, + dataset_folder: 'TM', + dataset_title: `Tasking Manger Project ${project.projectId}`, + }; + /** + * Downloads an S3 file from the given URL and saves it as a file. + * + * @param {string} title - The title of the file. + * @param {string} fileFormat - The format of the file. + * @param {string} feature_type - The feature type of the ffile. + * @return {Promise} Promise that resolves when the download is complete. + */ + const downloadS3File = async (title, fileFormat, feature_type) => { + // Create the base URL for the S3 file + const baseUrl = `${EXPORT_TOOL_S3_URL}/${datasetConfig.dataset_folder}/${ + datasetConfig.dataset_prefix + }/${title}/${feature_type}/${ + datasetConfig.dataset_prefix + }_${title}_${feature_type}_${fileFormat.toLowerCase()}.zip`; + + // Set the state to indicate that the file download is in progress + setIsDownloadingState({ title: title, fileFormat: fileFormat, isDownloading: true }); + + try { + // Fetch the file from the S3 URL + const response = await fetch(baseUrl); + + // Check if the request was successful + if (response.ok) { + // Get the file data as a blob + const blob = await response.blob(); + + // Create a download link for the file + const href = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.setAttribute( + 'download', + `hotosm_project_${ + project.projectId + }_${title}_${feature_type}_${fileFormat?.toLowerCase()}.zip`, + ); + + // Add the link to the document body, click it, and then remove it + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + // Set the state to indicate that the file download is complete + setIsDownloadingState({ title: title, fileFormat: fileFormat, isDownloading: false }); + } else { + // Show a popup and throw an error if the request was not successful + setShowPopup(true); + throw new Error(`Request failed with status: ${response.status}`); + } + } catch (error) { + // Show a popup and log the error if an error occurs during the download + setShowPopup(true); + setIsDownloadingState({ title: title, fileFormat: fileFormat, isDownloading: false }); + console.error('Error:', error.message); + } + }; + const filteredMappingTypes = TITLED_ICONS?.filter((icon) => + projectMappingTypes?.includes(icon.value), + ); + return ( +
+ setShowPopup(false)}> + {(close) => ( +
+

+ +

+

+ +

+
+ +
+
+ )} +
+ {filteredMappingTypes.map((type) => ( +
+
+ +
+
+
+

{type.title}

+ +
+
+ {selectedCategoryFormat && + selectedCategoryFormat.title === type.title && + type?.featuretype?.map((typ) => ( + + downloadS3File( + selectedCategoryFormat.title, + selectedCategoryFormat.format, + typ, + ) + } + onKeyUp={() => + downloadS3File( + selectedCategoryFormat.title, + selectedCategoryFormat.format, + typ, + ) + } + className="flex flex-row items-center pointer link hover-red color-inherit categorycard" + style={{ gap: '10px' }} + > + +

+ {typ} {selectedCategoryFormat.format} +

+
+ ))} +
+
+
+ ))} +
+ ); +}; + +DownloadOsmData.propTypes = { + projectMappingTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + project: PropTypes.objectOf(PropTypes.any).isRequired, +}; diff --git a/frontend/src/components/projectDetail/fileFormatCard.js b/frontend/src/components/projectDetail/fileFormatCard.js new file mode 100644 index 0000000000..fc2c11b24c --- /dev/null +++ b/frontend/src/components/projectDetail/fileFormatCard.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { AnimatedLoadingIcon } from '../button'; +import PropTypes from 'prop-types'; + +/** + * Renders a list of file formats as clickable links. + * + * @param {string} props.title - The title of the card. + * @param {Object[]} fileFormats - An array of file format objects. + * @param {Object} isDownloadingState - The downloading state object. + * @param {function} setSelectedCategoryFormat - The function to set the selected category format. + * @param {Object} selectedCategoryFormat - The selected category format object. + * @return {JSX.Element} The rendered list of file formats. + */ + +function FileFormatCard({ + title, + fileFormats, + isDownloadingState, + setSelectedCategoryFormat, + selectedCategoryFormat, +}) { + return ( + <> + {fileFormats.map((fileFormat, index) => { + const loadingState = + isDownloadingState?.isDownloading && + isDownloadingState?.title === title && + isDownloadingState?.fileFormat === fileFormat; + + return ( + + setSelectedCategoryFormat({ title, format: fileFormat })} + onKeyUp={() => setSelectedCategoryFormat({ title, format: fileFormat })} + className={`link ${ + selectedCategoryFormat === fileFormat && selectedCategoryFormat.title === title + ? 'red' + : '' + } hover-red color-inherit`} + > +

+ {fileFormat} + {loadingState ? : null} +

+
+ {index !== fileFormats.length - 1 &&
} +
+ ); + })} + + ); +} + +export default FileFormatCard; + +FileFormatCard.propTypes = { + title: PropTypes.string, + fileFormats: PropTypes.arrayOf(PropTypes.object), + isDownloadingState: PropTypes.bool, + setSelectedCategoryFormat: PropTypes.func, + selectedCategoryFormat: PropTypes.objectOf({ + title: PropTypes.string, + format: PropTypes.PropTypes.string, + }), +}; diff --git a/frontend/src/components/projectDetail/footer.js b/frontend/src/components/projectDetail/footer.js index 09cc1ad65a..a14f13abd6 100644 --- a/frontend/src/components/projectDetail/footer.js +++ b/frontend/src/components/projectDetail/footer.js @@ -36,6 +36,10 @@ const menuItems = [ href: '#contributions', label: , }, + { + href: '#downloadOsmData', + label: , + }, { href: '#similarProjects', label: , diff --git a/frontend/src/components/projectDetail/index.js b/frontend/src/components/projectDetail/index.js index 675113cbc4..54a7e83193 100644 --- a/frontend/src/components/projectDetail/index.js +++ b/frontend/src/components/projectDetail/index.js @@ -4,6 +4,7 @@ import ReactPlaceholder from 'react-placeholder'; import centroid from '@turf/centroid'; import { FormattedMessage } from 'react-intl'; import { supported } from 'mapbox-gl'; +import PropTypes from 'prop-types'; import messages from './messages'; import viewsMessages from '../../views/messages'; @@ -26,6 +27,8 @@ import { Alert } from '../alert'; import './styles.scss'; import { useWindowSize } from '../../hooks/UseWindowSize'; +import { DownloadOsmData } from './downloadOsmData.js'; +import { ENABLE_EXPORT_TOOL } from '../../config/index.js'; /* lazy imports must be last import */ const ProjectTimeline = React.lazy(() => import('./timeline' /* webpackChunkName: "timeline" */)); @@ -78,7 +81,13 @@ const ProjectDetailMap = (props) => { }} >
- setTaskBordersOnly(false)} className="pb2 mh2 pointer ph2"> + setTaskBordersOnly(false)} + onKeyDown={() => setTaskBordersOnly(false)} + className="pb2 mh2 pointer ph2 " + >
@@ -88,7 +97,7 @@ const ProjectDetailMap = (props) => { ); }; -export const ProjectDetailLeft = ({ project, contributors, className, type }: Object) => { +export const ProjectDetailLeft = ({ project, contributors, className, type }) => { const htmlShortDescription = project.projectInfo && htmlFromMarkdown(project.projectInfo.shortDescription); @@ -285,6 +294,26 @@ export const ProjectDetail = (props) => { /> )} + + {/* Download OSM Data section Start */} + {/* Converted String to Integer */} + {+ENABLE_EXPORT_TOOL === 1 && ( +
+ + + +

+ +

+ +
+ )} + + {/* Download OSM Data section End */} + @@ -340,3 +369,46 @@ export const ProjectDetail = (props) => { ); }; + +ProjectDetail.propTypes = { + project: PropTypes.shape({ + projectId: PropTypes.number, + projectInfo: PropTypes.shape({ + description: PropTypes.string, + }), + mappingTypes: PropTypes.arrayOf(PropTypes.any).isRequired, + author: PropTypes.string, + organisationName: PropTypes.string, + organisationSlug: PropTypes.string, + organisationLogo: PropTypes.string, + mappingPermission: PropTypes.string, + validationPermission: PropTypes.string, + teams: PropTypes.arrayOf(PropTypes.object), + }).isRequired, + className: PropTypes.string, +}; + +ProjectDetailMap.propTypes = { + project: PropTypes.shape({ + areaOfInterest: PropTypes.object, + priorityAreas: PropTypes.arrayOf(PropTypes.object), + }).isRequired, + tasks: PropTypes.arrayOf(PropTypes.object), + navigate: PropTypes.func, + type: PropTypes.string, + tasksError: PropTypes.string, + projectLoading: PropTypes.bool, +}; + +ProjectDetailLeft.propTypes = { + project: PropTypes.shape({ + projectInfo: PropTypes.shape({ + shortDescription: PropTypes.string, + }), + projectId: PropTypes.number, + tasks: PropTypes.arrayOf(PropTypes.object), + }).isRequired, + contributors: PropTypes.arrayOf(PropTypes.object), + className: PropTypes.string, + type: PropTypes.string, +}; diff --git a/frontend/src/components/projectDetail/messages.js b/frontend/src/components/projectDetail/messages.js index 8298442b66..76e452b827 100644 --- a/frontend/src/components/projectDetail/messages.js +++ b/frontend/src/components/projectDetail/messages.js @@ -223,6 +223,19 @@ export default defineMessages({ id: 'project.detail.sections.contributionsTimeline', defaultMessage: 'Contributions timeline', }, + downloadOsmData: { + id: 'project.detail.sections.downloadOsmData', + defaultMessage: 'Download OSM Data', + }, + errorDownloadOsmData: { + id: 'project.detail.sections.errorDownloadOsmData', + defaultMessage: 'Data Extraction Unavailable', + }, + errorDownloadOsmDataDescription: { + id: 'project.detail.sections.errorDownloadOsmDataDescription', + defaultMessage: + 'The data extract you are attempting to download is currently inactive or unavailable. Please ensure that the extract is active and try again later.', + }, viewInOsmcha: { id: 'project.detail.sections.contributions.osmcha', defaultMessage: 'Changesets in OSMCha', diff --git a/frontend/src/components/projectDetail/styles.scss b/frontend/src/components/projectDetail/styles.scss index 8e8aab2e1a..d454a55656 100644 --- a/frontend/src/components/projectDetail/styles.scss +++ b/frontend/src/components/projectDetail/styles.scss @@ -26,3 +26,10 @@ .menu-items-container { scrollbar-width: none; } + +.file-list-separator { + top: 13px; + height: 27px; + position: relative; + border: 1px solid #d73f3f; +} diff --git a/frontend/src/components/svgIcons/download.js b/frontend/src/components/svgIcons/download.js new file mode 100644 index 0000000000..61d0399042 --- /dev/null +++ b/frontend/src/components/svgIcons/download.js @@ -0,0 +1,21 @@ +import React from 'react'; + +export class DownloadIcon extends React.PureComponent { + render() { + return ( + + + + ); + } +} diff --git a/frontend/src/components/svgIcons/index.js b/frontend/src/components/svgIcons/index.js index b59491f78d..c828d472a3 100644 --- a/frontend/src/components/svgIcons/index.js +++ b/frontend/src/components/svgIcons/index.js @@ -82,3 +82,4 @@ export { CutIcon } from './cut'; export { FileImportIcon } from './fileImport'; export { CalendarIcon } from './calendar'; export { CommentIcon } from './comment'; +export { DownloadIcon } from './download'; diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index 41074cd781..343f7c3171 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -59,6 +59,8 @@ export const POTLATCH2_EDITOR_URL = 'https://www.openstreetmap.org/edit?editor=potlatch2'; export const RAPID_EDITOR_URL = process.env.REACT_APP_RAPID_EDITOR_URL || 'https://mapwith.ai/rapid'; +export const EXPORT_TOOL_S3_URL = process.env.REACT_APP_EXPORT_TOOL_S3_URL || ''; +export const ENABLE_EXPORT_TOOL = process.env.REACT_APP_ENABLE_EXPORT_TOOL || ''; export const TASK_COLOURS = { READY: '#fff',