diff --git a/src/components/FeaturePanel/Properties/Direction.tsx b/src/components/FeaturePanel/Properties/Direction.tsx index e0b3ebc44..7050a2964 100644 --- a/src/components/FeaturePanel/Properties/Direction.tsx +++ b/src/components/FeaturePanel/Properties/Direction.tsx @@ -1,7 +1,7 @@ -import { keyframes } from '@emotion/react'; -import styled from '@emotion/styled'; -import ArrowUpward from '@mui/icons-material/ArrowUpward'; import React from 'react'; +import { useUserThemeContext } from '../../../helpers/theme'; +import { use2dContext } from './helpers'; +import ArrowUpward from '@mui/icons-material/ArrowUpward'; const cardinalMiddle = (angle1: number, angle2: number) => { // Add 360 on big difference like NW @@ -52,40 +52,145 @@ const parseCardinal = (cardinal: string): number => { ); }; -const rotateAnimation = (start: number, end: number) => { - const finalEnd = start > end ? end + 360 : end; +function findMaxRangeStep( + start: number, + end: number | undefined, + max = 30, +): number { + if (end === undefined) { + return 0; + } + const isFullCircle = start % 360 === end % 360; + const rangeLength = isFullCircle ? 360 : (end + 360 - start) % 360; - return keyframes` - 0% { - transform: rotate(${start}deg); + // Checking for divisors from max down to 1 + for (let i = Math.min(max, rangeLength); i >= 1; i--) { + if (rangeLength % i === 0) { + return i; } - 100% { - transform: rotate(${finalEnd}deg); - } - `; + } + + return 0; +} + +const degreeToRadian = (degrees: number) => degrees * (Math.PI / 180); + +type RayProps = { + centerX: number; + centerY: number; + degrees: number; + ctx: CanvasRenderingContext2D; + rayLength: number; + gap: number; + color: string; }; -const AnimatedArrow = styled(ArrowUpward)<{ - start: number; - end: number; -}>` - animation: ${({ start, end }) => rotateAnimation(start, end)} 4s linear - infinite alternate; -`; +const drawRay = ({ + centerX, + centerY, + degrees, + ctx, + rayLength, + gap, + color, +}: RayProps) => { + const rayWidth = Math.PI / 10; + + const startAngle = degreeToRadian(degrees - 90); + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = rayLength; + ctx.arc( + centerX, + centerY, + gap + rayLength / 2, + startAngle - rayWidth / 2, + startAngle + rayWidth / 2, + ); + ctx.stroke(); +}; + +const DirectionIndicator = ({ start, end }: { start: number; end: number }) => { + const canvas = React.useRef(); + const [color, setColor] = React.useState('#000'); + const { currentTheme } = useUserThemeContext(); + React.useEffect(() => { + setColor(currentTheme === 'light' ? '#000' : '#fff'); + }, [currentTheme]); + + use2dContext( + canvas, + (ctx, { width: canvasWidth, height: canvasHeight }) => { + const centerX = canvasWidth / 2; + const centerY = canvasHeight / 2; + const radius = canvasHeight / 10; + const gap = canvasHeight / 10; + const rayLength = (canvasWidth / 20) * 7; + + const ray = (angle: number) => + drawRay({ + degrees: angle, + rayLength, + centerX, + centerY, + ctx, + color, + gap: radius / 2 + gap, + }); + + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + + const rangeStep = findMaxRangeStep(start, end); + const isFullcircle = start % 360 === end % 360; + const angleInrease = (angle: number, rangeStep: number) => { + if (isFullcircle) { + return angle + rangeStep; + } + return (angle + rangeStep) % 360; + }; + + for ( + let angle = start; + angle !== end; + angle = angleInrease(angle, rangeStep) + ) { + ray(angle); + } + ray(end); + }, + [start, end, color], + ); + + return ; +}; export const DirectionValue: React.FC<{ v: string }> = ({ children, v }) => { try { const [start, end] = v.split('-', 2).map(parseCardinal); + const indicator = + end === undefined ? ( + + ) : ( + + ); + return ( - + {indicator} {children} ); diff --git a/src/components/FeaturePanel/Properties/IdSchemaFields.tsx b/src/components/FeaturePanel/Properties/IdSchemaFields.tsx index ba2b21a9f..3d7f6604e 100644 --- a/src/components/FeaturePanel/Properties/IdSchemaFields.tsx +++ b/src/components/FeaturePanel/Properties/IdSchemaFields.tsx @@ -97,7 +97,12 @@ const UiFields = ({ fields }: { fields: UiField[] }) => { const { key, label, field, tagsForField } = uiField; return ( - {removeUnits(label)} + + {removeUnits(label)} + {addUnits(label, render(uiField, feature))} diff --git a/src/components/FeaturePanel/Properties/TagsTableInner.tsx b/src/components/FeaturePanel/Properties/TagsTableInner.tsx index 7f6230194..3baf5876b 100644 --- a/src/components/FeaturePanel/Properties/TagsTableInner.tsx +++ b/src/components/FeaturePanel/Properties/TagsTableInner.tsx @@ -140,7 +140,7 @@ export const TagsTableInner = ({ /> {rest.map(([k, v]) => ( - {k} + {k} {renderValue(k, v)} diff --git a/src/components/FeaturePanel/Properties/helpers.tsx b/src/components/FeaturePanel/Properties/helpers.tsx index 6cc050a56..afd10ac5e 100644 --- a/src/components/FeaturePanel/Properties/helpers.tsx +++ b/src/components/FeaturePanel/Properties/helpers.tsx @@ -25,3 +25,23 @@ export const ShowMoreButton = ({ onClick, isShown }) => ( )} ); + +export function use2dContext( + ref: React.RefObject, + handler: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void, + deps: React.DependencyList = [], +) { + React.useEffect(() => { + const canvas = ref.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + handler(ctx, canvas); + + return () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref, ...deps]); +}