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

Add new map filters #1090

Merged
merged 18 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions packages/api/src/sites/sites.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SketchFab } from 'site-sketchfab/site-sketchfab.entity';
import { ReefCheckSurvey } from '../reef-check-surveys/reef-check-surveys.entity';
import { SitesController } from './sites.controller';
import { SitesService } from './sites.service';
import { Site } from './sites.entity';
Expand Down Expand Up @@ -33,6 +34,7 @@ import { ScheduledUpdate } from './scheduled-updates.entity';
TimeSeries,
ScheduledUpdate,
SketchFab,
ReefCheckSurvey,
]),
],
controllers: [SitesController],
Expand Down
10 changes: 10 additions & 0 deletions packages/api/src/sites/sites.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DataSource, Repository } from 'typeorm';
import { omit } from 'lodash';
import Bluebird from 'bluebird';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { ReefCheckSurvey } from 'reef-check-surveys/reef-check-surveys.entity';
import { DateTime } from '../luxon-extensions';
import { Site, SiteStatus } from './sites.entity';
import { DailyData } from './daily-data.entity';
Expand All @@ -29,6 +30,7 @@ import {
getLatestData,
getSite,
createSite,
getReefCheckDataSubQuery,
} from '../utils/site.utils';
import { getSpotterData, sofarLatest } from '../utils/sofar';
import { ExclusionDates } from './exclusion-dates.entity';
Expand Down Expand Up @@ -88,6 +90,9 @@ export class SitesService {
@InjectRepository(ScheduledUpdate)
private scheduledUpdateRepository: Repository<ScheduledUpdate>,

@InjectRepository(ReefCheckSurvey)
private reefCheckSurveyRepository: Repository<ReefCheckSurvey>,

private dataSource: DataSource,
) {}

Expand Down Expand Up @@ -204,12 +209,17 @@ export class SitesService {
this.latestDataRepository,
);

const reefCheckDataSet = await getReefCheckDataSubQuery(
this.reefCheckSurveyRepository,
);

return res.map((site) => ({
...site,
applied: site.applied,
collectionData: mappedSiteData[site.id],
hasHobo: hasHoboDataSet.has(site.id),
waterQualitySources: waterQualityDataSet.get(site.id),
reefCheckData: reefCheckDataSet[site.id],
}));
}

Expand Down
53 changes: 52 additions & 1 deletion packages/api/src/utils/site.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
NotFoundException,
} from '@nestjs/common';
import { ObjectLiteral, Repository } from 'typeorm';
import { groupBy, mapValues, some } from 'lodash';
import { Dictionary, groupBy, keyBy, mapValues, merge, some } from 'lodash';
import geoTz from 'geo-tz';
import { ReefCheckSurvey } from 'reef-check-surveys/reef-check-surveys.entity';
import { Region } from '../regions/regions.entity';
import { ExclusionDates } from '../sites/exclusion-dates.entity';
import { ValueWithTimestamp, SpotterData } from './sofar.types';
Expand Down Expand Up @@ -325,6 +326,56 @@ export const getWaterQualityDataSubQuery = async (
return waterQualityDataSet;
};

/**
* Get all reef check related data like organisms and substrates spotted each site
* This information is intented to be used to filter sites
*/
export const getReefCheckDataSubQuery = async (
reefCheckSurveyRepository: Repository<ReefCheckSurvey>,
): Promise<
Dictionary<{ siteId: number; organism: string[]; substrate: string[] }>
> => {
const organisms: { siteId: number; organism: string[] }[] =
await reefCheckSurveyRepository
.createQueryBuilder('survey')
.select('survey.site_id', 'siteId')
.addSelect('json_agg(distinct rco.organism)', 'organism')
.leftJoin('reef_check_organism', 'rco', 'rco.survey_id = survey.id')
.where('rco.s1 > 0')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it simplify a bit to so something like this? .where('(rco.s1 + rco.s2 + rco.s3 + rco.s4) > 0')
Or maybe not? Not sure if it would make it faster

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume that with multiple simple predicates (s1 > 0 or s2 > 0) postgres would be able to do more optimizations and therefore it would be faster, but after analyzing the query plan and the execution time it seems they are equivalent. It likely won't make it faster or slower, but it would make it simpler, so let's go with your suggestion.

.orWhere('rco.s2 > 0')
.orWhere('rco.s3 > 0')
.orWhere('rco.s4 > 0')
.addGroupBy('survey.site_id')
.getRawMany();

const substrates: { siteId: number; substrate: string[] }[] =
await reefCheckSurveyRepository
.createQueryBuilder('survey')
.select('survey.site_id', 'siteId')
.addSelect('json_agg(distinct substrate_code)', 'substrate')
.leftJoin('reef_check_substrate', 'rcs', 'survey_id = survey.id')
.where('rcs.s1 > 0')
.orWhere('rcs.s2 > 0')
.orWhere('rcs.s3 > 0')
.orWhere('rcs.s4 > 0')
.addGroupBy('survey.site_id')
.getRawMany();

const impact: { siteId: number; impact: string[] }[] =
await reefCheckSurveyRepository
.createQueryBuilder('survey')
.select('survey.site_id', 'siteId')
.addSelect('json_agg(distinct overall_anthro_impact)', 'impact')
.addGroupBy('survey.site_id')
.getRawMany();

return merge(
keyBy(organisms, 'siteId'),
keyBy(substrates, 'siteId'),
keyBy(impact, 'siteId'),
);
};

export const getLatestData = async (
site: Site,
latestDataRepository: Repository<LatestData>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ exports[`Delete Button should render with given state from Redux store 1`] = `
class="MuiCollapse-wrapperInner MuiCollapse-vertical css-1i4ywhz-MuiCollapse-wrapperInner"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorError MuiAlert-standardError MuiAlert-standard css-1yey3j8-MuiPaper-root-MuiAlert-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorError MuiAlert-standardError MuiAlert-standard css-95d7z1-MuiPaper-root-MuiAlert-root"
role="alert"
style="--Paper-shadow: none;"
>
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/common/Dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const styles = (theme: Theme) =>
dialogTitle: {
backgroundColor: theme.palette.primary.main,
overflowWrap: 'break-word',
color: 'white',
},
loading: {
color: 'white',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ exports[`Error Page should render with given state from Redux store 1`] = `
class="MuiCollapse-wrapperInner MuiCollapse-vertical css-1i4ywhz-MuiCollapse-wrapperInner"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorSuccess MuiAlert-standardSuccess MuiAlert-standard css-1fa6j4p-MuiPaper-root-MuiAlert-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorSuccess MuiAlert-standardSuccess MuiAlert-standard css-1v1cteu-MuiPaper-root-MuiAlert-root"
role="alert"
style="--Paper-shadow: none;"
>
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/common/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const styles = (theme: Theme) =>
appBar: {
'&.MuiPaper-root': {
backgroundColor: theme.palette.primary.main,
color: 'white',
},
},
navBarLink: {
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/common/NavBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ const styles = (theme: Theme) =>
height: 64,
'&.MuiPaper-root': {
backgroundColor: theme.palette.primary.main,
color: 'white',
},
},
navBarLink: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exports[`Search should render with given state from Redux store 1`] = `
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1umw9bq-MuiSvgIcon-root"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-g7pk96-MuiSvgIcon-root"
data-testid="SearchIcon"
focusable="false"
viewBox="0 0 24 24"
Expand Down Expand Up @@ -55,7 +55,7 @@ exports[`Search should render with given state from Redux store 1`] = `
>
<mock-iconbutton
aria-label="Open"
classname="MuiAutocomplete-popupIndicator css-jhk8ta-MuiAutocomplete-popupIndicator"
classname="MuiAutocomplete-popupIndicator css-z4hckw-MuiAutocomplete-popupIndicator"
disabled="false"
tabindex="-1"
title="Open"
Expand Down
13 changes: 12 additions & 1 deletion packages/website/src/common/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const Search = ({ geocodingEnabled = false, classes }: SearchProps) => {
<div className={classes.searchBar}>
<div className={classes.searchBarIcon}>
<IconButton size="small" onClick={onSearchSubmit}>
<SearchIcon />
<SearchIcon sx={{ color: 'black' }} />
</IconButton>
</div>
<div className={classes.searchBarText}>
Expand All @@ -126,6 +126,7 @@ const Search = ({ geocodingEnabled = false, classes }: SearchProps) => {
: undefined
}
getOptionLabel={siteAugmentedName}
getOptionKey={(option) => option.id.toString()}
value={searchedSite}
onChange={onDropdownItemSelect}
onInputChange={(_event, _value, reason) =>
Expand All @@ -144,6 +145,11 @@ const Search = ({ geocodingEnabled = false, classes }: SearchProps) => {
}}
/>
)}
slotProps={{
popupIndicator: {
sx: { color: 'black' },
},
}}
/>
</div>
</div>
Expand All @@ -157,6 +163,7 @@ const styles = () =>
alignItems: 'stretch',
borderRadius: 4,
overflow: 'hidden',
color: 'black',
},
searchBarIcon: {
display: 'flex',
Expand All @@ -181,6 +188,10 @@ const styles = () =>
},
height: '100%',
width: '100%',

'& input::placeholder': {
opacity: 1,
},
},
listbox: {
overflowX: 'hidden',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`Survey Card should render with given state from Redux store 1`] = `
<div>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-5 css-1bvccj3-MuiPaper-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-5 css-14gemme-MuiPaper-root"
style="--Paper-shadow: none;"
>
<div
Expand Down Expand Up @@ -142,7 +142,7 @@ exports[`Survey Card should render with given state from Redux store 1`] = `
class="MuiCollapse-wrapperInner MuiCollapse-vertical css-1i4ywhz-MuiCollapse-wrapperInner"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorError MuiAlert-standardError MuiAlert-standard css-1yey3j8-MuiPaper-root-MuiAlert-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorError MuiAlert-standardError MuiAlert-standard css-95d7z1-MuiPaper-root-MuiAlert-root"
role="alert"
style="--Paper-shadow: none;"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ exports[`Surveys should render with given state from Redux store 1`] = `
component="div"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-47 css-1bvccj3-MuiPaper-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-47 css-14gemme-MuiPaper-root"
style="--Paper-shadow: none;"
>
<div
Expand Down Expand Up @@ -505,7 +505,7 @@ exports[`Surveys should render with given state from Redux store 1`] = `
class="MuiGrid-root MuiGrid-container MuiGrid-item MuiGrid-grid-xs-12 makeStyles-surveyCardWrapper-65 css-wcmnoq-MuiGrid-root"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-47 css-1bvccj3-MuiPaper-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 makeStyles-surveyCard-47 css-14gemme-MuiPaper-root"
style="--Paper-shadow: none;"
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exports[`renders as expected 1`] = `
open="true"
>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorSuccess MuiAlert-filledSuccess MuiAlert-filled makeStyles-alert-2 css-19sgit9-MuiPaper-root-MuiAlert-root"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorSuccess MuiAlert-filledSuccess MuiAlert-filled makeStyles-alert-2 css-d55hu6-MuiPaper-root-MuiAlert-root"
role="alert"
style="--Paper-shadow: none;"
>
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/common/styles/dialogStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const styles = {
},
dialogHeader: {
backgroundColor: theme.palette.primary.main,
color: 'white',
},
dialogHeaderSecondPart: {
color: '#8AC6DE',
Expand Down
72 changes: 71 additions & 1 deletion packages/website/src/helpers/siteUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LatLng } from 'leaflet';
import { maxBy, meanBy, snakeCase } from 'lodash';
import { isEmpty, keyBy, maxBy, meanBy, snakeCase } from 'lodash';

import {
DataRangeWithMetric,
Expand All @@ -11,6 +11,8 @@ import {
UpdateSiteNameFromListArgs,
} from 'store/Sites/types';
import type {
SiteFilters,
SiteOption,
TimeSeriesDataRequestParams,
siteOptions,
} from 'store/Sites/types';
Expand Down Expand Up @@ -164,3 +166,71 @@ export const sitesFilterFn = (
return true;
}
};

export const filterOutFalsy = <T extends Record<string, boolean>>(obj: T): Record<string, true> => {
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value)) as Record<string, true>;
};


export const filterSiteByHeatStress = (site: Site, { heatStress }: SiteFilters) => {
if (isEmpty(heatStress)) {
return true;
}
const { tempWeeklyAlert } = site.collectionData || {};
return tempWeeklyAlert !== undefined && heatStress[tempWeeklyAlert];
}

export const filterSiteBySensorData = (site: Site, { siteOptions: sensorDataTypes }: SiteFilters) => {
if (isEmpty(sensorDataTypes)) {
return true;
}
const siteOptions: SiteOption[] = Object.entries(sensorDataTypes).filter(([, value]) => value).map(([key]) => key) as SiteOption[];
return siteOptions.every((option) => {
switch (option) {
case 'liveStreams':
return !!site.videoStream;
case '3DModels':
return !!site.sketchFab;
case 'activeBuoys':
return hasDeployedSpotter(site);
case 'hoboLoggers':
return site.hasHobo;
case 'waterQuality':
return site.waterQualitySources?.length;
case 'reefCheckSites':
return !!site.reefCheckSite;
default:
console.error(`Unhandled Option: ${option}`);
// This will cause a TS error if there is an unhandled option
// eslint-disable-next-line prettier/prettier
option satisfies never;
return true;
}
});
}

export const filterSiteBySpecies = (site: Site, { species }: SiteFilters) => {
if (isEmpty(species)) {
return true;
}
const speciesToMatch = Object.entries(species).filter(([, value]) => value).map(([key]) => key);
return speciesToMatch.every((s) => (site.reefCheckData?.organism || []).find((o) => o.includes(s)));
}

export const filterSiteByReefComposition = (site: Site, { reefComposition }: SiteFilters) => {
if (isEmpty(reefComposition)) {
return true;
}

const substratesToMatch = Object.entries(reefComposition).filter(([, value]) => value).map(([key]) => key);
const siteSubstratesMap = keyBy(site.reefCheckData?.substrate || []);
return substratesToMatch.every((s) => siteSubstratesMap[s]);
}

export const filterSiteByImpact = (site: Site, { impact }: SiteFilters) => {
if (isEmpty(impact)) {
return true;
}
const impactToMatch = Object.entries(impact).filter(([, value]) => value).map(([key]) => key);
return impactToMatch.every((i) => site.reefCheckData?.impact?.includes(i));
}
7 changes: 7 additions & 0 deletions packages/website/src/layout/App/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ theme.components = {
},
},
},
MuiPaper: {
styleOverrides: {
root: {
color: black,
},
},
},
};

export default theme;
1 change: 1 addition & 0 deletions packages/website/src/mocks/mockSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const mockSite: Site = {
},
reefCheckSurveys: [],
reefCheckSite: null,
reefCheckData: null,
};

export const generateMockSite = ({
Expand Down
Loading
Loading