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

Restructured API endpoints for ohsomeNow stats #6021

Merged
merged 12 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ OSM_REGISTER_URL=https://www.openstreetmap.org/user/new
# It's not required to set this tag. Case it isn't set, an image will be used as background.
# TM_HOMEPAGE_VIDEO_URL=

# Endpoint for the missing maps stats
# API base URL and token(used to retrieve user stats only) for ohsomeNow Stats
#
TM_USER_STATS_API_URL=https://osm-stats-production-api.azurewebsites.net/users/
TM_HOMEPAGE_STATS_API_URL=https://osmstats-api.hotosm.org/wildcard?key=hotosm-project-*
OHSOME_STATS_BASE_URL=https://stats.now.ohsome.org/api
OHSOME_STATS_TOKEN=testSuperSecretTestToken

# Secret (required)
#
Expand Down
4 changes: 2 additions & 2 deletions frontend/.env.expand
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ REACT_APP_MAPBOX_TOKEN=$TM_MAPBOX_TOKEN
REACT_APP_ENABLE_SERVICEWORKER=$TM_ENABLE_SERVICEWORKER
REACT_APP_MAX_FILESIZE=$TM_IMPORT_MAX_FILESIZE
REACT_APP_MAX_AOI_AREA=$TM_MAX_AOI_AREA
REACT_APP_USER_STATS_API_URL=$TM_USER_STATS_API_URL
REACT_APP_HOMEPAGE_STATS_API_URL=$TM_HOMEPAGE_STATS_API_URL
REACT_APP_OHSOME_STATS_BASE_URL=$OHSOME_STATS_BASE_URL
REACT_APP_OHSOME_STATS_TOKEN=$OHSOME_STATS_TOKEN
REACT_APP_OSM_CLIENT_ID=$TM_CLIENT_ID
REACT_APP_OSM_CLIENT_SECRET=$TM_CLIENT_SECRET
REACT_APP_OSM_REDIRECT_URI=$TM_REDIRECT_URI
Expand Down
19 changes: 7 additions & 12 deletions frontend/src/api/stats.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';

import api from './apiClient';
import { HOMEPAGE_STATS_API_URL } from '../config';
import { OHSOME_STATS_BASE_URL } from '../config';

export const useSystemStatisticsQuery = () => {
const fetchSystemStats = ({ signal }) => {
Expand Down Expand Up @@ -33,7 +33,7 @@ export const useProjectStatisticsQuery = (projectId) => {

export const useOsmStatsQuery = () => {
const fetchOsmStats = ({ signal }) => {
return api().get(HOMEPAGE_STATS_API_URL, {
return api().get(`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`, {
signal,
});
};
Expand All @@ -42,27 +42,22 @@ export const useOsmStatsQuery = () => {
queryKey: ['osm-stats'],
queryFn: fetchOsmStats,
useErrorBoundary: true,
select: (data) => data.data.result
});
};

export const useOsmHashtagStatsQuery = (defaultComment) => {
const fetchOsmStats = ({ signal }) => {
return api().get(
`https://osm-stats-production-api.azurewebsites.net/stats/${defaultComment[0].replace(
'#',
'',
)}`,
{
signal,
},
);
return api().get(`${OHSOME_STATS_BASE_URL}/stats/${defaultComment[0].replace('#', '')}`, {
signal,
});
};

return useQuery({
queryKey: ['osm-hashtag-stats'],
queryFn: fetchOsmStats,
useErrorBoundary: true,
enabled: Boolean(defaultComment?.[0]),
select: (data) => data.data,
select: (data) => data.data.result,
});
};
6 changes: 3 additions & 3 deletions frontend/src/components/homepage/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export const StatsSection = () => {
<div className="pt5 pb2 ph6-l ph4 flex justify-around flex-wrap flex-nowrap-ns stats-container">
<StatsColumn
label={messages.buildingsStats}
value={hasStatsLoaded ? osmStatsData?.data.building_count_add : undefined}
value={hasStatsLoaded ? osmStatsData?.buildings : undefined}
/>
<StatsColumn
label={messages.roadsStats}
value={hasStatsLoaded ? osmStatsData?.data.road_km_add : undefined}
value={hasStatsLoaded ? osmStatsData?.roads : undefined}
/>
<StatsColumn
label={messages.editsStats}
value={hasStatsLoaded ? osmStatsData?.data.edits : undefined}
value={hasStatsLoaded ? osmStatsData?.edits : undefined}
/>
<StatsColumn
label={messages.communityStats}
Expand Down
21 changes: 9 additions & 12 deletions frontend/src/components/projectStats/edits.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import React from 'react';
import ReactTooltip from 'react-tooltip';
import { FormattedMessage, useIntl } from 'react-intl';
import { FormattedMessage } from 'react-intl';

import projectMessages from './messages';
import userDetailMessages from '../userDetail/messages';
import { MappingIcon, HomeIcon, RoadIcon, EditIcon, InfoIcon } from '../svgIcons';
import { MappingIcon, HomeIcon, RoadIcon, EditIcon } from '../svgIcons';
import { StatsCard } from '../statsCard';
import StatsTimestamp from '../statsTimestamp';

export const EditsStats = ({ data }) => {
const intl = useIntl();
const { changesets, buildings, roads, edits } = data;

const iconClass = 'h-50 w-50';
const iconStyle = { height: '45px' };

return (
<div className="cf w-100 pb4 ph2 ph4-ns blue-dark">
<h3 className="barlow-condensed ttu f3">
<FormattedMessage {...projectMessages.edits} />
<InfoIcon
data-tip={intl.formatMessage(projectMessages.editsStats)}
className="blue-grey h1 w1 v-mid pb1 ml2"
/>
</h3>
<ReactTooltip place="top" className="mw6" effect="solid" />
<div className="flex items-center">
<h3 className="barlow-condensed ttu f3">
<FormattedMessage {...projectMessages.edits} />
</h3>
<StatsTimestamp messageType="project" />
</div>
<div className="db pb2 project-edit-stats">
<StatsCard
field={'changesets'}
Expand Down
4 changes: 0 additions & 4 deletions frontend/src/components/projectStats/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,4 @@ export default defineMessages({
id: 'project.stats.edits',
defaultMessage: 'Edits',
},
editsStats: {
id: 'project.stats.edits.info',
defaultMessage: 'These stats are retrieved using the default changeset comment of the project',
},
});
44 changes: 44 additions & 0 deletions frontend/src/components/statsTimestamp/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import ReactTooltip from 'react-tooltip';

import { fetchExternalJSONAPI } from '../../network/genericJSONRequest';
import { OHSOME_STATS_BASE_URL } from '../../config';
import { InfoIcon } from '../svgIcons';
import messages from './messages';

function StatsTimestamp({ messageType }) {
const intl = useIntl();
const [lastUpdated, setLastUpdated] = useState(null);

useEffect(() => {
fetchExternalJSONAPI(`${OHSOME_STATS_BASE_URL}/metadata`)
.then((res) => {
setLastUpdated(res.result.max_timestamp);
})
.catch((error) => console.error(error));
}, []);

const dateOptions = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
};

return (
<div>
<InfoIcon
className="blue-grey h1 w1 v-mid ml2 pointer"
data-tip={intl.formatMessage(messages[messageType], {
formattedDate: intl.formatDate(lastUpdated, dateOptions),
})}
data-for="ohsome-timestamp"
/>
<ReactTooltip id="ohsome-timestamp" place="top" className="mw6" effect="solid" />
</div>
);
}

export default StatsTimestamp;
13 changes: 13 additions & 0 deletions frontend/src/components/statsTimestamp/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineMessages } from 'react-intl';

export default defineMessages({
generic: {
id: 'stats.ohsome.timestamp.generic',
defaultMessage: 'These statistics come from ohsomeNow Stats and were last updated at {formattedDate}. Missing fields will be made available soon!',
},
project: {
id: 'stats.ohsome.timestamp.project',
defaultMessage:
'These stats were retrieved using the default changeset comment of the project and were last updated at {formattedDate}',
},
});
75 changes: 44 additions & 31 deletions frontend/src/components/teamsAndOrgs/featureStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { FormattedMessage } from 'react-intl';

import messages from './messages';
import userDetailMessages from '../userDetail/messages';
import { HOMEPAGE_STATS_API_URL } from '../../config';
import { OHSOME_STATS_BASE_URL } from '../../config';
import { RoadIcon, HomeIcon, WavesIcon, MarkerIcon } from '../svgIcons';
import { StatsCard } from '../statsCard';
import StatsTimestamp from '../statsTimestamp';

export const FeatureStats = () => {
const [stats, setStats] = useState({ edits: 0, buildings: 0, roads: 0, pois: 0, waterways: 0 });
const getStats = async () => {
try {
const response = await axios.get(HOMEPAGE_STATS_API_URL);
const response = await axios.get(
`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`,
);
const { edits, buildings, roads } = response.data.result;
setStats({
edits: response.data.edits,
buildings: response.data.building_count_add,
roads: response.data.road_km_add,
edits,
buildings,
roads,
pois: response.data.poi_count_add,
waterways: response.data.waterway_km_add,
});
Expand All @@ -32,31 +37,39 @@ export const FeatureStats = () => {
const iconStyle = { height: '45px' };

return (
<div className="w-100 cf">
<StatsCard
icon={<HomeIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.buildingsMapped} />}
value={stats.buildings || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<RoadIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.roadMapped} />}
value={stats.roads || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<MarkerIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.poiMapped} />}
value={stats.pois || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<WavesIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.waterwaysMapped} />}
value={stats.waterways || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
</div>
<>
<div className="flex items-center">
<h4 className="f3 fw6 ttu barlow-condensed blue-dark">
<FormattedMessage {...messages.totalFeatures} />
</h4>
<StatsTimestamp messageType="generic" />
</div>
<div className="w-100 cf">
<StatsCard
icon={<HomeIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.buildingsMapped} />}
value={stats.buildings || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<RoadIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.roadMapped} />}
value={stats.roads || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<MarkerIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.poiMapped} />}
value={stats.pois || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
<StatsCard
icon={<WavesIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...userDetailMessages.waterwaysMapped} />}
value={stats.waterways || 0}
className={'w-25-l w-50-m w-100 mv1'}
/>
</div>
</>
);
};
4 changes: 4 additions & 0 deletions frontend/src/components/teamsAndOrgs/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,4 +553,8 @@ export default defineMessages({
id: 'management.stats.overview',
defaultMessage: 'Overview',
},
totalFeatures: {
id: 'management.stats.features',
defaultMessage: 'Total features',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ test('FeatureStats renders the correct values and labels', async () => {
expect(screen.getByText('Km waterways mapped')).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('2,380,562')).toBeInTheDocument());
expect(screen.getByText('101,367,027')).toBeInTheDocument();
expect(screen.getByText('183,011')).toBeInTheDocument();
expect(screen.getByText('350,906')).toBeInTheDocument();
// Uncomment the following when POIs and waterways become available
// expect(screen.getByText('183,011')).toBeInTheDocument();
// expect(screen.getByText('350,906')).toBeInTheDocument();
});
4 changes: 2 additions & 2 deletions frontend/src/components/userDetail/editsByNumbers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const EditsByNumbers = ({ osmStats }) => {
let reference = [
{
label: intl.formatMessage(typesMessages.buildings),
field: 'total_building_count_add',
field: 'buildings',
backgroundColor: CHART_COLOURS.red,
borderColor: CHART_COLOURS.white,
},
{
label: intl.formatMessage(typesMessages.roads),
field: 'total_road_km_add',
field: 'roads',
backgroundColor: CHART_COLOURS.green,
borderColor: CHART_COLOURS.white,
},
Expand Down
18 changes: 4 additions & 14 deletions frontend/src/components/userDetail/elementsMapped.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import humanizeDuration from 'humanize-duration';
import ReactTooltip from 'react-tooltip';
import { FormattedMessage } from 'react-intl';

import messages from './messages';
Expand All @@ -10,11 +9,11 @@ import {
HomeIcon,
WavesIcon,
MarkerIcon,
QuestionCircleIcon,
MappedIcon,
ValidatedIcon,
} from '../svgIcons';
import { StatsCard } from '../statsCard';
import StatsTimestamp from '../statsTimestamp';

export const TaskStats = ({ userStats, username }) => {
const {
Expand Down Expand Up @@ -137,12 +136,12 @@ export const ElementsMapped = ({ userStats, osmStats }) => {
<StatsCard
icon={<HomeIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...messages.buildingsMapped} />}
value={osmStats.total_building_count_add || 0}
value={osmStats.buildings || 0}
/>
<StatsCard
icon={<RoadIcon className={iconClass} style={iconStyle} />}
description={<FormattedMessage {...messages.roadMapped} />}
value={osmStats.total_road_km_add || 0}
value={osmStats.roads || 0}
/>
<StatsCard
icon={<MarkerIcon className={iconClass} style={iconStyle} />}
Expand All @@ -156,16 +155,7 @@ export const ElementsMapped = ({ userStats, osmStats }) => {
/>
</div>
<div className="cf w-100 relative tr pt3 pr3">
<FormattedMessage {...messages.delayPopup}>
{(msg) => (
<QuestionCircleIcon
className="pointer dib v-mid pl2 pb1 blue-light"
height="1.25rem"
data-tip={msg}
/>
)}
</FormattedMessage>
<ReactTooltip />
<StatsTimestamp messageType="generic" />
</div>
</div>
);
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/components/userDetail/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,6 @@ export default defineMessages({
id: 'users.detail.heatmapLegendLess',
defaultMessage: 'less',
},
delayPopup: {
id: 'users.detail.delay_popup',
defaultMessage:
'These statistics need heavy calculations and changes are showing up with a delay of around one hour.',
},
teams: {
id: 'users.detail.teams',
defaultMessage: 'Teams',
Expand Down
Loading
Loading