Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #524 from ElektraInitiative/522-map-grid
Browse files Browse the repository at this point in the history
Grid layer.
  • Loading branch information
markus2330 authored Jul 21, 2023
2 parents 317e00a + b993aaf commit 90c4626
Show file tree
Hide file tree
Showing 26 changed files with 468 additions and 31 deletions.
2 changes: 1 addition & 1 deletion doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Syntax: `- short text describing the change _(Your Name)_`
- _()_
- _()_
- _()_
- _()_
- Add grid functionality. _(Moritz)_
- _()_
- _()_
- _()_
Expand Down
17 changes: 17 additions & 0 deletions doc/tests/manual/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,23 @@
- Test Result:
- Notes:

## TC-013 - Grid
- Description: Display a point grid on the screen.
- Preconditions:
- [ ] User must be on the map screen.
- Test Steps:
1. Press the grid button in the left upper menu bar.
2. Zoom all the way in.
3. Zoom all the way out.
- Expected Result:
- [ ] The grid is displayed.
- [ ] Each press on the grid button toggles the grid of/on.
- [ ] Zooming in, grid spacing should switch from one meter to ten centimeters.
- [ ] Zooming out, grid spacing should switch ten centimeters to one meter to ten meter.
- Actual Result:
- Test Result:
- Notes:

<!--
DONT DELETE THIS.
USE THIS TO CREATE A NEW TESTCASE.
Expand Down
21 changes: 21 additions & 0 deletions doc/usecases/done/gridlayer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Use Case: Grid

## Summary

- **Scope:** Grid
- **Level:** User Goal
- **Actors:** App User
- **Brief:** The app will display a fixed scale coordinate grid.
- **Status:** Done
- **Assignee:** Moritz
- **Protocol:**

## Scenarios

- **Precondition:**
- User has opened the map.
- **Main success scenario:**
- The user sees a fixed scale coordinate grid with 1 meter spacing.
- **Non-functional Constraints:**
- Support for changing viewport (zoom, position, etc.)
- The functionality is only available in the frontend.
2 changes: 2 additions & 0 deletions frontend/src/config/i18n/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"unknown_error": "Ein unbekannter Fehler ist aufgetreten.",
"meters": "Meter",
"centimeters": "Zentimeter",
"meter_shorthand": "m",
"centimeter_shorthand": "cm",
"save": "Speichern"
}
3 changes: 3 additions & 0 deletions frontend/src/config/i18n/de/grid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tooltip": "Raster ein/aus"
}
2 changes: 2 additions & 0 deletions frontend/src/config/i18n/de/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import enums from './enums.json';
import featureDescriptions from './featureDescriptions.json';
import fileSelector from './fileSelector.json';
import geomap from './geomap.json';
import grid from './grid.json';
import imprint from './imprint.json';
import landingPage from './landingPage.json';
import layerSettings from './layerSettings.json';
Expand Down Expand Up @@ -36,6 +37,7 @@ const de = {
enums,
featureDescriptions,
geomap,
grid,
imprint,
landingPage,
pricing,
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/config/i18n/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"yes": "Yes",
"no": "No",
"cancel": "Cancel",
"save": "Save",
"unknown_error": "An unknown error occurred.",
"save": "Save",
"meters": "Meters",
"centimeters": "Centimeters"
"centimeters": "Centimeters",
"meter_shorthand": "m",
"centimeter_shorthand": "cm"
}
3 changes: 3 additions & 0 deletions frontend/src/config/i18n/en/grid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tooltip": "Toggle Grid"
}
2 changes: 2 additions & 0 deletions frontend/src/config/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import enums from './enums.json';
import featureDescriptions from './featureDescriptions.json';
import fileSelector from './fileSelector.json';
import geomap from './geomap.json';
import grid from './grid.json';
import imprint from './imprint.json';
import landingPage from './landingPage.json';
import layerSettings from './layerSettings.json';
Expand Down Expand Up @@ -36,6 +37,7 @@ const en = {
enums,
featureDescriptions,
geomap,
grid,
imprint,
landingPage,
pricing,
Expand Down
48 changes: 42 additions & 6 deletions frontend/src/features/map_planning/components/BaseStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,29 +80,50 @@ export const BaseStage = ({
const step = useMapStore((map) => map.step);
const historyLength = useMapStore((map) => map.history.length);

const updateMapBounds = useMapStore((store) => store.updateMapBounds);
const mapBounds = useMapStore((store) => store.untrackedState.editorBounds);
useEffect(() => {
if (mapBounds.width !== 0 || mapBounds.height !== 0) return;
updateMapBounds({
x: 0,
y: 0,
width: Math.floor(window.innerWidth / stage.scale),
height: Math.floor(window.innerHeight / stage.scale),
});
});

// Event listener responsible for allowing zooming with the ctrl key + mouse wheel
const onStageWheel = (e: KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();

const stage = e.target.getStage();
if (stage === null) return;
const targetStage = e.target.getStage();
if (targetStage === null) return;

if (tooltipRef.current) {
setTooltipPosition(tooltipRef.current, stage);
setTooltipPosition(tooltipRef.current, targetStage);
}

const pointerVector = stage.getPointerPosition();
const pointerVector = targetStage.getPointerPosition();
if (pointerVector === null) return;

if (e.evt.ctrlKey) {
if (zoomable) {
handleZoom(pointerVector, e.evt.deltaY, stage, setStage);
handleZoom(pointerVector, e.evt.deltaY, targetStage, setStage);
}
} else {
if (scrollable) {
handleScroll(e.evt.deltaX, e.evt.deltaY, stage);
handleScroll(e.evt.deltaX, e.evt.deltaY, targetStage);
}
}

if (stageRef.current === null) return;

updateMapBounds({
x: Math.floor(stageRef.current.getAbsolutePosition().x / stage.scale),
y: Math.floor(stageRef.current.getAbsolutePosition().y / stage.scale),
width: Math.floor(window.innerWidth / stage.scale),
height: Math.floor(window.innerHeight / stage.scale),
});
};

// Event listener responsible for allowing dragging of the stage only with the wheel mouse button
Expand All @@ -126,6 +147,20 @@ export const BaseStage = ({
}
};

const onStageDragEnd = (e: KonvaEventObject<DragEvent>) => {
if (e.evt === null || e.evt === undefined) return;
e.evt.preventDefault();

if (stageRef.current === null) return;

updateMapBounds({
x: Math.floor(stageRef.current.getAbsolutePosition().x / stage.scale),
y: Math.floor(stageRef.current.getAbsolutePosition().y / stage.scale),
width: Math.floor(window.innerWidth / stage.scale),
height: Math.floor(window.innerHeight / stage.scale),
});
};

// Event listener responsible for updating the selection rectangle
const onMouseMove = (e: KonvaEventObject<MouseEvent>) => {
e.evt.preventDefault();
Expand Down Expand Up @@ -191,6 +226,7 @@ export const BaseStage = ({
width={window.innerWidth}
height={window.innerHeight}
onWheel={onStageWheel}
onDragEnd={onStageDragEnd}
onDragStart={onStageDragStart}
onMouseDown={onStageMouseDown}
onMouseMove={onMouseMove}
Expand Down
38 changes: 30 additions & 8 deletions frontend/src/features/map_planning/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { Layers } from './toolbar/Layers';
import { Toolbar } from './toolbar/Toolbar';
import { LayerDto, LayerType } from '@/bindings/definitions';
import IconButton from '@/components/Button/IconButton';
import { FrontendOnlyLayerType } from '@/features/map_planning/layers/_frontend_only';
import { GridLayer } from '@/features/map_planning/layers/_frontend_only/grid/GridLayer';
import { CombinedLayerType } from '@/features/map_planning/store/MapStoreTypes';
import { ReactComponent as GridIcon } from '@/icons/grid-dots.svg';
import { ReactComponent as RedoIcon } from '@/icons/redo.svg';
import { ReactComponent as UndoIcon } from '@/icons/undo.svg';
import i18next from 'i18next';
Expand All @@ -26,19 +30,20 @@ export type MapProps = {
* In order to add a new layer you can add another layer file under the "layers" folder.
* Features such as zooming and panning are handled by the BaseStage component.
* You only have to make sure that every shape has the property "draggable" set to true.
* Otherwise they cannot be moved.
* Otherwise, they cannot be moved.
*/
export const Map = ({ layers }: MapProps) => {
const untrackedState = useMapStore((map) => map.untrackedState);
const undo = useMapStore((map) => map.undo);
const redo = useMapStore((map) => map.redo);
const selectedLayer = useMapStore((state) => state.untrackedState.selectedLayer);
const updateLayerVisible = useMapStore((map) => map.updateLayerVisible);
const getSelectedLayerType = useMapStore((map) => map.getSelectedLayerType);
const timelineDate = useMapStore((state) => state.untrackedState.timelineDate);
const updateTimelineDate = useMapStore((state) => state.updateTimelineDate);

const { t } = useTranslation(['undoRedo', 'timeline']);
const { t } = useTranslation(['undoRedo', 'grid', 'timeline']);

const getToolbarContent = (layerType: LayerType) => {
const getToolbarContent = (layerType: CombinedLayerType) => {
const content = {
[LayerType.Base]: {
left: <div></div>,
Expand All @@ -63,6 +68,7 @@ export const Map = ({ layers }: MapProps) => {
[LayerType.Todo]: { right: <div></div>, left: <div></div> },
[LayerType.Photo]: { right: <div></div>, left: <div></div> },
[LayerType.Watering]: { right: <div></div>, left: <div></div> },
[FrontendOnlyLayerType.Grid]: { right: <div></div>, left: <div></div> },
};

return content[layerType];
Expand All @@ -89,9 +95,21 @@ export const Map = ({ layers }: MapProps) => {
>
<RedoIcon></RedoIcon>
</IconButton>
<IconButton
className="m-2 h-8 w-8 border border-neutral-500 p-1"
onClick={() =>
updateLayerVisible(
FrontendOnlyLayerType.Grid,
!untrackedState.layers.grid.visible,
)
}
title={t('grid:tooltip')}
>
<GridIcon></GridIcon>
</IconButton>
</div>
}
contentBottom={getToolbarContent(untrackedState.selectedLayer.type_).left}
contentBottom={getToolbarContent(getSelectedLayerType()).left}
position="left"
></Toolbar>
</section>
Expand All @@ -100,14 +118,18 @@ export const Map = ({ layers }: MapProps) => {
<BaseLayer
opacity={untrackedState.layers.base.opacity}
visible={untrackedState.layers.base.visible}
listening={selectedLayer.type_ === LayerType.Base}
listening={getSelectedLayerType() === LayerType.Base}
/>
<PlantsLayer
visible={untrackedState.layers.plants.visible}
opacity={untrackedState.layers.plants.opacity}
listening={selectedLayer.type_ === LayerType.Plants}
listening={getSelectedLayerType() === LayerType.Plants}
></PlantsLayer>
<BaseMeasurementLayer />
<GridLayer
visible={untrackedState.layers.grid.visible}
opacity={untrackedState.layers.grid.opacity}
></GridLayer>
</BaseStage>
<div>
<Timeline onSelectDate={(date) => updateTimelineDate(date)} defaultDate={timelineDate} />
Expand All @@ -116,7 +138,7 @@ export const Map = ({ layers }: MapProps) => {
<section className="min-h-full bg-neutral-100 dark:bg-neutral-200-dark">
<Toolbar
contentTop={<Layers layers={layers} />}
contentBottom={getToolbarContent(untrackedState.selectedLayer.type_).right}
contentBottom={getToolbarContent(getSelectedLayerType()).right}
position="right"
minWidth={200}
fixedContentBottom={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const LayerList = ({
const [alternativesVisible, setAlternativesVisible] = useState(false);
const { t } = useTranslation(['layerSettings', 'layers']);

// If a frontend only layer is active, no other layer should be selected.
const selectedLayerId = typeof selectedLayer === 'object' ? selectedLayer.id : null;

return (
<>
<div className="flex items-center justify-center">
Expand All @@ -54,7 +57,7 @@ export const LayerList = ({
className="h-4 w-4"
type="radio"
value={layer.name}
checked={selectedLayer.id === layer.id}
checked={selectedLayerId === layer.id}
onChange={() => {
if (setSelectedLayer) setSelectedLayer(layer);
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Grid } from '@/features/map_planning/layers/_frontend_only/grid/groups/Grid';
import { YardStick } from '@/features/map_planning/layers/_frontend_only/grid/groups/YardStick';
import useMapStore from '@/features/map_planning/store/MapStore';
import Konva from 'konva/cmj';
import { Layer } from 'react-konva';

export const GridLayer = (props: Konva.LayerConfig) => {
const mapBounds = useMapStore((state) => state.untrackedState.editorBounds);

return (
<Layer listening={false} visible={props.visible} opacity={props.opacity}>
<Grid x={mapBounds.x} y={mapBounds.y} width={mapBounds.width} height={mapBounds.height} />
<YardStick
x={mapBounds.x}
y={mapBounds.y}
width={mapBounds.width}
height={mapBounds.height}
/>
</Layer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { calculateGridStep } from '@/features/map_planning/layers/_frontend_only/grid/util/Calculations';
import {
RELATIVE_DOT_SIZE,
SEA_BLUE_500,
} from '@/features/map_planning/layers/_frontend_only/grid/util/Constants';
import { BoundsRect } from '@/features/map_planning/store/MapStoreTypes';
import { Group, Line } from 'react-konva';

export const Grid = (rect: BoundsRect) => {
const gridStep = calculateGridStep(rect.width);

const gridDotSize = rect.width * RELATIVE_DOT_SIZE;

// Draw the grid larger than necessary to avoid artifacts while panning the viewport.
const startX = -rect.x - rect.width - ((-rect.x - rect.width) % gridStep);
const startY = -rect.y - rect.height - ((-rect.y - rect.height) % gridStep);

const endX = -rect.x + rect.width * 2;
const endY = -rect.y + rect.height * 2;

const lines = [];
for (let y = startY; y < endY; y += gridStep) {
lines.push(
<Line
strokeWidth={gridDotSize}
stroke={SEA_BLUE_500}
points={[startX, y, endX, y]}
dash={[gridDotSize, gridStep - gridDotSize]}
></Line>,
);
}

return <Group>{lines}</Group>;
};
Loading

0 comments on commit 90c4626

Please sign in to comment.