Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/live monitoring #40

Merged
merged 5 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 172 additions & 54 deletions frontend/src/views/projectLiveMonitoring.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import ReactPlaceholder from 'react-placeholder';
import Select from 'react-select';
import centroid from '@turf/centroid';
import {
UnderpassFeatureList,
Expand All @@ -16,9 +17,52 @@ import { useFetch } from '../hooks/UseFetch';
import './projectLiveMonitoring.css';
import { MAPBOX_TOKEN } from '../config';

const availableImageryOptions = [
{ label: 'Bing', value: 'Bing' },
{ label: 'Mapbox Satellite', value: 'Mapbox' },
{ label: 'ESRI World Imagery', value: 'EsriWorldImagery' },
];

const availableImageryValues = availableImageryOptions.map((item) => item.value);

const config = {
API_URL: `https://underpass.live:8000`,
MAPBOX_TOKEN: MAPBOX_TOKEN,
// set default sources of Tasking Manager
sources: {
osm: {
type: 'raster',
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap Contributors',
maxzoom: 19,
},
Mapbox: {
type: 'raster',
tiles: [
`https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/{z}/{x}/{y}?access_token=${MAPBOX_TOKEN}`,
],
tileSize: 512,
attribution: '© OpenStreetMap Contributors © Mapbox',
maxzoom: 19,
},
EsriWorldImagery: {
type: 'raster',
tiles: [
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
],
tileSize: 256,
attribution: '© OpenStreetMap Contributors © ESRI',
maxzoom: 18,
},
Bing: {
type: 'raster',
tiles: ['http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=1'],
tileSize: 256,
attribution: '© OpenStreetMap Contributors',
maxzoom: 18,
},
},
};

const statusList = {
Expand All @@ -34,21 +78,27 @@ const mappingTypesTags = {
WATERWAYS: 'waterway',
};

const mappingTypesFeatureTypes = {
ROADS: 'line',
BUILDINGS: 'polygon',
WATERWAYS: 'line',
};

export function ProjectLiveMonitoring() {
const { id } = useParams();
const [coords, setCoords] = useState([0, 0]);
const [activeFeature, setActiveFeature] = useState(null);
const [tags, setTags] = useState('building');
const [hashtag, setHashtag] = useState('hotosm-project-' + id);
const [featureType, setFeatureType] = useState('polygon');
const [mapSource, setMapSource] = useState('osm');
const [imageryOptions, setImageryOptions] = useState(availableImageryOptions);
const [mapConfig, setMapConfig] = useState(config);
const [realtimeList, setRealtimeList] = useState(false);
const [realtimeMap, setRealtimeMap] = useState(false);
const [status, setStatus] = useState(statusList.UNSQUARED);
// eslint-disable-next-line
const [area, setArea] = useState(null);
const tagsInputRef = useRef('');
const hashtagInputRef = useRef('');
const styleSelectRef = useRef();

useSetTitleTag(`Project #${id} Live Monitoring`);
const [error, loading, data] = useFetch(`projects/${id}/`, id);
Expand All @@ -57,7 +107,33 @@ export function ProjectLiveMonitoring() {
const [project, setProject] = useState(null);

useEffect(() => {
if (!Object.keys(data).length) return;
setProject(data);
// add custom to config sources if the project has custom imagery
const hasCustomImagery = data.imagery.includes('http');
if (hasCustomImagery) {
setMapConfig((prev) => ({
...prev,
sources: {
...prev.sources,
custom: {
type: 'raster',
tiles: [data.imagery],
tileSize: 256,
attribution: 'custom',
maxzoom: 19,
},
},
}));
setImageryOptions((prev) => [...prev, { label: 'Custom', value: 'custom' }]);
}
// set mapSource after data fetch
const mapSourceValue = hasCustomImagery
? 'custom'
: availableImageryValues.includes(data.imagery)
? data.imagery
: 'Bing';
setMapSource(mapSourceValue);
}, [data]);

useEffect(() => {
Expand Down Expand Up @@ -85,6 +161,7 @@ export function ProjectLiveMonitoring() {
].join(','),
);
setTags(mappingTypesTags[project.mappingTypes] || 'building');
setFeatureType(mappingTypesFeatureTypes[project.mappingTypes] || 'polygon');
}
}, [project]);

Expand Down Expand Up @@ -113,12 +190,11 @@ export function ProjectLiveMonitoring() {
const handleFilterClick = (e) => {
e.preventDefault();
setTags(tagsInputRef.current.value);
setHashtag(hashtagInputRef.current.value);
return false;
};

const handleMapSourceSelect = (e) => {
setMapSource(e.target.options[e.target.selectedIndex].value);
const handleMapSourceSelect = (selectedItem) => {
setMapSource(selectedItem.value);
};

const handleMapMove = ({ bbox }) => {
Expand Down Expand Up @@ -151,41 +227,32 @@ export function ProjectLiveMonitoring() {
defaultValue="building"
/>
 
<input
className="border px-2 py-2 text-sm"
type="text"
placeholder="hashtag (ex: hotosm-project)"
ref={hashtagInputRef}
defaultValue={'hotosm-project-' + id}
/>
&nbsp;
<button
className="inline-flex items-center rounded bg-primary px-2 py-2 text-sm font-medium text-white"
onClick={handleFilterClick}
>
Search
</button>
</form>
<select
<Select
classNamePrefix="react-select"
isClearable={true}
value={imageryOptions.find((item) => item.value === mapSource)}
options={imageryOptions}
// placeholder={<FormattedMessage {...messages.selectImagery} />}
onChange={handleMapSourceSelect}
ref={styleSelectRef}
className="border mt-2 bg-white px-2 py-2 text-sm"
>
<option value="osm">OSM</option>
<option value="bing">Bing</option>
<option value="esri">ESRI</option>
<option value="mapbox">Mapbox</option>
<option value="oam">OAM</option>
</select>
className="w-50 z-2"
/>
</div>
<UnderpassMap
center={coords}
tags={tags}
hashtag={hashtag}
hashtag={'hotosm-project-' + id}
featureType={featureType}
highlightDataQualityIssues
popupFeature={activeFeature}
source={mapSource}
config={config}
config={mapConfig}
realtime={realtimeMap}
theme={demoTheme}
zoom={17}
Expand All @@ -197,26 +264,30 @@ export function ProjectLiveMonitoring() {
style={{
flex: 1,
padding: 10,
display: 'flex',
flexDirection: 'column',
backgroundColor: `rgb(${hottheme.colors.white})`,
}}
>
<div className="border-b-2 pb-5 space-y-3">
<UnderpassFeatureStats
tags={tags}
hashtag={hashtag}
hashtag={'hotosm-project-' + id}
featureType={featureType}
apiUrl={config.API_URL}
area={areaOfInterest}
/>
<UnderpassValidationStats
tags={tags}
hashtag={hashtag}
hashtag={'hotosm-project-' + id}
featureType={featureType}
apiUrl={config.API_URL}
status="badgeom"
area={areaOfInterest}
/>
</div>
<div className="border-b-2 py-5 mb-5">
<form className="space-x-2 mb-3">
<div className="border-b-2 py-5 mb-4">
<form className="space-x-2">
<input
onChange={() => {
setRealtimeList(!realtimeList);
Expand All @@ -234,7 +305,7 @@ export function ProjectLiveMonitoring() {
/>
<label target="liveMapCheckbox">Live map</label>
</form>
<form className="space-x-2">
<form className="space-x-2 py-4">
<input
checked={status === statusList.ALL}
onChange={() => {
Expand Down Expand Up @@ -266,30 +337,77 @@ export function ProjectLiveMonitoring() {
/>
<label htmlFor="semanticCheckbox">Semantic</label>
</form>
<form className="space-x-2">
<input
checked={featureType === 'all'}
onChange={() => {
setFeatureType('all');
}}
name="featureTypeAllCheckbox"
id="featureTypeAllCheckbox"
type="radio"
/>
<label htmlFor="featureTypeAllCheckbox">All</label>
<input
checked={featureType === 'polygon'}
onChange={() => {
setFeatureType('polygon');
}}
name="featureTypePolygonCheckbox"
id="featureTypePolygonCheckbox"
type="radio"
/>
<label htmlFor="featureTypePolygonCheckbox">Polygon</label>
<input
checked={featureType === 'line'}
onChange={() => {
setFeatureType('line');
}}
name="featureTypeLineCheckbox"
id="featureTypeLineCheckbox"
type="radio"
/>
<label htmlFor="featureTypeLineCheckbox">Line</label>
<input
checked={featureType === 'node'}
onChange={() => {
setFeatureType('node');
}}
name="featureTypeNodeCheckbox"
id="featureTypeNodeCheckbox"
type="radio"
/>
<label htmlFor="featureTypeNodeCheckbox">Node</label>
</form>
</div>
<div style={{ height: '512px', overflow: 'hidden' }}>
<UnderpassFeatureList
tags={tags}
hashtag={hashtag}
page={0}
area={areaOfInterest}
onSelect={(feature) => {
setCoords([feature.lat, feature.lon]);
const tags = JSON.stringify(feature.tags);
const status = feature.status;
setActiveFeature({ properties: { tags, status }, ...feature });
}}
realtime={realtimeList}
config={config}
status={status}
orderBy="created_at"
onFetchFirstTime={(mostRecentFeature) => {
if (mostRecentFeature) {
setCoords([mostRecentFeature.lat, mostRecentFeature.lon]);
}
}}
/>
</div>
<UnderpassFeatureList
style={{
display: 'flex',
'flex-flow': 'column',
height: '100px',
flex: '1 1 auto',
}}
tags={tags}
hashtag={'hotosm-project-' + id}
featureType={featureType}
page={0}
area={areaOfInterest}
onSelect={(feature) => {
setCoords([feature.lat, feature.lon]);
const tags = JSON.stringify(feature.tags);
const status = feature.status;
setActiveFeature({ properties: { tags, status }, ...feature });
}}
realtime={realtimeList}
config={config}
status={status}
orderBy="created_at"
onFetchFirstTime={(mostRecentFeature) => {
if (mostRecentFeature) {
setCoords([mostRecentFeature.lat, mostRecentFeature.lon]);
}
}}
/>
</div>
</div>
</div>
Expand Down
Loading
Loading