feat: completed measure handling
130
frontend/react/public/images/cursors/measure.svg
Normal file
@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg7234"
|
||||
sodipodi:docname="measure.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata6">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7238" />
|
||||
<sodipodi:namedview
|
||||
id="namedview7236"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.57797693"
|
||||
inkscape:cx="-77.857779"
|
||||
inkscape:cy="196.37462"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg7234"
|
||||
inkscape:pageshadow="2"
|
||||
width="512px" />
|
||||
<path
|
||||
d="M 2.4865894,3.2348193 V 20.790103 c 0,0.5776 0.468708,1.041576 1.0415756,1.041576 0.2982683,0 0.5870705,-0.127832 0.785916,-0.355083 l 3.9106433,-4.474042 2.7507057,5.506148 c 0.374022,0.748041 1.283033,1.051042 2.031074,0.677023 0.748041,-0.374022 1.051042,-1.283033 0.677024,-2.031073 l -2.684426,-5.38305 h 5.591368 c 0.577601,0 1.04631,-0.468711 1.04631,-1.046312 0,-0.298268 -0.127829,-0.582334 -0.35035,-0.781179 L 4.314081,2.4157613 C 4.1105025,2.2358517 3.8548429,2.1364289 3.5849798,2.1364289 c -0.6060096,0 -1.0983904,0.4923807 -1.0983904,1.0983904 z"
|
||||
id="path1"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#262626;stroke-width:3.89387;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<g
|
||||
id="g2"
|
||||
transform="matrix(0.64819955,-0.57060757,0.57060757,0.64819955,-3.326076,14.927962)">
|
||||
<rect
|
||||
style="fill:#272727;fill-opacity:1;stroke:#272727;stroke-width:5.62061;stroke-linejoin:round;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect1"
|
||||
width="17.881115"
|
||||
height="1.7855351"
|
||||
x="7.5564566"
|
||||
y="25.487162" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.57061;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8-1"
|
||||
width="0.31468576"
|
||||
height="2.6468244"
|
||||
x="25.435246"
|
||||
y="24.995579" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.57061;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2"
|
||||
width="0.31468576"
|
||||
height="2.6468244"
|
||||
x="7.4698963"
|
||||
y="24.995579" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.57061;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1"
|
||||
width="0.31468576"
|
||||
height="2.6468244"
|
||||
x="11.961234"
|
||||
y="24.995579" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.57061;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-8"
|
||||
width="0.31468576"
|
||||
height="2.6468244"
|
||||
x="16.452572"
|
||||
y="24.995579" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.57061;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8"
|
||||
width="0.31468576"
|
||||
height="2.6468244"
|
||||
x="20.943909"
|
||||
y="24.995579" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.826764;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8-9"
|
||||
width="0.17920054"
|
||||
height="1.2879221"
|
||||
x="9.8781376"
|
||||
y="26.505974" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.826764;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8-9-5"
|
||||
width="0.17920054"
|
||||
height="1.2879221"
|
||||
x="14.389886"
|
||||
y="26.505974" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.826764;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8-9-9"
|
||||
width="0.17920054"
|
||||
height="1.2879221"
|
||||
x="18.901634"
|
||||
y="26.505974" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.826764;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect2-1-8-9-7"
|
||||
width="0.17920054"
|
||||
height="1.2879221"
|
||||
x="23.413382"
|
||||
y="26.505974" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@ -26,8 +26,8 @@
|
||||
width="52mm"
|
||||
units="px"
|
||||
inkscape:zoom="8.3856042"
|
||||
inkscape:cx="14.250613"
|
||||
inkscape:cy="36.550735"
|
||||
inkscape:cx="-19.855457"
|
||||
inkscape:cy="24.506284"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
@ -46,14 +46,67 @@
|
||||
offset="0"
|
||||
id="stop4715" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3-3">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3-4" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4">
|
||||
<ellipse
|
||||
style="fill:#f48800;fill-opacity:1;stroke:none;stroke-width:1.10409;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="ellipse4"
|
||||
cx="6.4735489"
|
||||
cy="8.0908279"
|
||||
rx="7.8196473"
|
||||
ry="8.125103"
|
||||
clip-path="url(#clipPath3)" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3-3-7">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3-4-8" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath5">
|
||||
<ellipse
|
||||
style="fill:#f48800;fill-opacity:1;stroke:none;stroke-width:1.10409;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="ellipse5"
|
||||
cx="6.4735489"
|
||||
cy="8.0908279"
|
||||
rx="7.8196473"
|
||||
ry="8.125103"
|
||||
clip-path="url(#clipPath3-3-7)"
|
||||
transform="translate(-0.00203625,0.00601104)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="stroke-width:6.985;stroke-linejoin:round;paint-order:stroke fill markers;fill-opacity:1"
|
||||
style="stroke-width:1.485;stroke-linejoin:round;paint-order:stroke fill markers;fill-opacity:1;stroke:#272727;stroke-opacity:1;stroke-dasharray:none;fill:#fbfbfb;stroke-linecap:butt"
|
||||
d="M 6.5628344,12.967909 1.2936356,11.16944 4.7012613,11.421857 1.4829482,6.9099074 5.0483343,9.6864912 3.5022818,0.8519064 6.8468032,9.623387 8.6452724,3.0290005 8.0142306,9.9389079 11.737377,6.8783553 8.5506162,10.980127 12.904805,10.885471 Z"
|
||||
id="path1" />
|
||||
<path
|
||||
style="fill:#f88000;fill-opacity:1;stroke:#f88000;stroke-width:0.285;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.533962,12.945328 1.2647628,11.146859 4.672389,11.399276 1.4540754,6.8873262 5.019462,9.66391 3.473409,0.82932521 6.81793,9.6008058 8.6164,3.0064193 7.985358,9.9163262 11.708504,6.8557741 8.521743,10.957546 l 4.354189,-0.09466 z"
|
||||
id="path1-3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 618 B |
60
frontend/react/public/images/markers/measure-end.svg
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666665 8.4666665"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="measure-end.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
width="52mm"
|
||||
units="px"
|
||||
inkscape:zoom="11.859035"
|
||||
inkscape:cx="20.912325"
|
||||
inkscape:cy="21.797726"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:deskcolor="#505050" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
id="linearGradient4717"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#0cffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4715" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.00136;stroke-linejoin:round;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 0.67200757,6.3113323 0.660837,1.9213136 4.8944675,4.0548848 Z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
62
frontend/react/public/images/markers/measure-start.svg
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666665 8.4666665"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="measure-start.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
width="52mm"
|
||||
units="px"
|
||||
inkscape:zoom="11.859035"
|
||||
inkscape:cx="20.912325"
|
||||
inkscape:cy="21.88205"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:deskcolor="#505050" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
id="linearGradient4717"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#0cffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4715" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<ellipse
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.0296;stroke-linejoin:round;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
cx="4.2414646"
|
||||
cy="4.1874084"
|
||||
rx="2.2678015"
|
||||
ry="2.267802" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
114
frontend/react/public/images/markers/path.svg
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="41"
|
||||
height="51"
|
||||
viewBox="0 0 41 51"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="path.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="15.941176"
|
||||
inkscape:cx="20.48155"
|
||||
inkscape:cy="25.5"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg5" />
|
||||
<g
|
||||
filter="url(#filter0_d_1082_10311)"
|
||||
id="g3"
|
||||
transform="translate(2.0701107,2.0701107)">
|
||||
<mask
|
||||
id="path-1-outside-1_1082_10311"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="2"
|
||||
y="0"
|
||||
width="33"
|
||||
height="43"
|
||||
fill="#000000">
|
||||
<rect
|
||||
fill="#ffffff"
|
||||
x="2"
|
||||
width="33"
|
||||
height="43"
|
||||
id="rect1"
|
||||
y="0" />
|
||||
<path
|
||||
d="m 19.957,39.8594 c -0.8906,1.1133 -2.5976,1.1133 -3.4882,0 C 12.6836,35.0352 4,23.5312 4,17 4,9.13281 10.3828,2.75 18.25,2.75 26.1172,2.75 32.5,9.13281 32.5,17 c 0,6.5312 -8.6836,18.0352 -12.543,22.8594 z M 18.25,12.25 c -1.707,0 -3.2656,0.9648 -4.1562,2.375 -0.8165,1.4844 -0.8165,3.3398 0,4.75 0.8906,1.4844 2.4492,2.375 4.1562,2.375 1.6328,0 3.1914,-0.8906 4.082,-2.375 0.8164,-1.4102 0.8164,-3.2656 0,-4.75 C 21.4414,13.2148 19.8828,12.25 18.25,12.25 Z"
|
||||
id="path1" />
|
||||
</mask>
|
||||
<path
|
||||
d="m 19.957,39.8594 c -0.8906,1.1133 -2.5976,1.1133 -3.4882,0 C 12.6836,35.0352 4,23.5312 4,17 4,9.13281 10.3828,2.75 18.25,2.75 26.1172,2.75 32.5,9.13281 32.5,17 c 0,6.5312 -8.6836,18.0352 -12.543,22.8594 z M 18.25,12.25 c -1.707,0 -3.2656,0.9648 -4.1562,2.375 -0.8165,1.4844 -0.8165,3.3398 0,4.75 0.8906,1.4844 2.4492,2.375 4.1562,2.375 1.6328,0 3.1914,-0.8906 4.082,-2.375 0.8164,-1.4102 0.8164,-3.2656 0,-4.75 C 21.4414,13.2148 19.8828,12.25 18.25,12.25 Z"
|
||||
fill="#247be2"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 16.4688,39.8594 -1.5735,1.2345 0.0058,0.0075 0.0059,0.0074 z m -2.375,-25.2344 -1.691,-1.068 -0.0323,0.0512 -0.0292,0.053 z m 0,4.75 -1.7309,1.0021 0.0078,0.0135 0.0081,0.0134 z m 8.2382,0 1.715,1.029 0.0081,-0.0134 0.0078,-0.0135 z m 0,-4.75 1.7525,-0.9638 -0.0292,-0.053 L 24.023,13.557 Z M 18.3953,38.61 c -0.0427,0.0533 -0.1026,0.0843 -0.1824,0.0843 -0.0798,0 -0.1398,-0.031 -0.1824,-0.0843 l -3.1235,2.4988 c 1.6913,2.1141 4.9205,2.1141 6.6118,0 z m -0.3531,0.0148 C 16.1614,36.2276 13.101,32.2175 10.5181,28.0214 9.22642,25.923 8.07852,23.817 7.25901,21.8721 6.42596,19.8951 6,18.2407 6,17 H 2 c 0,2.0249 0.65949,4.2577 1.5729,6.4254 0.92695,2.1998 2.18652,4.4959 3.53885,6.6928 2.70521,4.3948 5.87925,8.5487 7.78355,10.9757 z M 6,17 C 6,10.2374 11.4874,4.75 18.25,4.75 v -4 C 9.27824,0.75 2,8.02824 2,17 Z M 18.25,4.75 c 6.7626,0 12.25,5.4874 12.25,12.25 h 4 C 34.5,8.02824 27.2218,0.75 18.25,0.75 Z M 30.5,17 c 0,1.2405 -0.4259,2.8948 -1.2598,4.8712 -0.8204,1.9444 -1.9702,4.0499 -3.266,6.1476 -2.5908,4.1941 -5.6686,8.2033 -7.5789,10.5912 l 3.1235,2.4988 c 1.949,-2.4364 5.1427,-6.5913 7.8585,-10.9879 1.3576,-2.1978 2.6199,-4.4943 3.5483,-6.6947 C 33.8404,21.258 34.5,19.0251 34.5,17 Z M 18.25,10.25 c -2.4681,0 -4.632,1.3829 -5.8472,3.307 l 3.3819,2.136 C 16.3507,14.7968 17.304,14.25 18.25,14.25 Z m -5.9087,3.4112 c -1.1377,2.0685 -1.1625,4.6706 0.0216,6.7159 l 3.4617,-2.0042 c -0.4487,-0.775 -0.4736,-1.8838 0.0216,-2.7841 z m 0.0375,6.7428 c 1.2458,2.0763 3.4496,3.346 5.8712,3.346 v -4 c -0.9924,0 -1.9058,-0.5116 -2.4413,-1.404 z M 18.25,23.75 c 2.3645,0 4.5602,-1.2847 5.797,-3.346 l -3.43,-2.058 c -0.5444,0.9074 -1.4659,1.404 -2.367,1.404 z m 5.8129,-3.3729 c 1.1841,-2.0453 1.1592,-4.6474 0.0216,-6.7159 l -3.5049,1.9276 c 0.4951,0.9003 0.4703,2.0091 0.0216,2.7841 z M 24.023,13.557 C 22.8175,11.6483 20.6603,10.25 18.25,10.25 v 4 c 0.8553,0 1.8153,0.5314 2.3911,1.443 z"
|
||||
fill="#ffffff"
|
||||
mask="url(#path-1-outside-1_1082_10311)"
|
||||
id="path3" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs5">
|
||||
<filter
|
||||
id="filter0_d_1082_10311"
|
||||
x="0"
|
||||
y="0.75"
|
||||
width="40.5"
|
||||
height="49.9443"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood3" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix3" />
|
||||
<feOffset
|
||||
dx="2"
|
||||
dy="4"
|
||||
id="feOffset3" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
id="feGaussianBlur3" />
|
||||
<feComposite
|
||||
in2="hardAlpha"
|
||||
operator="out"
|
||||
id="feComposite3" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
|
||||
id="feColorMatrix4" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_1082_10311"
|
||||
id="feBlend4" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_1082_10311"
|
||||
result="shape"
|
||||
id="feBlend5" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
69
frontend/react/public/images/markers/pin.svg
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="52"
|
||||
height="52"
|
||||
viewBox="0 0 13.758333 13.758333"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="pin.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
width="52mm"
|
||||
units="px"
|
||||
inkscape:zoom="11.859035"
|
||||
inkscape:cx="20.870164"
|
||||
inkscape:cy="21.88205"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:deskcolor="#505050" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
id="linearGradient4717"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#0cffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4715" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#272727;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect1"
|
||||
width="0.69414598"
|
||||
height="2.7608078"
|
||||
x="6.5155063"
|
||||
y="3.8651309" />
|
||||
<ellipse
|
||||
style="fill:#ffffff;stroke:#272727;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
cx="6.867465"
|
||||
cy="2.2450833"
|
||||
rx="1.2483708"
|
||||
ry="1.2483709" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -3,7 +3,7 @@ import { getApp } from "../olympusapp";
|
||||
import { BoxSelect } from "./boxselect";
|
||||
import { Airbase } from "../mission/airbase";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { areaContains, bearing, bearingAndDistanceToLatLng, deepCopyTable, deg2rad, getGroundElevation, mToFt, mToNm, nmToM, rad2deg } from "../other/utils";
|
||||
import { areaContains, deepCopyTable, deg2rad, getGroundElevation } from "../other/utils";
|
||||
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
|
||||
import { ClickableMiniMap } from "./clickableminimap";
|
||||
import {
|
||||
@ -67,6 +67,9 @@ import {
|
||||
import { ContextActionSet } from "../unit/contextactionset";
|
||||
import { SmokeMarker } from "./markers/smokemarker";
|
||||
import { MeasureMarker } from "./markers/measuremarker";
|
||||
import { MeasureStartMarker } from "./markers/measurestartmarker";
|
||||
import { MeasureEndMarker } from "./markers/measureendmarker";
|
||||
import { Measure } from "./measure";
|
||||
|
||||
/* Register the handler for the box selection */
|
||||
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
|
||||
@ -107,6 +110,7 @@ export class Map extends L.Map {
|
||||
#isDragging: boolean = false;
|
||||
#isSelecting: boolean = false;
|
||||
|
||||
#originalMouseClickLatLng: L.LatLng | null = null;
|
||||
#debounceTimeout: number | null = null;
|
||||
#isLeftMouseDown: boolean = false;
|
||||
#isRightMouseDown: boolean = false;
|
||||
@ -157,9 +161,11 @@ export class Map extends L.Map {
|
||||
#IPToTargetLine: L.Polygon | null = null;
|
||||
|
||||
/* Measure tool */
|
||||
#measureReference: L.LatLng | null = null;
|
||||
#measureLines: L.Polyline[] = [];
|
||||
#measureMarkers: MeasureMarker[] = [];
|
||||
#measures: Measure[] = [];
|
||||
|
||||
/* State variables */
|
||||
#previousAppState: OlympusState = OlympusState.IDLE;
|
||||
#previousAppSubstate: OlympusSubState = NO_SUBSTATE;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -338,6 +344,15 @@ export class Map extends L.Map {
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
}).addShortcut("clearMeasures", {
|
||||
label: "Clear measures",
|
||||
keyUpCallback: () => {
|
||||
this.clearMeasures();
|
||||
},
|
||||
code: "KeyComma",
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
})
|
||||
.addShortcut("toggleUnitLabels", {
|
||||
label: "Hide/show labels",
|
||||
@ -854,7 +869,24 @@ export class Map extends L.Map {
|
||||
this.getContainer().classList.remove(`explosion-cursor`);
|
||||
["white", "blue", "red", "green", "orange"].forEach((color) => this.getContainer().classList.remove(`smoke-${color}-cursor`));
|
||||
this.getContainer().classList.remove(`plus-cursor`);
|
||||
|
||||
this.getContainer().classList.remove(`measure-cursor`);
|
||||
|
||||
/* Clear the last measure if the state is changed */
|
||||
if (this.#previousAppState === OlympusState.MEASURE) {
|
||||
if (this.#measures.length > 0 && this.#measures[this.#measures.length - 1].isActive()) {
|
||||
this.#measures[this.#measures.length - 1].remove();
|
||||
this.#measures.pop();
|
||||
if (this.#measures.length > 0) {
|
||||
if (this.#measures[this.#measures.length - 1].getDistance() < 1) {
|
||||
this.#measures[this.#measures.length - 1].remove();
|
||||
this.#measures.pop();
|
||||
} else {
|
||||
this.#measures[this.#measures.length - 1].showEndMarker();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Operations to perform when entering a state */
|
||||
if (state === OlympusState.IDLE) {
|
||||
getApp().getUnitsManager()?.deselectAllUnits();
|
||||
@ -883,7 +915,12 @@ export class Map extends L.Map {
|
||||
console.log(this.#contextAction);
|
||||
} else if (state === OlympusState.DRAW) {
|
||||
if (subState === DrawSubState.DRAW_CIRCLE || subState === DrawSubState.DRAW_POLYGON) this.getContainer().classList.add(`plus-cursor`);
|
||||
} else if (state === OlympusState.MEASURE) {
|
||||
this.getContainer().classList.add(`measure-cursor`);
|
||||
}
|
||||
|
||||
this.#previousAppState = state;
|
||||
this.#previousAppSubstate = subState;
|
||||
}
|
||||
|
||||
#onDragStart(e: any) {
|
||||
@ -918,18 +955,10 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
#onMouseDown(e: any) {
|
||||
this.#originalMouseClickLatLng = e.latlng;
|
||||
if (e.originalEvent?.button === 0) {
|
||||
this.#isLeftMouseDown = true;
|
||||
this.#leftMouseDownEpoch = Date.now();
|
||||
|
||||
/* If we are in the measure state there can only be a short click so immediately perform the action */
|
||||
if (getApp().getState() === OlympusState.MEASURE) {
|
||||
if (this.#measureLines.length > 0 && this.#measureReference)
|
||||
this.#measureLines[this.#measureLines.length - 1].setLatLngs([this.#measureReference, e.latlng]);
|
||||
this.#measureReference = e.latlng;
|
||||
this.#measureLines.push(new L.Polyline([this.#measureReference, e.latlng], { color: "magenta" }).addTo(this));
|
||||
this.#measureMarkers.push(new MeasureMarker(e.latlng, "", 0).addTo(this));
|
||||
}
|
||||
} else if (e.originalEvent?.button === 2) {
|
||||
this.#isRightMouseDown = true;
|
||||
this.#rightMouseDownEpoch = Date.now();
|
||||
@ -947,8 +976,6 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
#onLeftShortClick(e: L.LeafletMouseEvent) {
|
||||
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
|
||||
|
||||
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) {
|
||||
this.#debounceTimeout = window.setTimeout(() => {
|
||||
if (!this.#isSelecting) {
|
||||
@ -1060,12 +1087,25 @@ export class Map extends L.Map {
|
||||
else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
|
||||
else getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
} else if (getApp().getState() === OlympusState.MEASURE) {
|
||||
/* Do nothing, we already clicked on the mouse down callback */
|
||||
const newMeasure = new Measure(this);
|
||||
const previousMeasure = this.#measures[this.#measures.length - 1];
|
||||
this.#measures.push(newMeasure);
|
||||
newMeasure.onClick(e.latlng);
|
||||
if (previousMeasure && previousMeasure.isActive()) {
|
||||
previousMeasure.finish();
|
||||
previousMeasure.hideEndMarker();
|
||||
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
|
||||
previousMeasure.moveMarkers(null, startLatLng);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
|
||||
else getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
|
||||
this.#debounceTimeout = null;
|
||||
}, DEBOUNCE_MILLISECONDS);
|
||||
}
|
||||
}
|
||||
@ -1128,18 +1168,13 @@ export class Map extends L.Map {
|
||||
if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng);
|
||||
if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng);
|
||||
} else if (getApp().getState() === OlympusState.MEASURE) {
|
||||
if (this.#measureLines.length > 0) this.#measureLines[this.#measureLines.length - 1].setLatLngs([this.#measureReference, e.latlng]);
|
||||
if (this.#measureMarkers.length > 0 && this.#measureReference) {
|
||||
const distance = this.#measureReference.distanceTo(e.latlng);
|
||||
let distanceString = ""
|
||||
if (distance > nmToM(1)) distanceString = `${mToNm(distance).toFixed(2)} NM`;
|
||||
else distanceString = `${mToFt(distance).toFixed(2)} ft`;
|
||||
const bearingTo = deg2rad(bearing(this.#measureReference.lat, this.#measureReference.lng, e.latlng.lat, e.latlng.lng, false));
|
||||
const halfPoint = bearingAndDistanceToLatLng(this.#measureReference.lat, this.#measureReference.lng, bearingTo, distance/2);
|
||||
const bearingString = `${(Math.floor(rad2deg(bearingTo) + 360) % 360)}°`;
|
||||
this.#measureMarkers[this.#measureMarkers.length - 1].setLatLng(halfPoint);
|
||||
this.#measureMarkers[this.#measureMarkers.length - 1].setRotationAngle(bearingTo + Math.PI / 2);
|
||||
this.#measureMarkers[this.#measureMarkers.length - 1].setTextValue(`${distanceString} - ${bearingString}`);
|
||||
if (this.#debounceTimeout === null) {
|
||||
this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng);
|
||||
let totalLength = 0;
|
||||
this.#measures.forEach((measure) => {
|
||||
measure.setTotalDistance(totalLength);
|
||||
totalLength += measure.getDistance();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -1208,11 +1243,6 @@ export class Map extends L.Map {
|
||||
});
|
||||
}
|
||||
|
||||
#clearMeasures() {
|
||||
this.#measureLines.forEach((line) => line.removeFrom(this));
|
||||
this.#measureMarkers.forEach((marker) => marker.removeFrom(this));
|
||||
}
|
||||
|
||||
/* */
|
||||
#panToUnit(unit: Unit) {
|
||||
var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng);
|
||||
@ -1294,4 +1324,9 @@ export class Map extends L.Map {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearMeasures() {
|
||||
this.#measures.forEach((measure) => measure.remove());
|
||||
this.#measures = [];
|
||||
}
|
||||
}
|
||||
|
||||
57
frontend/react/src/map/markers/measureendmarker.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { DivIcon, LatLngExpression, Map, MarkerOptions } from "leaflet";
|
||||
import { CustomMarker } from "./custommarker";
|
||||
import { SVGInjector } from "@tanem/svg-injector";
|
||||
|
||||
export class MeasureEndMarker extends CustomMarker {
|
||||
#rotationAngle: number = 0;
|
||||
|
||||
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
|
||||
super(latlng, options);
|
||||
this.options.interactive = true;
|
||||
this.options.draggable = true;
|
||||
this.setZIndexOffset(9999);
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
this.setIcon(
|
||||
new DivIcon({
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
className: "leaflet-measure-end-marker",
|
||||
})
|
||||
);
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("ol-measure-end-icon");
|
||||
var img = document.createElement("img");
|
||||
img.src = "images/markers/measure-end.svg";
|
||||
img.onload = () => {
|
||||
SVGInjector(img);
|
||||
this.#applyRotation();
|
||||
}
|
||||
el.appendChild(img);
|
||||
this.getElement()?.appendChild(el);
|
||||
}
|
||||
|
||||
setRotationAngle(angle: number) {
|
||||
this.#rotationAngle = angle;
|
||||
this.#applyRotation();
|
||||
}
|
||||
|
||||
getRotationAngle() {
|
||||
return this.#rotationAngle;
|
||||
}
|
||||
|
||||
onAdd(map: Map): this {
|
||||
super.onAdd(map);
|
||||
this.#applyRotation();
|
||||
return this;
|
||||
}
|
||||
|
||||
#applyRotation() {
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const svg = element.querySelector("svg");
|
||||
if (svg) svg.style.transform = `rotate(${this.#rotationAngle - Math.PI / 2}rad)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,7 @@ import { Marker, LatLng, DivIcon, Map } from "leaflet";
|
||||
|
||||
export class MeasureMarker extends Marker {
|
||||
#textValue: string;
|
||||
#isEditable: boolean = false;
|
||||
#rotationAngle: number; // Rotation angle in radians
|
||||
#previousValue: string;
|
||||
|
||||
onValueUpdated: (value: number) => void = () => {};
|
||||
onDeleteButtonClicked: () => void = () => {};
|
||||
@ -56,9 +54,7 @@ export class MeasureMarker extends Marker {
|
||||
*/
|
||||
setRotationAngle(angle: number) {
|
||||
this.#rotationAngle = angle;
|
||||
if (!this.#isEditable) {
|
||||
this.#updateRotation();
|
||||
}
|
||||
this.#updateRotation();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
30
frontend/react/src/map/markers/measurestartmarker.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
|
||||
import { CustomMarker } from "./custommarker";
|
||||
import { SVGInjector } from "@tanem/svg-injector";
|
||||
|
||||
export class MeasureStartMarker extends CustomMarker {
|
||||
|
||||
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
|
||||
super(latlng, options);
|
||||
this.options.interactive = true;
|
||||
this.options.draggable = true;
|
||||
this.setZIndexOffset(9999);
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
this.setIcon(
|
||||
new DivIcon({
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
className: "leaflet-measure-start-marker",
|
||||
})
|
||||
);
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("ol-measure-start-icon");
|
||||
var img = document.createElement("img");
|
||||
img.src = "images/markers/measure-start.svg";
|
||||
img.onload = () => SVGInjector(img);
|
||||
el.appendChild(img);
|
||||
this.getElement()?.appendChild(el);
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ export class SmokeMarker extends CustomMarker {
|
||||
);
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("ol-smoke-icon");
|
||||
el.setAttribute("data-color", this.#color);
|
||||
el.style.fill = this.#color;
|
||||
var img = document.createElement("img");
|
||||
img.src = "images/markers/smoke.svg";
|
||||
img.onload = () => SVGInjector(img);
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
|
||||
}
|
||||
|
||||
.airbase-icon svg {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: white;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
|
||||
}
|
||||
|
||||
.bullseye-icon[data-coalition="red"] svg * {
|
||||
|
||||
@ -4,23 +4,36 @@
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
|
||||
width: fit-content !important;
|
||||
height: fit-content !important;
|
||||
margin: 0 !important;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
/* Container for the measure marker content */
|
||||
.leaflet-measure-marker .container {
|
||||
min-width: 150px;
|
||||
transform-origin: center;
|
||||
background-color: var(--background-steel);
|
||||
color: white;
|
||||
background-color: white;
|
||||
color: #272727;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
align-content: center;
|
||||
border: 2px solid transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Text inside the measure marker */
|
||||
.leaflet-measure-marker .text {
|
||||
margin-left: 12px;
|
||||
display: block;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
font-weight: bolder;
|
||||
text-wrap: nowrap;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.leaflet-measure-start-marker, .leaflet-measure-end-marker {
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
|
||||
}
|
||||
|
||||
[data-object|="unit"].attack-cursor {
|
||||
|
||||
118
frontend/react/src/map/measure.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { LatLng, LeafletMouseEvent, Polyline } from "leaflet";
|
||||
import { Map } from "./map";
|
||||
import { MeasureMarker } from "./markers/measuremarker";
|
||||
import { MeasureStartMarker } from "./markers/measurestartmarker";
|
||||
import { MeasureEndMarker } from "./markers/measureendmarker";
|
||||
import { bearing, deg2rad, midpoint, mToFt, mToNm, nmToM, rad2deg } from "../other/utils";
|
||||
import { AppStateChangedEvent } from "../events";
|
||||
import { OlympusState } from "../constants/constants";
|
||||
|
||||
export class Measure {
|
||||
#active: boolean = false;
|
||||
#map: Map;
|
||||
#line: Polyline;
|
||||
#measureMarker: MeasureMarker;
|
||||
#startMarker: MeasureStartMarker;
|
||||
#endMarker: MeasureEndMarker;
|
||||
#totalDistance: number = 0;
|
||||
onMarkerMoved: (startLatLng: LatLng, endLatLng: LatLng) => void = () => {};
|
||||
|
||||
constructor(map) {
|
||||
this.#map = map;
|
||||
}
|
||||
|
||||
onClick(latlng: LatLng) {
|
||||
if (this.#startMarker === undefined) {
|
||||
this.#startMarker = new MeasureStartMarker(latlng).addTo(this.#map);
|
||||
|
||||
this.#endMarker = new MeasureEndMarker(latlng).addTo(this.#map);
|
||||
this.#line = new Polyline([this.#startMarker.getLatLng(), this.#endMarker.getLatLng()], { color: "#FFFFFF", dashArray: "5, 5" }).addTo(this.#map);
|
||||
this.#measureMarker = new MeasureMarker(new LatLng(0, 0), "", 0).addTo(this.#map);
|
||||
|
||||
this.#startMarker.on("drag", (event) => {
|
||||
this.#onMarkersMove();
|
||||
});
|
||||
|
||||
this.#endMarker.on("drag", (event) => {
|
||||
this.#onMarkersMove();
|
||||
});
|
||||
|
||||
this.#active = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(latlng: LatLng) {
|
||||
if (this.#endMarker !== undefined && this.isActive()) {
|
||||
this.#endMarker.setLatLng(latlng);
|
||||
this.#onMarkersMove();
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.#startMarker !== undefined) this.#map.removeLayer(this.#startMarker);
|
||||
if (this.#endMarker !== undefined) this.#map.removeLayer(this.#endMarker);
|
||||
if (this.#line !== undefined) this.#map.removeLayer(this.#line);
|
||||
if (this.#measureMarker !== undefined) this.#map.removeLayer(this.#measureMarker);
|
||||
}
|
||||
|
||||
hideEndMarker() {
|
||||
if (this.#endMarker !== undefined) this.#map.removeLayer(this.#endMarker);
|
||||
}
|
||||
|
||||
showEndMarker() {
|
||||
this.#onMarkersMove();
|
||||
if (this.#endMarker !== undefined) this.#endMarker.addTo(this.#map);
|
||||
}
|
||||
|
||||
moveMarkers(startLatLng: LatLng | null, endLatLng: LatLng | null) {
|
||||
startLatLng && this.#startMarker.setLatLng(startLatLng);
|
||||
endLatLng && this.#endMarker.setLatLng(endLatLng);
|
||||
this.#onMarkersMove();
|
||||
}
|
||||
|
||||
getDistance() {
|
||||
return this.#startMarker.getLatLng().distanceTo(this.#endMarker.getLatLng());
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.#active = false;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.#active;
|
||||
}
|
||||
|
||||
setTotalDistance(distance: number) {
|
||||
this.#totalDistance = distance;
|
||||
}
|
||||
|
||||
#onMarkersMove() {
|
||||
const distance = this.#startMarker.getLatLng().distanceTo(this.#endMarker.getLatLng());
|
||||
let distanceString = "";
|
||||
if (distance > nmToM(1)) distanceString = `${mToNm(distance).toFixed(distance < nmToM(10) ? 2 : 0)} NM`;
|
||||
else distanceString = `${mToFt(distance).toFixed(0)} ft`;
|
||||
const bearingTo = deg2rad(
|
||||
bearing(this.#startMarker.getLatLng().lat, this.#startMarker.getLatLng().lng, this.#endMarker.getLatLng().lat, this.#endMarker.getLatLng().lng, false)
|
||||
);
|
||||
|
||||
if (this.#totalDistance > 0) {
|
||||
if (this.#totalDistance + this.getDistance() > nmToM(1)) distanceString += ` / ${mToNm(this.#totalDistance + this.getDistance()).toFixed(0)} NM`;
|
||||
else distanceString += ` / ${mToFt(this.#totalDistance + this.getDistance()).toFixed(0)} ft`;
|
||||
}
|
||||
|
||||
const halfPoint = midpoint(
|
||||
this.#startMarker.getLatLng().lat,
|
||||
this.#startMarker.getLatLng().lng,
|
||||
this.#endMarker.getLatLng().lat,
|
||||
this.#endMarker.getLatLng().lng
|
||||
);
|
||||
const bearingString = `${Math.floor(rad2deg(bearingTo) + 360) % 360}°`;
|
||||
this.#measureMarker.setLatLng(halfPoint);
|
||||
this.#measureMarker.setRotationAngle(bearingTo + Math.PI / 2);
|
||||
this.#measureMarker.setTextValue(`${distanceString} - ${bearingString}`);
|
||||
this.#endMarker.setRotationAngle(bearingTo);
|
||||
this.#line.setLatLngs([this.#startMarker.getLatLng(), this.#endMarker.getLatLng()]);
|
||||
|
||||
this.onMarkerMoved(this.#startMarker.getLatLng(), this.#endMarker.getLatLng());
|
||||
}
|
||||
}
|
||||
@ -139,12 +139,15 @@
|
||||
background-image: url("/images/markers/target.svg");
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.ol-spot-icon {
|
||||
background-image: url("/images/markers/target.svg");
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.ol-text-icon {
|
||||
@ -159,26 +162,7 @@
|
||||
|
||||
.ol-smoke-icon {
|
||||
opacity: 75%;
|
||||
}
|
||||
|
||||
[data-color="white"].ol-smoke-icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
[data-color="blue"].ol-smoke-icon {
|
||||
fill: blue;
|
||||
}
|
||||
|
||||
[data-color="red"].ol-smoke-icon {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
[data-color="green"].ol-smoke-icon {
|
||||
fill: green;
|
||||
}
|
||||
|
||||
[data-color="orange"].ol-smoke-icon {
|
||||
fill: orange;
|
||||
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.ol-explosion-icon * {
|
||||
@ -225,6 +209,11 @@ path.leaflet-interactive:focus {
|
||||
cursor: url("/images/cursors/plus.svg"), auto !important;
|
||||
}
|
||||
|
||||
.measure-cursor {
|
||||
cursor: url("/images/cursors/measure.svg"), auto !important;
|
||||
}
|
||||
|
||||
|
||||
#map-container.leaflet-grab {
|
||||
cursor: url("/images/cursors/grab.svg") 16 16, auto;
|
||||
}
|
||||
|
||||
@ -46,6 +46,32 @@ export function bearingAndDistanceToLatLng(lat: number, lon: number, brng: numbe
|
||||
return new LatLng(rad2deg(φ2), rad2deg(λ2));
|
||||
}
|
||||
|
||||
export function midpoint(lat1: number, lon1: number, lat2: number, lon2: number, zoom: number = 10) {
|
||||
const φ1 = deg2rad(lat1); // Convert latitude of point 1 from degrees to radians
|
||||
const λ1 = deg2rad(lon1); // Convert longitude of point 1 from degrees to radians
|
||||
const φ2 = deg2rad(lat2); // Convert latitude of point 2 from degrees to radians
|
||||
const λ2 = deg2rad(lon2); // Convert longitude of point 2 from degrees to radians
|
||||
|
||||
// Convert point 1 to Mercator projection coordinates
|
||||
const x1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ1);
|
||||
const y1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ1 / 2)));
|
||||
|
||||
// Convert point 2 to Mercator projection coordinates
|
||||
const x2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ2);
|
||||
const y2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ2 / 2)));
|
||||
|
||||
// Calculate the midpoint in Mercator projection coordinates
|
||||
const mx = (x1 + x2) / 2;
|
||||
const my = (y1 + y2) / 2;
|
||||
|
||||
// Convert the midpoint back to latitude and longitude
|
||||
const λ = (2 * Math.PI * mx / Math.pow(2, zoom)) - Math.PI;
|
||||
const φ = 2 * Math.atan(Math.exp(Math.PI - (2 * Math.PI * my) / Math.pow(2, zoom))) - Math.PI / 2;
|
||||
|
||||
// Return the midpoint as a LatLng object
|
||||
return new LatLng(rad2deg(φ), rad2deg(λ));
|
||||
}
|
||||
|
||||
export function ConvertDDToDMS(D: number, lng: boolean) {
|
||||
var deg = 0 | (D < 0 ? (D = -D) : D);
|
||||
var min = 0 | (((D += 1e-9) % 1) * 60);
|
||||
@ -93,6 +119,11 @@ export function latLngToMGRS(lat: number, lng: number, precision: number = 4): M
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (lng > 360 || lng < -180 || lat > 84 || lat < -80) {
|
||||
console.error("latLngToMGRS: value outside of bounds");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision);
|
||||
const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`));
|
||||
if (match) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { colors, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons";
|
||||
import { LatLng } from "leaflet";
|
||||
@ -97,7 +97,7 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
|
||||
<span className="my-auto text-white">Smoke color</span>
|
||||
</div>
|
||||
<div className="flex w-full gap-2">
|
||||
{["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => {
|
||||
{[colors.WHITE, colors.BLUE, colors.RED, colors.GREEN, colors.ORANGE].map((optionSmokeColor) => {
|
||||
return (
|
||||
<OlStateButton
|
||||
checked={smokeColor === optionSmokeColor}
|
||||
@ -132,7 +132,7 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
|
||||
getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
|
||||
getApp()
|
||||
.getMap()
|
||||
.addSmokeMarker(props.latlng, smokeColor ?? "white");
|
||||
.addSmokeMarker(props.latlng, smokeColor ?? colors.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
SelectionEnabledChangedEvent,
|
||||
ShortcutsChangedEvent,
|
||||
} from "../../events";
|
||||
import { faCopy, faObjectGroup, faPaste } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCopy, faEraser, faObjectGroup, faPaste, faTape } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { ShortcutOptions, UnitData } from "../../interfaces";
|
||||
import { Unit } from "../../unit/unit";
|
||||
@ -215,6 +215,40 @@ export function MapToolBar(props: {}) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"measure"}
|
||||
checked={appState === OlympusState.MEASURE}
|
||||
icon={faTape}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["measure"]?.getOptions())}
|
||||
<div className="my-auto">Enter measure mode</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().setState(appState === OlympusState.MEASURE? OlympusState.IDLE : OlympusState.MEASURE);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"clearMeasures"}
|
||||
checked={false}
|
||||
icon={faEraser}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
|
||||
<div className="my-auto">Clear all measures</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().getMap().clearMeasures();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{reorderedActions.map((contextActionIt: ContextAction) => {
|
||||
|
||||
@ -5,6 +5,7 @@ import { RadioSink } from "../../audio/radiosink";
|
||||
import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { colors } from "../../constants/constants";
|
||||
|
||||
export function RadiosSummaryPanel(props: {}) {
|
||||
const [audioSinks, setAudioSinks] = useState([] as AudioSink[]);
|
||||
@ -41,7 +42,7 @@ export function RadiosSummaryPanel(props: {}) {
|
||||
radioSink.setPtt(false);
|
||||
}}
|
||||
tooltip="Click to talk, lights up when receiving"
|
||||
buttonColor={radioSink.getReceiving() ? "white" : null}
|
||||
buttonColor={radioSink.getReceiving() ? colors.WHITE : undefined}
|
||||
className="min-h-12 min-w-12"
|
||||
>
|
||||
<span className={`text-gray-200`}><FaRadio className={`
|
||||
|
||||
@ -77,9 +77,8 @@ import { get } from "http";
|
||||
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
|
||||
|
||||
var pathIcon = new Icon({
|
||||
iconUrl: "images/markers/marker-icon.png",
|
||||
shadowUrl: "images/markers/marker-shadow.png",
|
||||
iconAnchor: [13, 41],
|
||||
iconUrl: "images/markers/path.svg",
|
||||
iconAnchor: [20, 43],
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||