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

Implement pan and zoom #114

Merged
merged 2 commits into from
Mar 6, 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
1 change: 1 addition & 0 deletions src/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ export const PLACES_PREVIEW = {
VERSION: 'places-1.0',
DESCRIPTION: 'Converted by AzMapsCreatorOnboardingTool',
STORAGE_RETENTION: 'day',
SEARCH_PARAMETER: 'placespreview',
};
5 changes: 5 additions & 0 deletions src/common/store/layers.store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { imdfCategories } from 'common/imdf-categories';
import Papa from 'papaparse';
import nextId from 'react-id-generator';
import { getFeatureFlags } from 'utils';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { TRUNCATE_FRACTION_DIGITS } from '../constants';
Expand Down Expand Up @@ -232,6 +233,10 @@ export const useLayersStore = createWithEqualityFn(
};
}),
getLayerNameError: layerName => {
const { isPlacesPreview } = getFeatureFlags();
// For places preview, name is automatically generated
if (isPlacesPreview) return null;

if (!layerName) {
return 'error.layer.name.cannot.be.empty';
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/dropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ const DropdownComponent = props => {
</>
)}
{filteredOptions?.map(option => (
<Option key={option.key} value={option.key} style={dropdownOption}>
<Option {...option} key={option.key} value={option.key} style={dropdownOption}>
{option.text}
</Option>
))}
{filteredGroups?.map((group, i) => (
<OptionGroup key={i}>
{group.map(option => (
<Option key={option.key} value={option.key} style={dropdownOption}>
<Option {...option} key={option.key} value={option.key} style={dropdownOption}>
{option.text}
</Option>
))}
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useAlert from './useAlert';
import useCustomNavigate from './useCustomNavigate';
import useEventListener from './useEventListener';
import useFeatureFlags from './useFeatureFlags';

export { useAlert, useCustomNavigate, useFeatureFlags };
export { useAlert, useCustomNavigate, useEventListener, useFeatureFlags };
3 changes: 2 additions & 1 deletion src/hooks/useCustomNavigate/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PLACES_PREVIEW } from 'common/constants';
import useFeatureFlags from 'hooks/useFeatureFlags';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
Expand All @@ -12,7 +13,7 @@ const useCustomNavigate = () => {
const query = new URLSearchParams(search);

if (isPlacesPreview) {
query.set('placespreview', 'true');
query.set(PLACES_PREVIEW.SEARCH_PARAMETER, 'true');
}

navigate(`${path}?${query.toString()}`, options);
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useCustomNavigate/index.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { act, renderHook } from '@testing-library/react';
import { PLACES_PREVIEW } from 'common/constants';
import * as featureFlagsHook from 'hooks/useFeatureFlags';
import * as reactRouterDom from 'react-router-dom';
import useCustomNavigate from './index';
Expand Down Expand Up @@ -29,7 +30,7 @@ describe('useCustomNavigate', () => {
result.current('/path');
});

expect(mockNavigate).toHaveBeenCalledWith('/path?placespreview=true', undefined);
expect(mockNavigate).toHaveBeenCalledWith(`/path?${PLACES_PREVIEW.SEARCH_PARAMETER}=true`, undefined);
});

it('does not append placespreview to the query params if feature flag is disabled', () => {
Expand Down
39 changes: 39 additions & 0 deletions src/hooks/useEventListener/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef } from 'react';

const useEventListener = (handler, eventName, element) => {
const savedHandler = useRef();

useEffect(() => {
savedHandler.current = handler;
}, [handler]);

useEffect(() => {
if (!element) element = window;
const isSupported = element && element.addEventListener;
if (!isSupported) return;

const events = Array.isArray(eventName) ? eventName : [eventName];

const eventListener = event => savedHandler.current(event);
events.forEach(event => {
element.addEventListener(event, eventListener);
});
return () => {
events.forEach(event => {
element.removeEventListener(event, eventListener);
});
};
}, [eventName, element]);
};

const EVENTS = {
MOUSE_DOWN: 'mousedown',
MOUSE_MOVE: 'mousemove',
MOUSE_UP: 'mouseup',
WHEEL: 'wheel',
};

export { EVENTS };

export default useEventListener;
5 changes: 3 additions & 2 deletions src/hooks/useFeatureFlags/usePlacesPreviewParam.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { PLACES_PREVIEW } from 'common/constants';
import { useSearchParams } from 'react-router-dom';

const usePlacesPreviewParam = () => {
const [searchParams] = useSearchParams();
const placespreview = searchParams.get('placespreview') === 'true';
const isPlacesPreview = searchParams.get(PLACES_PREVIEW.SEARCH_PARAMETER) === 'true';

return placespreview;
return isPlacesPreview;
};

export default usePlacesPreviewParam;
2 changes: 1 addition & 1 deletion src/pages/layers/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Layer from './layer';
import { layersContainer, layersWithPreview } from './layers.style';
import Preview from './preview';
import Preview from './preview-map';

const layersSelector = s => [s.layers, s.setVisited];

Expand Down
4 changes: 1 addition & 3 deletions src/pages/layers/layers.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { render } from '@testing-library/react';

import { useLayersStore } from 'common/store';

import Layers from './layers';

jest.mock('./preview', () => () => 'Layer Preview');
jest.mock('./preview-map', () => () => 'Layer Preview');

const defaultLayers = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
import { useGeometryStore, useLayersStore } from 'common/store';
import Dropdown, { selectAllId } from 'components/dropdown';
import { useFeatureFlags } from 'hooks';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
dropdownContainer,
previewCanvas,
previewContainerStyles,
previewDropdownStyles,
previewSelectContainer,
previewSelectTitle,
previewTitle,
} from './preview.style';
} from './index.style';
import MapControls from './map-controls';
import useTransformations from './useTransformations';

const geometrySelector = s => s.dwgLayers;
const layersSelector = s => [s.dwgLayers, s.layers, s.getLayerNameError, s.previewSingleFeatureClass];
const canvasSide = 500;
const canvasPadding = 15;

const Preview = () => {
const canvasRef = useRef(null);
const drawRef = useRef(() => {});

const { t } = useTranslation();
const exteriorLayers = useGeometryStore(geometrySelector);
const [unselectedFeatureClasses, setUnselectedFeatureClasses] = useState([]);
const [dwgLayers, allUserCreatedFeatureClasses, getLayerNameError, previewSingleFeatureClass] =
useLayersStore(layersSelector);

const [selectedDrawings, setSelectedDrawings] = useState(Object.keys(dwgLayers));

const { isPlacesPreview } = useFeatureFlags();

const { isPanning, transformations, controls } = useTransformations({ canvasRef, drawRef });

const allValidFeatureClasses = useMemo(
() =>
allUserCreatedFeatureClasses.filter(
featureClass => !featureClass.isDraft && getLayerNameError(featureClass.name) === null
),
[allUserCreatedFeatureClasses, getLayerNameError]
);

const drawings = useMemo(() => Object.keys(dwgLayers), [dwgLayers]);

const midPoints = useMemo(() => getMidPointsFromLayers(dwgLayers), [dwgLayers]);

const allLayers = useMemo(
() =>
drawings.reduce((acc, dwgLayer) => {
Expand All @@ -51,6 +64,7 @@ const Preview = () => {
}
return [selectAllId, ...selectedDrawings];
}, [dwgLayers, selectedDrawings]);

const featureClasses = useMemo(
() => [
...(exteriorLayers.length > 0
Expand All @@ -66,18 +80,22 @@ const Preview = () => {
],
[exteriorLayers, allValidFeatureClasses]
);

const selectedFeatureClassesNames = featureClasses
.filter(fClass => !unselectedFeatureClasses.includes(fClass.id))
.map(fClass => fClass.name);

const selectedFeatureClassesIds = featureClasses
.filter(fClass => !unselectedFeatureClasses.includes(fClass.id))
.map(fClass => fClass.id);

const selectedFeatureClasses = useMemo(() => {
if (selectedFeatureClassesIds.length !== featureClasses.length) {
return selectedFeatureClassesIds;
}
return [selectAllId, ...selectedFeatureClassesIds];
}, [featureClasses, selectedFeatureClassesIds]);

const dwgLayersToShow = useMemo(() => {
if (previewSingleFeatureClass) {
return allUserCreatedFeatureClasses.find(fClass => fClass.id === previewSingleFeatureClass)?.value ?? [];
Expand All @@ -86,10 +104,12 @@ const Preview = () => {
.filter(fClass => !unselectedFeatureClasses.includes(fClass.id))
.reduce((acc, fClass) => acc.concat(fClass.value), []);
}, [allUserCreatedFeatureClasses, featureClasses, previewSingleFeatureClass, unselectedFeatureClasses]);

const layers = useMemo(
() => allLayers.filter(layer => dwgLayersToShow.includes(layer.name)),
[allLayers, dwgLayersToShow]
);

const levelDropdownOptions = useMemo(() => {
const options = drawings.map(drawing => ({
key: drawing,
Expand All @@ -108,6 +128,7 @@ const Preview = () => {
options,
];
}, [drawings, t]);

const featureClassDropdownOptions = useMemo(() => {
if (featureClasses.length === 0) {
return [
Expand Down Expand Up @@ -153,6 +174,7 @@ const Preview = () => {
);
}
};

const onLevelsChange = (e, item) => {
if (item.optionValue === selectAllId) {
if (item.selectedOptions.includes(selectAllId)) {
Expand All @@ -165,15 +187,19 @@ const Preview = () => {
}
};

useEffect(() => {
const canvas = document.getElementById('canvas');
const draw = () => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasSide, canvasSide);
ctx.fillStyle = '#000';
ctx.strokeStyle = '#333';
ctx.save();

if (layers.length === 0) {
return;
}
ctx.translate(transformations.x, transformations.y);
ctx.scale(transformations.zoom, transformations.zoom);

if (layers.length === 0) return;

const pointsAndMultiPoints = [];
const geometries = [];
Expand Down Expand Up @@ -210,6 +236,13 @@ const Preview = () => {
return;
}

const applyPointPadding = (x, y) => {
return [
canvasPadding + (x * (canvasSide - 2 * canvasPadding)) / canvasSide,
canvasPadding + (y * (canvasSide - 2 * canvasPadding)) / canvasSide,
];
};

coordinatesFromLinesAndPolygons.forEach(points => {
if (points.length === 0) {
return;
Expand All @@ -221,7 +254,7 @@ const Preview = () => {
const distanceY = midPoints.midY - point[1];
const newX = canvasSide / 2 + distanceX * midPoints.multiplier + midPoints.offsetX;
const newY = canvasSide / 2 + distanceY * midPoints.multiplier - midPoints.offsetY;
return [newX, newY];
return applyPointPadding(newX, newY);
});

ctx.beginPath();
Expand All @@ -240,10 +273,20 @@ const Preview = () => {
const distanceY = midPoints.midY - point[1];
const newX = canvasSide / 2 + distanceX * midPoints.multiplier + midPoints.offsetX;
const newY = canvasSide / 2 + distanceY * midPoints.multiplier - midPoints.offsetY;
ctx.fillRect(newX, newY, 1, 1);
ctx.fillRect(...applyPointPadding(newX, newY), 1, 1);
});

ctx.restore();
};

useEffect(() => {
controls.reset();
draw();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layers, midPoints]);

drawRef.current = draw;

return (
<div className={previewContainerStyles}>
<div className={previewTitle}>Preview</div>
Expand Down Expand Up @@ -277,12 +320,19 @@ const Preview = () => {
</div>
)}
</div>
<canvas
style={{ maxWidth: '100%', display: selectedFeatureClassesIds.length ? 'block' : 'none' }}
id="canvas"
width={canvasSide}
height={canvasSide}
/>
<div style={{ position: 'relative' }}>
<MapControls controls={controls} />
<canvas
ref={canvasRef}
className={previewCanvas}
width={canvasSide}
height={canvasSide}
style={{
display: selectedFeatureClassesIds.length ? 'block' : 'none',
cursor: isPanning ? 'grabbing' : 'grab',
}}
/>
</div>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';

import { dropdownStyles } from './layers.style';
import { fontSize, fontWeight } from 'common/styles';
import { dropdownStyles } from '../layers.style';

export const previewContainerStyles = css`
display: flex;
Expand Down Expand Up @@ -36,3 +36,9 @@ export const previewTitle = css`
export const previewSelectTitle = css`
margin-bottom: 0.25rem;
`;

export const previewCanvas = css`
cursor: grab;
max-width: 100%;
background-color: #f7f7f7;
`;
Loading
Loading