Skip to content

Commit

Permalink
Merge pull request #60 from azavea/ms/implement-user-image-upload
Browse files Browse the repository at this point in the history
Allow user to upload reference images
  • Loading branch information
mstone121 authored Sep 14, 2022
2 parents d3daef9 + edf9b66 commit 2c5cbd2
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 112 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add Parcel Layer [#50](https://github.com/azavea/iow-boundary-tool/pull/50)
- Add distorable reference image layer [#46](https://github.com/azavea/iow-boundary-tool/pull/46)
- Add Land & water basemap [#48](https://github.com/azavea/iow-boundary-tool/pull/48)
- Add user reference image upload [#60](https://github.com/azavea/iow-boundary-tool/pull/60)

### Changed

Expand Down
97 changes: 68 additions & 29 deletions src/app/src/components/Layers/ReferenceImageLayer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import L from './L.DistortableImage.Edit.fix';
import ReferenceImage from '../../img/raleigh_sanborn_map.jpg';

import { useMapLayer } from '../../hooks';
import { customizePrototypeIcon } from '../../utils';
import { updateReferenceImage } from '../../store/mapSlice';
import { useMap } from 'react-leaflet';

customizePrototypeIcon(L.DistortHandle.prototype, 'ref-handle');
customizePrototypeIcon(L.DragHandle.prototype, 'ref-handle');
Expand All @@ -20,21 +19,26 @@ const convertCornerFromStateFormat = corner => ({
lng: corner[1],
});

export default function ReferenceImageLayerVisbilityWrapper() {
const showLayer = useSelector(state => state.map.referenceImage.visible);

return showLayer ? <ReferenceImageLayer /> : null;
}

function ReferenceImageLayer() {
export default function ReferenceImageLayer() {
const dispatch = useDispatch();
const map = useMap();
const referenceImageLayers = useRef({});

// TODO: Find a way to initialize the image with transparent/outlined enabled
const { corners, mode } = useSelector(state => state.map.referenceImage);
const images = useSelector(state => state.map.referenceImages);

const layer = useMemo(
() => {
const layer = new L.distortableImageOverlay(ReferenceImage, {
const visibleImages = useMemo(
() =>
Object.fromEntries(
Object.entries(images).filter(
([, imageInfo]) => imageInfo.visible
)
),
[images]
);

const createLayer = useCallback(
({ url, corners, mode }) => {
const layer = new L.distortableImageOverlay(url, {
actions: [
L.DragAction,
L.ScaleAction,
Expand All @@ -55,9 +59,14 @@ function ReferenceImageLayer() {
const updateImageHandler = ({ target: layer }) => {
dispatch(
updateReferenceImage({
corners: layer._corners.map(convertCornerToStateFormat),
mode: layer.editing._mode,
transparent: layer.editing._transparent,
url,
update: {
corners: layer._corners.map(
convertCornerToStateFormat
),
mode: layer.editing._mode,
transparent: layer.editing._transparent,
},
})
);
};
Expand All @@ -78,9 +87,12 @@ function ReferenceImageLayer() {
if (layer._corners) {
dispatch(
updateReferenceImage({
corners: layer._corners.map(
convertCornerToStateFormat
),
url,
update: {
corners: layer._corners.map(
convertCornerToStateFormat
),
},
})
);
}
Expand All @@ -89,14 +101,41 @@ function ReferenceImageLayer() {

return layer;
},
// TODO: Figure out how to prevent deselect on re-render
// eslint-disable-next-line react-hooks/exhaustive-deps
[
// corners,
// mode,
dispatch,
]
[dispatch]
);

useMapLayer(layer);
/**
* Since the reference image layers are stored in a mutable ref,
* they aren't updated when the redux state changes. This useEffect
* hook manually performs those updates by listening to changes of
* visibleImages.
*/
useEffect(() => {
const imageShouldBeAdded = url =>
!(url in referenceImageLayers.current);

const imageShouldBeHidden = url => !(url in visibleImages);

for (const [url, { corners, mode }] of Object.entries(visibleImages)) {
if (imageShouldBeAdded(url)) {
referenceImageLayers.current[url] = createLayer({
url,
corners,
mode,
});

map.addLayer(referenceImageLayers.current[url]);
}
}

for (const url of Object.keys(referenceImageLayers.current)) {
if (imageShouldBeHidden(url)) {
if (map.hasLayer(referenceImageLayers.current[url])) {
map.removeLayer(referenceImageLayers.current[url]);
}

delete referenceImageLayers.current[url];
}
}
}, [visibleImages, map, createLayer]);
}
57 changes: 19 additions & 38 deletions src/app/src/components/ModalSections/FileUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import {
Icon,
List,
ListItem,
Progress,
Text,
} from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { CloudUploadIcon } from '@heroicons/react/outline';

import { convertIndexedObjectToArray } from '../../utils';
import ModalSection from './ModalSection';
import { useAddReferenceImage, useFilePicker } from '../../hooks';
import { useSelector } from 'react-redux';

export default function FileUpload({ PreviousButton }) {
const navigate = useNavigate();
const addReferenceImage = useAddReferenceImage();

const [files, setFiles] = useState([]);

const addFiles = newFiles => setFiles(files => [...files, ...newFiles]);
const addFiles = newFiles => newFiles.forEach(addReferenceImage);

return (
<ModalSection
Expand All @@ -41,18 +41,18 @@ export default function FileUpload({ PreviousButton }) {
</Text>

<Flex mt={4} w='100%' grow>
<UploadBox setFiles={addFiles} />
<FilesBox files={files}></FilesBox>
<UploadBox addFiles={addFiles} />
<FilesBox />
</Flex>
</ModalSection>
);
}

function UploadBox({ setFiles }) {
function UploadBox({ addFiles }) {
const { hovering, handleUpload, startDrag, endDrag } =
useFileUpload(setFiles);
useFileUpload(addFiles);

const openFileDialog = useFilePicker(setFiles);
const openFileDialog = useFilePicker(addFiles);

const onLeaveDragBox = event => {
const enteredElement = event.relatedTarget;
Expand Down Expand Up @@ -103,7 +103,7 @@ function UploadBox({ setFiles }) {
<Text color='gray.400'>
<Bold>Shapefiles:</Bold> .SHP, .SHX and .DBF
<br />
<Bold>Other map files:</Bold> PDF, JPEG, PNG, TIFF
<Bold>Other map files:</Bold> JPEG, PNG
</Text>
</Flex>
</Box>
Expand Down Expand Up @@ -154,24 +154,6 @@ function usePreventBackgroundUpload() {
}, []);
}

function useFilePicker(onChange) {
const openFileDialog = () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = handlePickFiles;

input.click();
};

const handlePickFiles = event => {
onChange(convertIndexedObjectToArray(event.target.files));
event.target.remove();
};

return openFileDialog;
}

function CloudIconWithBackground() {
return (
<Flex
Expand Down Expand Up @@ -202,25 +184,24 @@ function Bold({ children }) {
);
}

function FilesBox({ files }) {
if (files.length === 0) return null;
function FilesBox() {
const imageEntries = Object.entries(
useSelector(state => state.map.referenceImages)
);

if (imageEntries.length === 0) return null;

return (
<Box w='50%' pl={4}>
<Heading pb={4} size='small'>
Uploaded Files
</Heading>
<List>
{files.map(file => (
<ListItem key={file.name} mb={6}>
{imageEntries.map(([url, { name }]) => (
<ListItem key={url} mb={6}>
<Text mb={2} p={2} color='gray.700' bg='gray.50'>
{file.name}
{name}
</Text>
<Progress
colorScheme='gray'
size='xs'
isIndeterminate
/>
</ListItem>
))}
</List>
Expand Down
84 changes: 51 additions & 33 deletions src/app/src/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Icon,
Text,
Circle,
Tooltip,
} from '@chakra-ui/react';
import {
MenuIcon,
Expand All @@ -26,7 +27,8 @@ import {
toggleLayer,
toggleReferenceImageVisibility,
} from '../store/mapSlice';
import { DATA_LAYERS } from '../constants';
import { DATA_LAYERS, SIDEBAR_TEXT_TOOLTIP_THRESHOLD } from '../constants';
import { useAddReferenceImage, useFilePicker } from '../hooks';

const marginLeft = 4;

Expand Down Expand Up @@ -55,9 +57,10 @@ function TitleBar() {

function ReferenceLayers() {
const dispatch = useDispatch();
const referenceImageVisible = useSelector(
state => state.map.referenceImage.visible
);
const addReferenceImage = useAddReferenceImage();
const openFileDialog = useFilePicker(files => files.map(addReferenceImage));

const images = useSelector(state => state.map.referenceImages);

return (
<Box ml={marginLeft} mt={6} mb={6}>
Expand All @@ -73,14 +76,22 @@ function ReferenceLayers() {
variant='button'
leftIcon={<Icon as={PlusIcon} />}
mb={4}
onClick={openFileDialog}
>
Upload file
</Button>
<VisibilityButton
visible={referenceImageVisible}
onChange={() => dispatch(toggleReferenceImageVisibility())}
label='Reference image'
/>
<Flex direction='column' align='flex-start'>
{Object.entries(images).map(([url, image]) => (
<VisibilityButton
key={url}
visible={image.visible}
onChange={() =>
dispatch(toggleReferenceImageVisibility(url))
}
label={image.name}
/>
))}
</Flex>
</Box>
);
}
Expand Down Expand Up @@ -137,31 +148,38 @@ function BasemapLayers() {

function VisibilityButton({ label, visible, onChange, disabled = false }) {
return (
<Button
mb={1}
leftIcon={
<Circle
color='white'
bg={visible ? 'gray.500' : 'gray.600'}
mr={2}
>
<Icon
as={visible ? EyeIcon : EyeOffIcon}
m={2}
fontSize='lg'
strokeWidth={1}
/>
</Circle>
}
onClick={onChange}
variant='link'
color={visible ? 'gray.300' : 'gray.500'}
textDecoration='none'
fontWeight={600}
disabled={disabled}
<Tooltip
label={label}
bg='gray.500'
hasArrow
isDisabled={label.length <= SIDEBAR_TEXT_TOOLTIP_THRESHOLD}
>
{label}
</Button>
<Button
mb={1}
leftIcon={
<Circle
color='white'
bg={visible ? 'gray.500' : 'gray.600'}
mr={2}
>
<Icon
as={visible ? EyeIcon : EyeOffIcon}
m={2}
fontSize='lg'
strokeWidth={1}
/>
</Circle>
}
onClick={onChange}
variant='link'
color={visible ? 'gray.300' : 'gray.500'}
textDecoration='none'
fontWeight={600}
disabled={disabled}
>
{label}
</Button>
</Tooltip>
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export const NC_SOUTH = 33.851169;

export const PARCELS_LAYER_URL =
'https://services.nconemap.gov/secure/rest/services/NC1Map_Parcels/MapServer';

export const SIDEBAR_TEXT_TOOLTIP_THRESHOLD = 30;
Loading

0 comments on commit 2c5cbd2

Please sign in to comment.