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