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

Grid layer. #524

Merged
merged 48 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c1ceaed
usecases: add grid layer.
Jun 28, 2023
a12c903
layers: add grid layer.
Jun 29, 2023
420a0f1
grid layer: add yard stick.
Jun 29, 2023
528fd24
chore: format and lint.
Jun 29, 2023
a47fbb9
grid layer: exchange lines with dots.
Jun 30, 2023
4ab7e8f
grid layer: improve yard stick.
Jun 30, 2023
00af2d8
usecase: suggested changes by markus
badnames Jul 5, 2023
17360e1
usecase: update with suggestions from Markus.
badnames Jul 5, 2023
2caf7af
Update doc/usecases/done/gridlayer.md
badnames Jul 5, 2023
21a42c2
grid: change title of use case
badnames Jul 10, 2023
b97c951
Merge branch 'master' into 522-map-grid
Jul 10, 2023
8159bb7
merge: add missing imports.
Jul 10, 2023
ac11c36
grid: extract constants.
Jul 10, 2023
52560c3
grid: use translations.
Jul 10, 2023
a3acd03
grid: lint and format.
Jul 10, 2023
ccb2e0e
grid: use blue color and for grid dots.
Jul 11, 2023
0fa29dc
grid: improve comments.
Jul 11, 2023
fbf3e90
grid: render on map load.
Jul 12, 2023
442bad0
grid: add button for controlling map grid.
Jul 12, 2023
8118ef2
grid: make visibility and transparency editable.
Jul 13, 2023
d92404e
chore: format.
Jul 13, 2023
41b3534
Merge branch 'master' into 522-map-grid
Jul 15, 2023
650b8a7
merge: fix errors that were introduced by merge.
Jul 15, 2023
a0cb2b5
merge: remove duplicate identifier.
Jul 15, 2023
fc564b7
grid: refactor every component into its own file.
Jul 15, 2023
e35973b
grid: add dark mode support.
Jul 15, 2023
a67a60f
chore: format.
Jul 15, 2023
6284184
Merge branch 'master' into 522-map-grid
Jul 15, 2023
51f40c1
merge: fix scroll to zoom.
Jul 15, 2023
e59fc3d
plants layer: fix build error related to missing ids.
Jul 15, 2023
faac364
Merge branch 'master' into 522-map-grid
Jul 20, 2023
6cc3666
grid: use grid button to toggle grid.
Jul 20, 2023
9cee036
grid: add manual test protocol.
Jul 20, 2023
623b2eb
chore: lint and format.
Jul 20, 2023
bc1039b
chore: lint and format.
Jul 20, 2023
e501820
chore: add changelog entry.
Jul 20, 2023
284a068
Update doc/tests/manual/protocol.md
badnames Jul 21, 2023
359c45b
Update doc/usecases/done/gridlayer.md
badnames Jul 21, 2023
db35320
Update doc/CHANGELOG.md
badnames Jul 21, 2023
bfd02c3
Update doc/usecases/done/gridlayer.md
badnames Jul 21, 2023
08e3d8d
Update frontend/src/config/i18n/de/grid.json
badnames Jul 21, 2023
542afdf
Update frontend/src/config/i18n/de/grid.json
badnames Jul 21, 2023
a8412f2
translations: remove unused strings.
Jul 21, 2023
c8f44f2
translations: correct struct name.
Jul 21, 2023
00d128b
grid: refactor grid step and yard stick label calculations.
Jul 21, 2023
5037b80
map left toolbar: use proper icon.
Jul 21, 2023
830cf73
chore: lint & format.
Jul 21, 2023
b993aaf
test: fix failing test.
Jul 21, 2023
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
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 Layer
badnames marked this conversation as resolved.
Show resolved Hide resolved

## Summary

- **Scope:** Grid Layer
badnames marked this conversation as resolved.
Show resolved Hide resolved
- **Level:** User Goal
- **Actors:** App User
- **Brief:** The app will display a fixed scale coordinate grid.
- **Status:** Done
- **Assignee:** Moritz

## Scenarios

- **Precondition:**
- User has opened the map.
- **Main success scenario:**
- The user sees a fixed scale coordinate grid with 10 centimeter spacing.
badnames marked this conversation as resolved.
Show resolved Hide resolved
- Lines that are a whole number of meters away from the origin are drawn boldly.
badnames marked this conversation as resolved.
Show resolved Hide resolved
- **Non-functional Constraints:**
- Support for changing viewport (zoom, position, etc.)
- The grid layer is only available in the frontend.
badnames marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 31 additions & 5 deletions frontend/src/features/map_planning/components/BaseStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const BaseStage = ({
selectable = true,
draggable = true,
}: BaseStageProps) => {
const updateMapBounds = useMapStore((store) => store.updateMapBounds);

// Represents the state of the stage
const [stage, setStage] = useState({
scale: 1,
Expand Down Expand Up @@ -79,21 +81,30 @@ export const BaseStage = ({
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;

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 @@ -117,6 +128,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 @@ -180,6 +205,7 @@ export const BaseStage = ({
width={window.innerWidth}
height={window.innerHeight}
onWheel={onStageWheel}
onDragEnd={onStageDragEnd}
onDragStart={onStageDragStart}
onMouseDown={onStageMouseDown}
onMouseMove={onMouseMove}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/features/map_planning/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Layers } from './toolbar/Layers';
import { Toolbar } from './toolbar/Toolbar';
import { LayerDto, LayerType } from '@/bindings/definitions';
import IconButton from '@/components/Button/IconButton';
import { GridLayer } from '@/features/map_planning/layers/_frontend_only/grid/GridLayer';
import { ReactComponent as ArrowIcon } from '@/icons/arrow.svg';
import { ReactComponent as MoveIcon } from '@/icons/move.svg';
import { ReactComponent as PlantIcon } from '@/icons/plant.svg';
Expand Down Expand Up @@ -155,6 +156,7 @@ export const Map = ({ layers }: MapProps) => {
opacity={untrackedState.layers.plants.opacity}
listening={selectedLayer.type_ === LayerType.Plants}
></PlantsLayer>
<GridLayer opacity={1} visible={true}></GridLayer>
</BaseStage>
<section className="min-h-full bg-neutral-100 dark:bg-neutral-200-dark">
<Toolbar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import useMapStore from '@/features/map_planning/store/MapStore';
import Konva from 'konva/cmj';
import { Layer, Group, Line, Text } 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>
);
};

interface GridProps {
x: number;
y: number;
width: number;
height: number;
}

const Grid = (rect: GridProps) => {
let step = 10;
let dynamicStrokeWidth = rect.width / 1000;
if (rect.width > 10000) {
step = 1000;
} else if (rect.width > 1000) {
step = 100;
}

const startX = -rect.x - rect.width - ((-rect.x - rect.width) % step);
const startY = -rect.y - rect.height - ((-rect.y - rect.height) % step);

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

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

return (
<Group>
{lines}
</Group>
);
};

const YardStick = (rect: GridProps) => {
const dynamicStrokeWidth = rect.width / 1000;

const startX = -rect.x + rect.width / 20;
const endX = -rect.x + rect.width / 20 + 100;

const y = -rect.y + rect.width / 30;

return (
<Group>
<Line strokeWidth={dynamicStrokeWidth} stroke={'#D0D0D0'} points={[startX, y, endX, y]} />
<Text x={startX} y={y * 0.995} fill={'#D0D0D0'} text={'1m'} fontSize={dynamicStrokeWidth * 10} />
</Group>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum FrontendOnlyLayerType {
Grid = 'grid',
}
35 changes: 31 additions & 4 deletions frontend/src/features/map_planning/store/MapStoreTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { LayerDto, LayerType, PlantingDto, PlantsSummaryDto } from '@/bindings/definitions';
import { FrontendOnlyLayerType } from '@/features/map_planning/layers/_frontend_only';
import Konva from 'konva';
import { Node } from 'konva/lib/Node';

/**
* This type combines layers that are only available in the frontend
* with layers that are also reflected in the backend.
*/
export type CombinedLayerType = LayerType | FrontendOnlyLayerType;

/**
* An action is a change to the map state, initiated by the user.
* It knows how to apply itself to the map state, how to reverse itself, and how to execute itself.
Expand Down Expand Up @@ -102,14 +109,22 @@ export type History = Array<Action<unknown, unknown>>;
export interface UntrackedMapSlice {
untrackedState: UntrackedMapState;
stageRef: React.RefObject<Konva.Stage>;
updateMapBounds: (bounds: BoundsRect) => void;
updateSelectedLayer: (selectedLayer: LayerDto) => void;
updateLayerVisible: (layerName: LayerType, visible: UntrackedLayerState['visible']) => void;
updateLayerOpacity: (layerName: LayerType, opacity: UntrackedLayerState['opacity']) => void;
updateLayerVisible: (
layerName: CombinedLayerType,
visible: UntrackedLayerState['visible'],
) => void;
updateLayerOpacity: (
layerName: CombinedLayerType,
opacity: UntrackedLayerState['opacity'],
) => void;
selectPlantForPlanting: (plant: PlantsSummaryDto | null) => void;
selectPlanting: (planting: PlantingDto | null) => void;
}

const LAYER_TYPES = Object.values(LayerType);
const COMBINED_LAYER_TYPES = [...Object.values(LayerType), ...Object.values(FrontendOnlyLayerType)];

export const TRACKED_DEFAULT_STATE: TrackedMapState = {
layers: LAYER_TYPES.reduce(
Expand All @@ -133,14 +148,15 @@ export const TRACKED_DEFAULT_STATE: TrackedMapState = {

export const UNTRACKED_DEFAULT_STATE: UntrackedMapState = {
mapId: -1,
editorBounds: { x: 0, y: 0, width: 0, height: 0 },
selectedLayer: {
id: -1,
is_alternative: false,
name: 'none',
type_: LayerType.Base,
map_id: -1,
},
layers: LAYER_TYPES.reduce(
layers: COMBINED_LAYER_TYPES.reduce(
(acc, layerName) => ({
...acc,
[layerName]: {
Expand Down Expand Up @@ -214,7 +230,7 @@ export type TrackedBaseLayerState = {
* The state of the layers of the map.
*/
export type UntrackedLayers = {
[key in Exclude<LayerType, LayerType.Plants>]: UntrackedLayerState;
[key in Exclude<CombinedLayerType, LayerType.Plants>]: UntrackedLayerState;
} & {
[LayerType.Plants]: UntrackedPlantLayerState;
};
Expand All @@ -236,6 +252,17 @@ export type TrackedMapState = {
*/
export type UntrackedMapState = {
mapId: number;
editorBounds: BoundsRect;
selectedLayer: LayerDto;
layers: UntrackedLayers;
};

/**
* Represents a simple rectangle with width, height and position.
*/
export type BoundsRect = {
x: number;
y: number;
width: number;
height: number;
};
16 changes: 15 additions & 1 deletion frontend/src/features/map_planning/store/UntrackedMapStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { TrackedMapSlice, UNTRACKED_DEFAULT_STATE, UntrackedMapSlice } from './MapStoreTypes';
import {
BoundsRect,
TrackedMapSlice,
UNTRACKED_DEFAULT_STATE,
UntrackedMapSlice,
} from './MapStoreTypes';
import Konva from 'konva';
import { createRef } from 'react';
import { StateCreator } from 'zustand';
Expand All @@ -11,6 +16,15 @@ export const createUntrackedMapSlice: StateCreator<
> = (set, get) => ({
untrackedState: UNTRACKED_DEFAULT_STATE,
stageRef: createRef<Konva.Stage>(),
updateMapBounds(bounds: BoundsRect) {
set((state) => ({
...state,
untrackedState: {
...state.untrackedState,
editorBounds: bounds,
},
}));
},
updateSelectedLayer(selectedLayer) {
// Clear the transformer's nodes.
get().transformer.current?.nodes([]);
Expand Down