Skip to content

Commit

Permalink
Merge pull request #131 from performant-software/feature/cdc113_place…
Browse files Browse the repository at this point in the history
…_layers

CDC #113 - Place Layers
  • Loading branch information
dleadbetter authored Feb 1, 2024
2 parents ba19f56 + 27f875c commit b1b11f3
Show file tree
Hide file tree
Showing 16 changed files with 541 additions and 63 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ gem 'resource_api', git: 'https://github.com/performant-software/resource-api.gi
gem 'jwt_auth', git: 'https://github.com/performant-software/jwt-auth.git', tag: 'v0.1.2'

# Core data
gem 'core_data_connector', git: 'https://github.com/performant-software/core-data-connector.git', tag: 'v0.1.28'
gem 'core_data_connector', git: 'https://github.com/performant-software/core-data-connector.git', tag: 'v0.1.29'

# IIIF
gem 'triple_eye_effable', git: 'https://github.com/performant-software/triple-eye-effable.git', tag: 'v0.1.10'
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
GIT
remote: https://github.com/performant-software/core-data-connector.git
revision: 94e693a718f4dd07bf034c3e926aaafb20d8fbe8
tag: v0.1.28
revision: a2dcd60f4da587a410cbdf9440427f3a3927bd7e
tag: v0.1.29
specs:
core_data_connector (0.1.0)
activerecord-postgis-adapter (~> 8.0)
Expand Down
8 changes: 4 additions & 4 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"flow": "flow"
},
"dependencies": {
"@performant-software/geospatial": "^1.1.2",
"@performant-software/semantic-components": "^1.1.2",
"@performant-software/shared-components": "^1.1.2",
"@performant-software/user-defined-fields": "^1.1.2",
"@performant-software/geospatial": "^1.1.3",
"@performant-software/semantic-components": "^1.1.3",
"@performant-software/shared-components": "^1.1.3",
"@performant-software/user-defined-fields": "^1.1.3",
"boring-avatars": "^1.10.1",
"classnames": "^2.3.1",
"i18next": "^21.9.1",
Expand Down
118 changes: 106 additions & 12 deletions client/src/components/PlaceForm.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,70 @@
// @flow

import { MapDraw } from '@performant-software/geospatial';
import {
GeoJsonLayer,
LayerMenu,
MapControl,
MapDraw,
RasterLayer
} from '@performant-software/geospatial';
import { BooleanIcon, EmbeddedList, FileInputButton } from '@performant-software/semantic-components';
import type { EditContainerProps } from '@performant-software/shared-components/types';
import { UserDefinedFieldsForm } from '@performant-software/user-defined-fields';
import cx from 'classnames';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Header } from 'semantic-ui-react';
import { Form, Header, Icon } from 'semantic-ui-react';
import _ from 'underscore';
import type { Place as PlaceType } from '../types/Place';
import PlaceLayerModal from './PlaceLayerModal';
import PlaceLayerUtils from '../utils/PlaceLayers';
import PlaceNameModal from './PlaceNameModal';
import styles from './PlaceForm.module.css';

type Props = EditContainerProps & {
item: PlaceType
};

const { LayerTypes } = PlaceLayerUtils;

const PlaceForm = (props: Props) => {
const { t } = useTranslation();

/**
* Memo-izes the names of the passed place layers.
*/
const layerNames = useMemo(() => _.pluck(props.item.place_layers, 'name'), [props.item.place_layers]);

/**
* Parses the geometry for the passed layers.
*/
const layers = useMemo(() => _.map(props.item.place_layers, (layer) => ({
...layer,
geometry: layer.geometry ? JSON.parse(layer.geometry) : undefined
})), [props.item.place_layers]);

/**
* Renders the passed layer.
*
* @type {(function(*): *)|*}
*/
const renderLayer = useCallback((layer) => {
if (layer.layer_type === LayerTypes.geojson) {
return (
<GeoJsonLayer
data={layer.geometry}
url={layer.url}
/>
);
}

return (
<RasterLayer
url={layer.url}
/>
);
}, []);

/**
* Sets the uploaded file as the GeoJSON object.
*
Expand Down Expand Up @@ -71,16 +117,64 @@ const PlaceForm = (props: Props) => {
data={props.item.place_geometry?.geometry_json}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${process.env.REACT_APP_MAP_TILER_KEY}`}
onChange={(data) => props.onSetState({ place_geometry: { geometry_json: data } })}
style={{
marginBottom: '4em'
}}
>
<MapControl
position='top-left'
>
<FileInputButton
className={cx(
'mapbox-gl-draw_ctrl-draw-btn',
'layer-button',
styles.ui,
styles.button,
styles.uploadButton
)}
color='white'
icon={(
<Icon
name='cloud upload'
/>
)}
onSelection={onUpload}
/>
</MapControl>
<LayerMenu
names={layerNames}
>
{ _.map(layers, renderLayer) }
</LayerMenu>
</MapDraw>
<Header
content={t('PlaceForm.labels.layers')}
size='tiny'
/>
<FileInputButton
className={cx(styles.uploadButton, styles.ui, styles.button)}
color='dark gray'
content={t('Place.buttons.upload')}
icon='upload'
onSelection={onUpload}
<EmbeddedList
actions={[{
name: 'edit'
}, {
name: 'delete'
}]}
addButton={{
basic: false,
color: 'dark gray',
location: 'bottom'
}}
className='compact'
columns={[{
name: 'name',
label: t('PlaceForm.placeLayers.columns.name')
}]}
configurable={false}
items={props.item.place_layers}
modal={{
component: PlaceLayerModal,
props: {
required: ['name'],
validate: PlaceLayerUtils.validate.bind(this)
}
}}
onSave={props.onSaveChildAssociation.bind(this, 'place_layers')}
onDelete={props.onDeleteChildAssociation.bind(this, 'place_layers')}
/>
{ props.item.project_model_id && (
<UserDefinedFieldsForm
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/PlaceForm.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.placeForm .uploadButton.ui.button {
margin-bottom: 1em;
background-color: transparent;
margin: 0;
}
179 changes: 179 additions & 0 deletions client/src/components/PlaceLayerModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// @flow

import type { EditContainerProps } from '@performant-software/shared-components/types';
import { FileInputButton } from '@performant-software/semantic-components';
import cx from 'classnames';
import React, {
useCallback,
useEffect,
useMemo,
useState,
type AbstractComponent
} from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Menu, Modal } from 'semantic-ui-react';
import type { PlaceLayer as PlaceLayerType } from '../types/Place';
import PlaceLayerUtils from '../utils/PlaceLayers';
import styles from './PlaceLayerModal.module.css';

type Props = EditContainerProps & {
item: PlaceLayerType
};

const { LayerTypes } = PlaceLayerUtils;

const Tabs = {
url: 0,
file: 1
};

const PlaceLayerModal: AbstractComponent<any> = (props: Props) => {
const [loading, setLoading] = useState(false);
const [tab, setTab] = useState(props.item.geometry ? Tabs.file : Tabs.url);

const { t } = useTranslation();

/**
* Sets a memo-ized version of the parsed and formatted GeoJSON.
*
* @type {string}
*/
const geometry = useMemo(() => (
JSON.stringify(JSON.parse(props.item.geometry || '{}'), null, 2)
), [props.item.geometry]);

/**
* Sets the uploaded file as the GeoJSON object.
*
* @type {(function([*]): void)|*}
*/
const onUpload = useCallback(([file]) => {
setLoading(true);

file.text()
.then((value) => props.onSetState({ geometry: value, url: null }))
.finally(() => setLoading(false));
}, []);

/**
* Sets the geometry to the passed value and clears the URL.
*
* @type {(function(*, {value: *}): void)|*}
*/
const onGeometryChange = useCallback((e, { value }) => {
props.onSetState({ url: null, geometry: value });
}, []);

/**
* Sets the URL to the passed value and clears the geometry.
*
* @type {(function(*, {value: *}): void)|*}
*/
const onUrlChange = useCallback((e, { value }) => {
props.onSetState({ url: value, geometry: null });
}, []);

/**
* Set the default layer type on the state for a new record.
*/
useEffect(() => {
if (!props.item.layer_type) {
props.onSetState({ layer_type: LayerTypes.geojson });
}
}, []);

return (
<Modal
as={Form}
centered={false}
className={styles.placeLayerModal}
noValidate
open
>
<Modal.Header
content={props.item.id
? t('PlaceLayerModal.title.edit')
: t('PlaceLayerModal.title.add')}
/>
<Modal.Content
className={styles.content}
>
<Form.Input
autoFocus
error={props.isError('name')}
label={t('PlaceLayerModal.labels.name')}
required={props.isRequired('name')}
onChange={props.onTextInputChange.bind(this, 'name')}
value={props.item.name}
/>
<Form.Dropdown
label={t('PlaceLayerModal.labels.type')}
required
onChange={props.onTextInputChange.bind(this, 'layer_type')}
options={PlaceLayerUtils.getLayerTypeOptions()}
selection
value={props.item.layer_type}
/>
{ props.item.layer_type === LayerTypes.geojson && (
<>
<Menu
secondary
>
<Menu.Item
active={tab === Tabs.url}
content={t('PlaceLayerModal.tabs.url')}
onClick={() => setTab(Tabs.url)}
/>
<Menu.Item
active={tab === Tabs.file}
content={t('PlaceLayerModal.tabs.file')}
onClick={() => setTab(Tabs.file)}
/>
</Menu>
{ tab === Tabs.url && (
<Form.Input
error={props.isError('url')}
label={t('PlaceLayerModal.labels.url')}
required
onChange={onUrlChange}
value={props.item.url}
/>
)}
{ tab === Tabs.file && (
<>
<FileInputButton
className={cx(styles.ui, styles.button, styles.uploadButton)}
color='dark gray'
content={t('Common.buttons.upload')}
disabled={loading}
icon='upload'
loading={loading}
onSelection={onUpload}
/>
<Form.TextArea
error={props.isError('geometry')}
label={t('PlaceLayerModal.labels.geometry')}
required
onChange={onGeometryChange}
value={geometry}
/>
</>
)}
</>
)}
{ props.item.layer_type === LayerTypes.raster && (
<Form.Input
error={props.isError('url')}
label={t('PlaceLayerModal.labels.url')}
required
onChange={props.onTextInputChange.bind(this, 'url')}
value={props.item.url}
/>
)}
</Modal.Content>
{ props.children }
</Modal>
);
};

export default PlaceLayerModal;
3 changes: 3 additions & 0 deletions client/src/components/PlaceLayerModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.placeLayerModal > .content .ui.button.uploadButton {
margin-bottom: 1em;
}
Loading

0 comments on commit b1b11f3

Please sign in to comment.