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

CDC #113 - Place Layers #131

Merged
merged 5 commits into from
Feb 1, 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
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