1// Copyright 2022 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import { LitElement, css, html, svg } from "lit"; 16 17function createIsometricMatrix() { 18 const m = new DOMMatrix(); 19 20 const angle = (Math.atan(Math.SQRT2) * 180) / Math.PI; 21 22 m.rotateAxisAngleSelf(1, 0, 0, angle); 23 m.rotateAxisAngleSelf(0, 0, 1, -45); 24 m.scaleSelf(0.7, 0.7, 0.7); 25 26 return m; 27} 28 29export const PROJECTION = createIsometricMatrix(); 30 31export class Map extends LitElement { 32 static styles = css` 33 svg { 34 margin-top: -15%; 35 } 36 37 .dragging { 38 cursor: grab; 39 } 40 `; 41 42 static properties = { 43 devices: {}, 44 }; 45 46 constructor() { 47 super(); 48 this.devices = []; 49 this.selected = null; 50 this.dragging = false; 51 this.changingElevation = false; 52 this.onMouseUp = this.onMouseUp.bind(this); 53 } 54 55 connectedCallback() { 56 super.connectedCallback(); 57 window.addEventListener("mouseup", this.onMouseUp); 58 } 59 60 disconnectedCallback() { 61 window.removeEventListener("mouseup", this.onMouseUp); 62 super.disconnectedCallback(); 63 } 64 65 onMouseDown(event) { 66 if (event.target.classList?.contains("handle")) { 67 this.changingElevation = true; 68 } else { 69 const element = event.composedPath().find((el) => el.classList?.contains("marker")); 70 if (element) { 71 const key = element.getAttribute("key"); 72 this.selected = this.devices[key]; 73 this.dragging = true; 74 } else { 75 this.selected = null; 76 } 77 this.dispatchEvent( 78 new CustomEvent("select", { detail: { device: this.selected } }) 79 ); 80 this.update(); 81 } 82 } 83 84 onMouseUp() { 85 if (this.dragging || this.changingElevation) 86 this.dispatchEvent( 87 new CustomEvent("end-move", { detail: { device: this.selected } }) 88 ); 89 this.dragging = false; 90 this.changingElevation = false; 91 this.update(); 92 } 93 94 onMouseMove(event) { 95 if (this.dragging) { 96 const to = this.screenToSvg(event.clientX, event.clientY); 97 this.selected.position.x = Math.min( 98 Math.max(Math.floor(-to.x) + this.selected.position.y, -600 + 40), 99 600 - 40 100 ); 101 this.selected.position.z = Math.min( 102 Math.max(Math.floor(to.y) + this.selected.position.y, -600 + 40), 103 600 - 40 104 ); 105 this.update(); 106 } 107 if (this.changingElevation) { 108 const distance = Math.min(event.movementY * 2, this.selected.position.y); 109 this.selected.position.y -= distance; 110 this.update(); 111 } 112 if (this.dragging || this.changingElevation) { 113 this.dispatchEvent(new CustomEvent("move")); 114 } 115 } 116 117 screenToSvg(x, y) { 118 const svg = this.renderRoot.children[0]; 119 const point = svg.createSVGPoint(); 120 point.x = x; 121 point.y = y; 122 return point.matrixTransform(svg.getScreenCTM().inverse()); 123 } 124 125 render() { 126 const m = PROJECTION; 127 return html`<svg 128 transform="matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m.f})" 129 viewBox="-600 -600 1200 1200" 130 @mousemove="${this.onMouseMove}" 131 @mousedown="${this.onMouseDown}" 132 class="${this.dragging ? "dragging" : ""}" 133 > 134 <defs> 135 <pattern 136 id="small" 137 width="20" 138 height="20" 139 patternUnits="userSpaceOnUse" 140 > 141 <path 142 d="M20,0 L0,0 L0,20" 143 fill="none" 144 stroke="var(--grid-line-color)" 145 stroke-width="1" 146 /> 147 </pattern> 148 <pattern 149 id="grid" 150 width="100" 151 height="100" 152 patternUnits="userSpaceOnUse" 153 > 154 <rect width="100" height="100" fill="url(#small)" /> 155 <path 156 d="M100,0 L0,0 L0,100" 157 fill="none" 158 stroke="var(--grid-line-color)" 159 stroke-width="2" 160 /> 161 </pattern> 162 </defs> 163 164 <rect 165 x="-50%" 166 y="-50%" 167 fill="var(--grid-background-color)" 168 width="100%" 169 height="100%" 170 /> 171 <rect 172 x="-50%" 173 y="-50%" 174 fill="url(#grid)" 175 width="100%" 176 height="100%" 177 ></rect> 178 179 ${this.devices.map( 180 (device, i) => svg` 181 <g key="${i}" transform=${`translate(${ 182 -device.position.x + device.position.y 183 } ${device.position.z - device.position.y})`} 184 class="${device == this.selected ? "selected marker" : "marker"}"> 185 <rect x="-40" y="-40" width="40" height="40" fill="#f44336" transform="skewY(-45)"></rect> 186 <rect x="0" y="0" width="40" height="40" fill="#ffeb3b" transform="skewX(-45)"></rect> 187 <rect x="0" y="-40" width="40" height="40" fill="#2196f3" transform=></rect> 188 189 <!-- Selection outline --> 190 <g stroke="var(--selection-color)" fill="var(--selection-color)" stroke-width="5"> 191 ${ 192 device == this.selected 193 ? svg` 194 <line x1="-40" y1="-40" x2="0" y2="-40" transform="skewY(-45)"></line> 195 <line x1="-40" y1="-40" x2="-40" y2="0" transform="skewY(-45)"></line> 196 <line x1="0" y1="40" x2="40" y2="40" transform="skewX(-45)"></line> 197 <line x1="40" y1="40" x2="40" y2="0" transform="skewX(-45)"></line> 198 <line x1="0" y1="-40" x2="40" y2="-40"></line> 199 <line x1="40" y1="-40" x2="40" y2="0"></line> 200 201 <circle cx="36" cy="-36" r="8" class="handle"></circle> 202 ` 203 : "" 204 } 205 </g> 206 207 <!-- Elevation arrow --> 208 <g stroke="black" stroke-width="5"> 209 ${ 210 device.position.y == 0 211 ? "" 212 : svg` 213 <line x1="-40" y1="0" x2="-${ 214 40 + device.position.y 215 }" y2="0" transform="skewY(-45)" stroke-dasharray="8" /> 216 <path d="M-${40 + device.position.y},-10 L-${ 217 40 + device.position.y 218 },10" transform="skewY(-45)" /> 219 <path d="M-${40 + device.position.y},-10 L-${ 220 40 + device.position.y 221 },10" transform="skewY(-45) skewX(45)"> 222 ` 223 } 224 </g> 225 </g> 226 ` 227 )} 228 </svg>`; 229 } 230} 231customElements.define("pika-map", Map); 232