Skip to content

Commit

Permalink
Allow user to upload reference images
Browse files Browse the repository at this point in the history
This commit updates the ReferenceLayer component to store the individual
reference image layers in a mutable react ref. This cuts down on the
number of times these layers are re-created; a high number of
re-creations has caused unpredictable behavior. I first tried using
L.DistortableImage's distortableCollection, but this would make the
reference images uneditable.
  • Loading branch information
Matt Stone committed Sep 14, 2022
1 parent d3daef9 commit e44208d
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 112 deletions.
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 e44208d

Please sign in to comment.