Skip to content

Commit

Permalink
feat(map): Clip and export layers [EP-3594]
Browse files Browse the repository at this point in the history
  • Loading branch information
ancashoria authored Dec 17, 2020
1 parent 3b6d5be commit 7a118c8
Show file tree
Hide file tree
Showing 16 changed files with 450 additions and 100 deletions.
1 change: 0 additions & 1 deletion packages/earth-admin/src/pages-client/widgets/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ export function NewWidget(props: IProps) {
selectedGroup={selectedGroup}
isClearable={true}
isSearchable={true}
closeMenuOnSelect={false}
placeholder={t('Select Widget Layers')}
/>
</Card>
Expand Down
6 changes: 6 additions & 0 deletions packages/earth-map/src/auth/auth0.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { routeToPage } from 'utils';
import {
getPrivateGroups,
getPublicGroups,
hasAccess,
isAuthz,
mapAuthzScopes,
mapRoleGroups,
Expand Down Expand Up @@ -191,6 +192,10 @@ export const Auth0Provider = ({
onSuccessHook({ accessToken });
};

const getPermissions = (type: string[], org: string) => {
return hasAccess(permissions[org], type) || hasAccess(permissions['*'], type);
};

return (
<Auth0Context.Provider
value={{
Expand All @@ -211,6 +216,7 @@ export const Auth0Provider = ({
getUser,
getAccessToken,
updateToken,
getPermissions,
setupUserOrg: setSelectedGroup,
}}
>
Expand Down
1 change: 1 addition & 0 deletions packages/earth-map/src/auth/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface Auth0 {
getAccessToken?(o?: GetTokenWithPopupOptions): Promise<string>;
updateToken?(): Promise<void>;
setupUserOrg?(org: string): void;
getPermissions?(type: string[], org: string): boolean;
}

export interface User {
Expand Down
244 changes: 244 additions & 0 deletions packages/earth-map/src/components/clip-layer/ClipLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* Copyright 2018-2020 National Geographic Society
*
* Use of this software does not constitute endorsement by National Geographic
* Society (NGS). The NGS name and NGS logo may not be used for any purpose without
* written permission from NGS.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

import FileSaver from 'file-saver';
import { IPlace } from 'modules/places/model';
import React, { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import ExportService from 'services/ExportService';
import LayersService from 'services/LayersService';

import {
AsyncSelect,
Card,
DropdownItem,
ReactSelect,
serializeFilters,
Spinner,
TitleHero,
} from '@marapp/earth-shared';

interface IProps {
place: Partial<IPlace>;
onCancel?: () => void;
groups?: string[];
}

export function ClipLayer(props: IProps) {
const { onCancel, place, groups } = props;
const { id, name, organization } = place;
const [saveError, setSaveError] = useState('');
const [childLayers, setChildLayers] = useState([]);
const { t } = useTranslation();
const { register, handleSubmit, formState, control, watch, setValue } = useForm({
mode: 'onChange',
});
const { dirty, isValid, isSubmitting } = formState;
const selectedPrimaryLayer = watch('primaryLayer');
const selectedChildLayer = watch('childLayer');

// When primaryLayer changes, clear errors and set child layers
useEffect(() => {
setSaveError('');
setChildLayers(selectedPrimaryLayer?.references || []);
}, [selectedPrimaryLayer]);

// Pre-select the first child layer
useEffect(() => {
setValue('childLayer', childLayers[0]);
}, [childLayers]);

// unable to make layers dropdown required otherwise
const isValidCustom =
isValid && selectedPrimaryLayer && (childLayers.length ? selectedChildLayer : true);

return (
<form
onSubmit={handleSubmit(onSubmit)}
className="marapp-qa-cliplayer sidebar-content-full ng-form ng-form-dark"
>
<Card elevation="high" className="ng-margin-bottom">
<TitleHero title={name} subtitle={organization} extra={t('Collection')} />
</Card>

<div className="scroll-container">
<Card elevation="raised">
<label htmlFor="layer-selector" className="ng-text-bold">
{t('Select layer for download')}
</label>
<Controller
as={AsyncSelect}
id="layer-selector"
name="primaryLayer"
className="marapp-qa-primarylayers ng-margin-medium-bottom"
placeholder={t('Select Widget Layers')}
control={control}
getOptionLabel={(option, extra) => (
<DropdownItem title={option.name} subtitle={option.organization} />
)}
getOptionValue={(option) => option.id}
loadFunction={fetchPrimaryLayers}
selectedGroup={organization}
isClearable={true}
isSearchable={true}
/>

{!!childLayers.length && (
<>
<label htmlFor="child-layer-selector" className="ng-text-bold">
{t('Select layer for download')}
</label>
<Controller
as={ReactSelect}
id="child-layer-selector"
name="childLayer"
className="marapp-qa-childlayers ng-margin-medium-bottom"
placeholder={t('Select Widget Layers')}
options={childLayers}
control={control}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
selectedGroup={organization}
isClearable={true}
isSearchable={true}
/>
</>
)}

<label className="ng-text-bold">{t('Select a file type for download')}</label>
<div className="legend-item-group--radio ng-margin-top ng-margin-medium-bottom">
<div className="ng-display-inline-block ng-margin-medium-right">
<input
type="radio"
id="radio-geotiff"
value="geotiff"
name="exportType"
ref={register({
required: true,
})}
className="marapp-qa-downloadgeotiff"
/>
<label htmlFor="radio-geotiff">
<span className="legend-item-group--symbol" />
<span className="legend-item-group--name">GeoTIFF</span>
</label>
</div>
<div className="ng-display-inline-block ng-margin-medium-left">
<input
type="radio"
id="radio-jpg"
value="thumbnail"
name="exportType"
ref={register({
required: true,
})}
className="marapp-qa-downloadjpg"
/>
<label htmlFor="radio-jpg">
<span className="legend-item-group--symbol" />
<span className="legend-item-group--name">JPG</span>
</label>
</div>
</div>

{saveError && (
<p className="marapp-qa-formerror ng-form-error-block ng-margin-bottom">{saveError}</p>
)}

<button
type="submit"
className="marapp-qa-actiondownload ng-button ng-button-primary ng-margin-right"
disabled={!isValidCustom || isSubmitting || !dirty}
>
{isSubmitting ? (
<>
<Spinner size="nano" position="relative" className="ng-display-inline" />
{t('Downloading')}
</>
) : (
<>{t('Download')}</>
)}
</button>
<button
className="marapp-qa-actioncancel ng-button ng-button-secondary"
onClick={onCancel}
>
{t('Cancel')}
</button>
</Card>
</div>
</form>
);

async function onSubmit(values) {
const { exportType, primaryLayer } = values;
const { childLayer: selectedLayer = primaryLayer } = values;
const { config } = selectedLayer;
const extension = exportType === 'geotiff' ? '.zip' : '.jpg';

try {
if (config.decodeConfig || config.decodeFunction) {
throw new Error(t('Selected layers are currently not supported'));
}

const { data } = await ExportService.exportLayerForLocation(selectedLayer.id, id, {
exportType,
group: groups.join(','),
});

const rawResponse = await fetch(data.downloadURL);

if (rawResponse.status === 200) {
const blob = await rawResponse.blob();
FileSaver.saveAs(blob, `${selectedLayer.name}${extension}`);
} else {
throw new Error(t('Could not download layer. Area too large'));
}
} catch (e) {
if (!e) {
setSaveError(t('Something went wrong'));
} else if (e.status === 413) {
setSaveError(t('Could not download layer. Area too large'));
} else {
setSaveError(e.message);
}
}
}

async function fetchPrimaryLayers(query) {
try {
const primaryLayers = await LayersService.fetchLayers({
...query,
group: groups.join(','),
filter: serializeFilters({
primary: true,
provider: 'gee',
}),
include: 'references',
select: 'name,organization,config,references.name,references.config',
});

return primaryLayers;
} catch (e) {
setSaveError(t('Something went wrong'));
} finally {
}
}
}
20 changes: 20 additions & 0 deletions packages/earth-map/src/components/clip-layer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2018-2020 National Geographic Society
*
* Use of this software does not constitute endorsement by National Geographic
* Society (NGS). The NGS name and NGS logo may not be used for any purpose without
* written permission from NGS.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

export { ClipLayer } from './ClipLayer';
Loading

0 comments on commit 7a118c8

Please sign in to comment.