blue-chestnut-solar-expert 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/components/index.d.ts +33 -0
- package/dist/components/map-draw.d.ts +11 -0
- package/dist/components/solar-calculator.d.ts +11 -0
- package/dist/components/solar-expert.d.ts +11 -0
- package/dist/stencil-library/api-By7kNIGr.js +18774 -0
- package/dist/stencil-library/api-By7kNIGr.js.map +1 -0
- package/dist/stencil-library/app-globals-DQuL1Twl.js +6 -0
- package/dist/stencil-library/app-globals-DQuL1Twl.js.map +1 -0
- package/dist/stencil-library/decoder-DSavpK4g.js +29 -0
- package/dist/stencil-library/decoder-DSavpK4g.js.map +1 -0
- package/dist/stencil-library/deflate-Cpl_7R0h.js +13 -0
- package/dist/stencil-library/deflate-Cpl_7R0h.js.map +1 -0
- package/dist/stencil-library/index-CEfm4WRP.js +4170 -0
- package/dist/stencil-library/index-CEfm4WRP.js.map +1 -0
- package/dist/stencil-library/index-DimvNaNS.js +412 -0
- package/dist/stencil-library/index-DimvNaNS.js.map +1 -0
- package/dist/stencil-library/index.esm.js +12 -0
- package/dist/stencil-library/index.esm.js.map +1 -0
- package/dist/stencil-library/jpeg-3kYgfUiy.js +902 -0
- package/dist/stencil-library/jpeg-3kYgfUiy.js.map +1 -0
- package/dist/stencil-library/lerc-D9ISp5i_.js +2487 -0
- package/dist/stencil-library/lerc-D9ISp5i_.js.map +1 -0
- package/dist/stencil-library/loader.esm.js.map +1 -0
- package/dist/stencil-library/lucide-DGcPbaht.js +27006 -0
- package/dist/stencil-library/lucide-DGcPbaht.js.map +1 -0
- package/dist/stencil-library/lzw-15JscBc_.js +136 -0
- package/dist/stencil-library/lzw-15JscBc_.js.map +1 -0
- package/dist/stencil-library/map-draw.entry.esm.js.map +1 -0
- package/dist/stencil-library/map-draw.entry.js +2777 -0
- package/dist/stencil-library/map-draw.entry.js.map +1 -0
- package/dist/stencil-library/packbits-i_L--d7r.js +31 -0
- package/dist/stencil-library/packbits-i_L--d7r.js.map +1 -0
- package/dist/stencil-library/pako.esm-BdkEMvj8.js +6879 -0
- package/dist/stencil-library/pako.esm-BdkEMvj8.js.map +1 -0
- package/dist/stencil-library/raw-Cp-44rFp.js +12 -0
- package/dist/stencil-library/raw-Cp-44rFp.js.map +1 -0
- package/dist/stencil-library/solar-calculator.entry.esm.js.map +1 -0
- package/dist/stencil-library/solar-calculator.entry.js +70 -0
- package/dist/stencil-library/solar-calculator.entry.js.map +1 -0
- package/dist/stencil-library/solar-expert.entry.esm.js.map +1 -0
- package/dist/stencil-library/solar-expert.entry.js +66 -0
- package/dist/stencil-library/solar-expert.entry.js.map +1 -0
- package/dist/stencil-library/stencil-library.esm.js +51 -0
- package/dist/stencil-library/stencil-library.esm.js.map +1 -0
- package/dist/stencil-library/webimage-Cn4h3lmO.js +45 -0
- package/dist/stencil-library/webimage-Cn4h3lmO.js.map +1 -0
- package/dist/types/components/map-draw/map-draw.d.ts +58 -0
- package/dist/types/components/solar-calculator/solar-calculator.d.ts +15 -0
- package/dist/types/components/solar-expert/solar-expert.d.ts +15 -0
- package/dist/types/components.d.ts +81 -0
- package/dist/types/config.d.ts +15 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/stencil-public-runtime.d.ts +1702 -0
- package/dist/types/types/shapes.d.ts +32 -0
- package/dist/types/utils/api.d.ts +13 -0
- package/dist/types/utils/geometry/fitting.d.ts +89 -0
- package/dist/types/utils/geometry/gridMatch.d.ts +26 -0
- package/dist/types/utils/render/color.d.ts +3 -0
- package/dist/types/utils/render/polygon.d.ts +35 -0
- package/dist/types/utils/render/projection.d.ts +13 -0
- package/dist/types/utils/render/tools.d.ts +18 -0
- package/dist/types/utils/solar.d.ts +155 -0
- package/dist/types/utils/utils.d.ts +8 -0
- package/dist/types/utils/visualize.d.ts +111 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +66 -0
- package/readme.md +150 -0
|
@@ -0,0 +1,2777 @@
|
|
|
1
|
+
import { r as registerInstance, g as getElement, h } from './index-CEfm4WRP.js';
|
|
2
|
+
import { f as fetchSolarData, g as getBuildingImages } from './api-By7kNIGr.js';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Copyright 2023 Google LLC
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
// [START visualize_render_rgb]
|
|
20
|
+
/**
|
|
21
|
+
* Renders an RGB GeoTiff image into an HTML canvas.
|
|
22
|
+
*
|
|
23
|
+
* The GeoTiff image must include 3 rasters (bands) which
|
|
24
|
+
* correspond to [Red, Green, Blue] in that order.
|
|
25
|
+
*
|
|
26
|
+
* @param {GeoTiff} rgb GeoTiff with RGB values of the image.
|
|
27
|
+
* @param {GeoTiff} mask Optional mask for transparency, defaults to opaque.
|
|
28
|
+
* @return {HTMLCanvasElement} Canvas element with the rendered image.
|
|
29
|
+
*/
|
|
30
|
+
function renderRGB(rgb, mask, canvas) {
|
|
31
|
+
// Create an HTML canvas to draw the image.
|
|
32
|
+
// https://www.w3schools.com/tags/canvas_createimagedata.asp
|
|
33
|
+
if (!canvas) {
|
|
34
|
+
canvas = document.createElement('canvas');
|
|
35
|
+
}
|
|
36
|
+
// Set the canvas size to the mask size if it's available,
|
|
37
|
+
// otherwise set it to the RGB data layer size.
|
|
38
|
+
canvas.width = mask ? mask.width : rgb.width;
|
|
39
|
+
canvas.height = mask ? mask.height : rgb.height;
|
|
40
|
+
// Since the mask size can be different than the RGB data layer size,
|
|
41
|
+
// we calculate the "delta" between the RGB layer size and the canvas/mask
|
|
42
|
+
// size. For example, if the RGB layer size is the same as the canvas size,
|
|
43
|
+
// the delta is 1. If the RGB layer size is smaller than the canvas size,
|
|
44
|
+
// the delta would be greater than 1.
|
|
45
|
+
// This is used to translate the index from the canvas to the RGB layer.
|
|
46
|
+
const dw = rgb.width / canvas.width;
|
|
47
|
+
const dh = rgb.height / canvas.height;
|
|
48
|
+
// Get the canvas image data buffer.
|
|
49
|
+
const ctx = canvas.getContext('2d');
|
|
50
|
+
const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
51
|
+
// Fill in every pixel in the canvas with the corresponding RGB layer value.
|
|
52
|
+
// Since Javascript doesn't support multidimensional arrays or tensors,
|
|
53
|
+
// everything is stored in flat arrays and we have to keep track of the
|
|
54
|
+
// indices for each row and column ourselves.
|
|
55
|
+
for (let y = 0; y < canvas.height; y++) {
|
|
56
|
+
for (let x = 0; x < canvas.width; x++) {
|
|
57
|
+
// RGB index keeps track of the RGB layer position.
|
|
58
|
+
// This is multiplied by the deltas since it might be a different
|
|
59
|
+
// size than the image size.
|
|
60
|
+
const rgbIdx = Math.floor(y * dh) * rgb.width + Math.floor(x * dw);
|
|
61
|
+
// Mask index keeps track of the mask layer position.
|
|
62
|
+
const maskIdx = y * canvas.width + x;
|
|
63
|
+
// Image index keeps track of the canvas image position.
|
|
64
|
+
// HTML canvas expects a flat array with consecutive RGBA values.
|
|
65
|
+
// Each value in the image buffer must be between 0 and 255.
|
|
66
|
+
// The Alpha value is the transparency of that pixel,
|
|
67
|
+
// if a mask was not provided, we default to 255 which is opaque.
|
|
68
|
+
const imgIdx = y * canvas.width * 4 + x * 4;
|
|
69
|
+
const factor = 0.65;
|
|
70
|
+
img.data[imgIdx + 0] = Math.round(rgb.rasters[0][rgbIdx] * factor); // Red
|
|
71
|
+
img.data[imgIdx + 1] = Math.round(rgb.rasters[1][rgbIdx] * factor); // Green
|
|
72
|
+
img.data[imgIdx + 2] = Math.round(rgb.rasters[2][rgbIdx] * factor); // Blue
|
|
73
|
+
img.data[imgIdx + 3] = mask // Alpha
|
|
74
|
+
? mask.rasters[0][maskIdx] * 255
|
|
75
|
+
: 255;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Draw the image data buffer into the canvas context.
|
|
79
|
+
ctx.putImageData(img, 0, 0);
|
|
80
|
+
return canvas;
|
|
81
|
+
}
|
|
82
|
+
// [END visualize_render_rgb]
|
|
83
|
+
// [START visualize_render_palette]
|
|
84
|
+
/**
|
|
85
|
+
* Renders a single value GeoTiff image into an HTML canvas.
|
|
86
|
+
*
|
|
87
|
+
* The GeoTiff image must include 1 raster (band) which contains
|
|
88
|
+
* the values we want to display.
|
|
89
|
+
*
|
|
90
|
+
* @param {GeoTiff} data GeoTiff with the values of interest.
|
|
91
|
+
* @param {GeoTiff} mask Optional mask for transparency, defaults to opaque.
|
|
92
|
+
* @param {string[]} colors Hex color palette, defaults to ['000000', 'ffffff'].
|
|
93
|
+
* @param {number} min Minimum value of the data range, defaults to 0.
|
|
94
|
+
* @param {number} max Maximum value of the data range, defaults to 1.
|
|
95
|
+
* @param {number} index Raster index for the data, defaults to 0.
|
|
96
|
+
* @return {HTMLCanvasElement} Canvas element with the rendered image.
|
|
97
|
+
*/
|
|
98
|
+
function renderPalette({ data, mask, colors, min, max, index, canvas, }) {
|
|
99
|
+
// First create a palette from a list of hex colors.
|
|
100
|
+
const palette = createPalette(colors ?? ['000000', 'ffffff']);
|
|
101
|
+
// Normalize each value of our raster/band of interest into indices,
|
|
102
|
+
// such that they always map into a value within the palette.
|
|
103
|
+
const indices = data.rasters[index ?? 0]
|
|
104
|
+
.map((x) => normalize(x, max ?? 1, min ?? 0))
|
|
105
|
+
.map((x) => Math.round(x * (palette.length - 1)));
|
|
106
|
+
return renderRGB({
|
|
107
|
+
...data,
|
|
108
|
+
// Map each index into the corresponding RGB values.
|
|
109
|
+
rasters: [
|
|
110
|
+
indices.map((i) => palette[i].r),
|
|
111
|
+
indices.map((i) => palette[i].g),
|
|
112
|
+
indices.map((i) => palette[i].b),
|
|
113
|
+
],
|
|
114
|
+
}, mask, canvas);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Creates an {r, g, b} color palette from a hex list of colors.
|
|
118
|
+
*
|
|
119
|
+
* Each {r, g, b} value is a number between 0 and 255.
|
|
120
|
+
* The created palette is always of size 256, regardless of the number of
|
|
121
|
+
* hex colors passed in. Inbetween values are interpolated.
|
|
122
|
+
*
|
|
123
|
+
* @param {string[]} hexColors List of hex colors for the palette.
|
|
124
|
+
* @return {{r, g, b}[]} RGB values for the color palette.
|
|
125
|
+
*/
|
|
126
|
+
function createPalette(hexColors) {
|
|
127
|
+
// Map each hex color into an RGB value.
|
|
128
|
+
const rgb = hexColors.map(colorToRGB);
|
|
129
|
+
// Create a palette with 256 colors derived from our rgb colors.
|
|
130
|
+
const size = 256;
|
|
131
|
+
const step = (rgb.length - 1) / (size - 1);
|
|
132
|
+
return Array(size)
|
|
133
|
+
.fill(0)
|
|
134
|
+
.map((_, i) => {
|
|
135
|
+
// Get the lower and upper indices for each color.
|
|
136
|
+
const index = i * step;
|
|
137
|
+
const lower = Math.floor(index);
|
|
138
|
+
const upper = Math.ceil(index);
|
|
139
|
+
// Interpolate between the colors to get the shades.
|
|
140
|
+
return {
|
|
141
|
+
r: lerp(rgb[lower].r, rgb[upper].r, index - lower),
|
|
142
|
+
g: lerp(rgb[lower].g, rgb[upper].g, index - lower),
|
|
143
|
+
b: lerp(rgb[lower].b, rgb[upper].b, index - lower),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Convert a hex color into an {r, g, b} color.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} color Hex color like 0099FF or #0099FF.
|
|
151
|
+
* @return {{r, g, b}} RGB values for that color.
|
|
152
|
+
*/
|
|
153
|
+
function colorToRGB(color) {
|
|
154
|
+
const hex = color.startsWith('#') ? color.slice(1) : color;
|
|
155
|
+
return {
|
|
156
|
+
r: parseInt(hex.substring(0, 2), 16),
|
|
157
|
+
g: parseInt(hex.substring(2, 4), 16),
|
|
158
|
+
b: parseInt(hex.substring(4, 6), 16),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Normalizes a number to a given data range.
|
|
163
|
+
*
|
|
164
|
+
* @param {number} x Value of interest.
|
|
165
|
+
* @param {number} max Maximum value in data range, defaults to 1.
|
|
166
|
+
* @param {number} min Minimum value in data range, defaults to 0.
|
|
167
|
+
* @return {number} Normalized value.
|
|
168
|
+
*/
|
|
169
|
+
function normalize(x, max = 1, min = 0) {
|
|
170
|
+
const y = (x - min) / (max - min);
|
|
171
|
+
return clamp(y, 0, 1);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Calculates the linear interpolation for a value within a range.
|
|
175
|
+
*
|
|
176
|
+
* @param {number} x Lower value in the range, when `t` is 0.
|
|
177
|
+
* @param {number} y Upper value in the range, when `t` is 1.
|
|
178
|
+
* @param {number} t "Time" between 0 and 1.
|
|
179
|
+
* @return {number} Inbetween value for that "time".
|
|
180
|
+
*/
|
|
181
|
+
function lerp(x, y, t) {
|
|
182
|
+
return x + t * (y - x);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Clamps a value to always be within a range.
|
|
186
|
+
*
|
|
187
|
+
* @param {number} x Value to clamp.
|
|
188
|
+
* @param {number} min Minimum value in the range.
|
|
189
|
+
* @param {number} max Maximum value in the range.
|
|
190
|
+
* @return {number} Clamped value.
|
|
191
|
+
*/
|
|
192
|
+
function clamp(x, min, max) {
|
|
193
|
+
return Math.min(Math.max(x, min), max);
|
|
194
|
+
}
|
|
195
|
+
// [END visualize_render_palette]
|
|
196
|
+
function rgbToColor({ r, g, b }) {
|
|
197
|
+
const f = (x) => {
|
|
198
|
+
const hex = Math.round(x).toString(16);
|
|
199
|
+
return hex.length == 1 ? `0${hex}` : hex;
|
|
200
|
+
};
|
|
201
|
+
return `#${f(r)}${f(g)}${f(b)}`;
|
|
202
|
+
}
|
|
203
|
+
function renderAzimuth(azimuth, ctx) {
|
|
204
|
+
if (azimuth === undefined) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
ctx.save();
|
|
208
|
+
ctx.moveTo(0, 0);
|
|
209
|
+
ctx.rotate((azimuth - 90) * Math.PI / 180);
|
|
210
|
+
ctx.translate(ctx.canvas.width - 100, 100);
|
|
211
|
+
// Set arrow style
|
|
212
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
213
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
214
|
+
ctx.lineWidth = 2;
|
|
215
|
+
ctx.moveTo(-50, 0);
|
|
216
|
+
ctx.lineTo(50, 0);
|
|
217
|
+
ctx.lineTo(40, -10);
|
|
218
|
+
ctx.moveTo(50, 0);
|
|
219
|
+
ctx.lineTo(40, 10);
|
|
220
|
+
ctx.stroke();
|
|
221
|
+
ctx.moveTo(0, 0);
|
|
222
|
+
ctx.beginPath();
|
|
223
|
+
ctx.arc(0, 0, 50, 0, Math.PI * 2);
|
|
224
|
+
ctx.stroke();
|
|
225
|
+
// Arrow parameters
|
|
226
|
+
// const arrowLength = 100;
|
|
227
|
+
// const arrowHeadLength = 15;
|
|
228
|
+
// const arrowHeadAngle = Math.PI / 6; // 30 degrees
|
|
229
|
+
// // Convert azimuth to radians (subtract 90 degrees to align with standard compass)
|
|
230
|
+
// const angle = (azimuth - 90) * Math.PI / 180;
|
|
231
|
+
// // Calculate arrow end point
|
|
232
|
+
// const endX = arrowLength * Math.cos(angle);
|
|
233
|
+
// const endY = arrowLength * Math.sin(angle);
|
|
234
|
+
// // Draw main arrow line
|
|
235
|
+
// ctx.beginPath();
|
|
236
|
+
// ctx.moveTo(0, 0);
|
|
237
|
+
// ctx.lineTo(endX, endY);
|
|
238
|
+
// ctx.stroke();
|
|
239
|
+
// // Calculate arrowhead points
|
|
240
|
+
// const arrowHeadAngle1 = angle + Math.PI - arrowHeadAngle;
|
|
241
|
+
// const arrowHeadAngle2 = angle + Math.PI + arrowHeadAngle;
|
|
242
|
+
// const arrowHead1X = endX + arrowHeadLength * Math.cos(arrowHeadAngle1);
|
|
243
|
+
// const arrowHead1Y = endY + arrowHeadLength * Math.sin(arrowHeadAngle1);
|
|
244
|
+
// const arrowHead2X = endX + arrowHeadLength * Math.cos(arrowHeadAngle2);
|
|
245
|
+
// const arrowHead2Y = endY + arrowHeadLength * Math.sin(arrowHeadAngle2);
|
|
246
|
+
// // Draw arrowhead
|
|
247
|
+
// ctx.beginPath();
|
|
248
|
+
// ctx.moveTo(endX, endY);
|
|
249
|
+
// ctx.lineTo(arrowHead1X, arrowHead1Y);
|
|
250
|
+
// ctx.lineTo(arrowHead2X, arrowHead2Y);
|
|
251
|
+
// ctx.closePath();
|
|
252
|
+
// ctx.fill();
|
|
253
|
+
// ctx.translate(ctx.canvas.width/2, 50);
|
|
254
|
+
ctx.restore();
|
|
255
|
+
console.log("renderAzimuth", azimuth);
|
|
256
|
+
}
|
|
257
|
+
function renderSolarPanel(solarPanel, ctx, panelWidth, panelHeight, azimuthAngle, pitchAngle) {
|
|
258
|
+
const { x, y } = solarPanel.pixelPosition;
|
|
259
|
+
let width = panelWidth;
|
|
260
|
+
let height = panelHeight * Math.cos(pitchAngle * Math.PI / 180);
|
|
261
|
+
if (solarPanel.horizontal) {
|
|
262
|
+
width = panelHeight;
|
|
263
|
+
height = panelWidth * Math.cos(pitchAngle * Math.PI / 180);
|
|
264
|
+
}
|
|
265
|
+
// Save the current context state
|
|
266
|
+
ctx.save();
|
|
267
|
+
// Translate to the panel's center point
|
|
268
|
+
ctx.translate(x, y);
|
|
269
|
+
// Rotate by the azimuth angle (convert to radians)
|
|
270
|
+
ctx.rotate((azimuthAngle * Math.PI) / 180);
|
|
271
|
+
// Draw panel rectangle centered at origin (since we translated)
|
|
272
|
+
ctx.fillStyle = 'rgba(115, 143, 255, 1)';
|
|
273
|
+
ctx.fillRect(-width / 2, -height / 2, width, height);
|
|
274
|
+
// Draw panel border
|
|
275
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';
|
|
276
|
+
ctx.lineWidth = 1;
|
|
277
|
+
ctx.strokeRect(-width / 2, -height / 2, width, height);
|
|
278
|
+
// Restore the context state
|
|
279
|
+
ctx.restore();
|
|
280
|
+
}
|
|
281
|
+
function renderPanels({ canvas, panelConfig, bounds, zoom, roofSegments }) {
|
|
282
|
+
if (!canvas) {
|
|
283
|
+
canvas = document.createElement('canvas');
|
|
284
|
+
}
|
|
285
|
+
const ctx = canvas.getContext("2d");
|
|
286
|
+
// Calculate pre zoomed dimensions the canvas is already zoomed
|
|
287
|
+
const preZoomedWidth = Math.floor(canvas.width / zoom);
|
|
288
|
+
const preZoomedHeight = Math.floor(canvas.height / zoom);
|
|
289
|
+
const startX = Math.floor((preZoomedWidth - canvas.width) / 2);
|
|
290
|
+
const startY = Math.floor((preZoomedHeight - canvas.height) / 2);
|
|
291
|
+
// Calculate the scale factors to convert lat/lng to pixel coordinates
|
|
292
|
+
const latToPixel = (lat) => {
|
|
293
|
+
return preZoomedHeight * (1 - (lat - bounds.south) / (bounds.north - bounds.south)) - startY;
|
|
294
|
+
};
|
|
295
|
+
const lngToPixel = (lng) => {
|
|
296
|
+
return preZoomedWidth * (lng - bounds.west) / (bounds.east - bounds.west) - startX;
|
|
297
|
+
};
|
|
298
|
+
// Draw each panel
|
|
299
|
+
for (const panel of panelConfig.panels) {
|
|
300
|
+
// Convert panel center coordinates to pixel coordinates
|
|
301
|
+
const centerX = lngToPixel(panel.center.longitude);
|
|
302
|
+
const centerY = latToPixel(panel.center.latitude);
|
|
303
|
+
// Calculate panel dimensions (assuming standard panel size)
|
|
304
|
+
const panelWidth = panel.orientation === 'LANDSCAPE' ? 19 : 10; // pixels
|
|
305
|
+
const panelHeight = panel.orientation === 'LANDSCAPE' ? 10 : 19;
|
|
306
|
+
// Get the roof segment's azimuth angle for this panel
|
|
307
|
+
const roofSegment = roofSegments[panel.segmentIndex];
|
|
308
|
+
const azimuthAngle = roofSegment ? roofSegment.azimuthDegrees : 0;
|
|
309
|
+
// Save the current context state
|
|
310
|
+
ctx.save();
|
|
311
|
+
// Translate to the panel's center point
|
|
312
|
+
ctx.translate(centerX, centerY);
|
|
313
|
+
// Rotate by the azimuth angle (convert to radians)
|
|
314
|
+
ctx.rotate((azimuthAngle * Math.PI) / 180);
|
|
315
|
+
// Draw panel rectangle centered at origin (since we translated)
|
|
316
|
+
ctx.fillStyle = 'rgba(115, 143, 255, 1)';
|
|
317
|
+
ctx.fillRect(-panelWidth / 2, -panelHeight / 2, panelWidth, panelHeight);
|
|
318
|
+
// Draw panel border
|
|
319
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';
|
|
320
|
+
ctx.lineWidth = 1;
|
|
321
|
+
ctx.strokeRect(-panelWidth / 2, -panelHeight / 2, panelWidth, panelHeight);
|
|
322
|
+
// Restore the context state
|
|
323
|
+
ctx.restore();
|
|
324
|
+
}
|
|
325
|
+
return canvas;
|
|
326
|
+
}
|
|
327
|
+
function renderCombinedWithZoom({ rgb, zoom = 1, canvas, }) {
|
|
328
|
+
// First render the RGB image
|
|
329
|
+
const rgbCanvas = renderRGB(rgb, undefined, canvas);
|
|
330
|
+
const ctx = rgbCanvas.getContext("2d");
|
|
331
|
+
const imgData = ctx.getImageData(0, 0, rgbCanvas.width, rgbCanvas.height);
|
|
332
|
+
// Calculate the center portion to show based on zoom
|
|
333
|
+
const width = rgbCanvas.width;
|
|
334
|
+
const height = rgbCanvas.height;
|
|
335
|
+
const zoomedWidth = Math.floor(width * zoom);
|
|
336
|
+
const zoomedHeight = Math.floor(height * zoom);
|
|
337
|
+
const startX = Math.floor((width - zoomedWidth) / 2);
|
|
338
|
+
const startY = Math.floor((height - zoomedHeight) / 2);
|
|
339
|
+
// Create a new image data for the zoomed portion
|
|
340
|
+
const zoomedImgData = new ImageData(zoomedWidth, zoomedHeight);
|
|
341
|
+
// Copy RGB data and apply palette where mask is non-zero
|
|
342
|
+
for (let y = 0; y < height; y++) {
|
|
343
|
+
for (let x = 0; x < width; x++) {
|
|
344
|
+
const i = (y * width + x) * 4;
|
|
345
|
+
if (x >= startX && x < startX + zoomedWidth &&
|
|
346
|
+
y >= startY && y < startY + zoomedHeight) {
|
|
347
|
+
// Calculate position in zoomed image
|
|
348
|
+
const zoomedX = x - startX;
|
|
349
|
+
const zoomedY = y - startY;
|
|
350
|
+
const zoomedI = (zoomedY * zoomedWidth + zoomedX) * 4;
|
|
351
|
+
zoomedImgData.data[zoomedI] = imgData.data[i];
|
|
352
|
+
zoomedImgData.data[zoomedI + 1] = imgData.data[i + 1];
|
|
353
|
+
zoomedImgData.data[zoomedI + 2] = imgData.data[i + 2];
|
|
354
|
+
zoomedImgData.data[zoomedI + 3] = 255; // Alpha channel
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Resize canvas to match zoomed dimensions
|
|
359
|
+
rgbCanvas.width = zoomedWidth;
|
|
360
|
+
rgbCanvas.height = zoomedHeight;
|
|
361
|
+
// Put the zoomed image data back to the canvas
|
|
362
|
+
ctx.putImageData(zoomedImgData, 0, 0);
|
|
363
|
+
return rgbCanvas;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const OPEN_POLYGON_COLOR = "red"; //"#ffffff";
|
|
367
|
+
const CLOSED_POLYGON_COLOR = "#fcba03";
|
|
368
|
+
const DOTTED_LINE_COLOR = "rgba(0, 0, 0, 0.5)";
|
|
369
|
+
const ROW_SPACING = 1;
|
|
370
|
+
const COLUMN_SPACING = 1;
|
|
371
|
+
const BORDER_INSET = 0.2;
|
|
372
|
+
|
|
373
|
+
function projectPointPerpendicularToLine({ x, y, lastPoint, secondLastPoint, }) {
|
|
374
|
+
// Calculate the direction vector of the last line
|
|
375
|
+
const dx = lastPoint.x - secondLastPoint.x;
|
|
376
|
+
const dy = lastPoint.y - secondLastPoint.y;
|
|
377
|
+
// Calculate perpendicular vector (rotate 90 degrees)
|
|
378
|
+
const perpDx = -dy;
|
|
379
|
+
const perpDy = dx;
|
|
380
|
+
// Normalize the perpendicular vector
|
|
381
|
+
const perpLength = Math.sqrt(perpDx * perpDx + perpDy * perpDy);
|
|
382
|
+
const normalizedPerpDx = perpDx / perpLength;
|
|
383
|
+
const normalizedPerpDy = perpDy / perpLength;
|
|
384
|
+
// Project the mouse point onto the perpendicular line
|
|
385
|
+
const vectorToMouse = {
|
|
386
|
+
x: x - lastPoint.x,
|
|
387
|
+
y: y - lastPoint.y,
|
|
388
|
+
};
|
|
389
|
+
// Calculate the projection length (dot product)
|
|
390
|
+
const projectionLength = vectorToMouse.x * normalizedPerpDx +
|
|
391
|
+
vectorToMouse.y * normalizedPerpDy;
|
|
392
|
+
// Calculate the new point position using the projected length
|
|
393
|
+
x = lastPoint.x + normalizedPerpDx * projectionLength;
|
|
394
|
+
y = lastPoint.y + normalizedPerpDy * projectionLength;
|
|
395
|
+
return { x, y };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function drawPolygon({ polygonCtx, polygonCanvas, polygon, shiftKeyPressed, mousePoint, strokeColor, fillColor }) {
|
|
399
|
+
if (!polygonCtx || !polygonCanvas)
|
|
400
|
+
return;
|
|
401
|
+
// Draw the polygon
|
|
402
|
+
polygonCtx.beginPath();
|
|
403
|
+
polygonCtx.strokeStyle = strokeColor;
|
|
404
|
+
polygonCtx.lineWidth = 2;
|
|
405
|
+
polygon.points.forEach((point, index) => {
|
|
406
|
+
if (index === 0) {
|
|
407
|
+
polygonCtx.moveTo(point.x, point.y);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
polygonCtx.lineTo(point.x, point.y);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
if (polygon.closed) {
|
|
414
|
+
polygonCtx.lineTo(polygon.points[0].x, polygon.points[0].y);
|
|
415
|
+
if (fillColor) {
|
|
416
|
+
polygonCtx.fillStyle = fillColor;
|
|
417
|
+
polygonCtx.fill();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (polygon.points.length > 0) {
|
|
421
|
+
polygonCtx.stroke();
|
|
422
|
+
}
|
|
423
|
+
if (polygon.points.length > 1 && !polygon.closed && shiftKeyPressed) {
|
|
424
|
+
const projectedPoint = projectPointPerpendicularToLine({
|
|
425
|
+
x: mousePoint.x,
|
|
426
|
+
y: mousePoint.y,
|
|
427
|
+
lastPoint: polygon.points[polygon.points.length - 1],
|
|
428
|
+
secondLastPoint: polygon.points[polygon.points.length - 2],
|
|
429
|
+
});
|
|
430
|
+
drawPerpendicularDottedLine({
|
|
431
|
+
ctx: polygonCtx,
|
|
432
|
+
startPoint: projectedPoint,
|
|
433
|
+
endPoint: polygon.points[polygon.points.length - 1],
|
|
434
|
+
strokeStyle: DOTTED_LINE_COLOR,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (shiftKeyPressed && polygon.points.length > 1 && !polygon.closed) {
|
|
438
|
+
const projectedPoint = projectPointPerpendicularToLine({
|
|
439
|
+
x: mousePoint.x,
|
|
440
|
+
y: mousePoint.y,
|
|
441
|
+
lastPoint: polygon.points[polygon.points.length - 1],
|
|
442
|
+
secondLastPoint: polygon.points[polygon.points.length - 2],
|
|
443
|
+
});
|
|
444
|
+
drawCircle({
|
|
445
|
+
ctx: polygonCtx,
|
|
446
|
+
x: projectedPoint.x,
|
|
447
|
+
y: projectedPoint.y,
|
|
448
|
+
radius: 5,
|
|
449
|
+
strokeStyle: DOTTED_LINE_COLOR,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// Draw circles around each point
|
|
453
|
+
polygon.points.forEach((point) => {
|
|
454
|
+
polygonCtx.beginPath();
|
|
455
|
+
const strokeStyle = strokeColor;
|
|
456
|
+
const distance = Math.sqrt(Math.pow(point.x - mousePoint.x, 2) + Math.pow(point.y - mousePoint.y, 2));
|
|
457
|
+
// Highlight the point if it's being hovered or dragged
|
|
458
|
+
if (distance < 10) {
|
|
459
|
+
drawCircle({
|
|
460
|
+
ctx: polygonCtx,
|
|
461
|
+
x: point.x,
|
|
462
|
+
y: point.y,
|
|
463
|
+
strokeStyle,
|
|
464
|
+
radius: 8,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
drawCircle({
|
|
469
|
+
ctx: polygonCtx,
|
|
470
|
+
x: point.x,
|
|
471
|
+
y: point.y,
|
|
472
|
+
strokeStyle,
|
|
473
|
+
radius: 5,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
polygonCtx.stroke();
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function drawCircle({ ctx, x, y, radius, strokeStyle = "black", lineWidth = 2 }) {
|
|
480
|
+
ctx.beginPath();
|
|
481
|
+
ctx.strokeStyle = strokeStyle;
|
|
482
|
+
ctx.lineWidth = lineWidth;
|
|
483
|
+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
|
484
|
+
ctx.stroke();
|
|
485
|
+
}
|
|
486
|
+
function drawPerpendicularDottedLine({ ctx, startPoint, endPoint, dashPattern = [5, 5], strokeStyle = "black", lineWidth = 2 }) {
|
|
487
|
+
// Set up the line style
|
|
488
|
+
ctx.beginPath();
|
|
489
|
+
ctx.strokeStyle = strokeStyle;
|
|
490
|
+
ctx.lineWidth = lineWidth;
|
|
491
|
+
ctx.setLineDash(dashPattern);
|
|
492
|
+
// Draw the dotted line
|
|
493
|
+
ctx.moveTo(startPoint.x, startPoint.y);
|
|
494
|
+
ctx.lineTo(endPoint.x, endPoint.y);
|
|
495
|
+
ctx.stroke();
|
|
496
|
+
// Reset the line dash
|
|
497
|
+
ctx.setLineDash([]);
|
|
498
|
+
}
|
|
499
|
+
function isPointOnBorder(point, polygon) {
|
|
500
|
+
if (!polygon.closed || polygon.points.length < 3)
|
|
501
|
+
return false;
|
|
502
|
+
const x = point.x;
|
|
503
|
+
const y = point.y;
|
|
504
|
+
// For each edge of the polygon
|
|
505
|
+
for (let i = 0; i < polygon.points.length; i++) {
|
|
506
|
+
const p1 = polygon.points[i];
|
|
507
|
+
const p2 = polygon.points[(i + 1) % polygon.points.length];
|
|
508
|
+
// calculate the distance from point p1 to p2
|
|
509
|
+
const distance = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
510
|
+
const distanceToPoint = Math.sqrt(Math.pow(p1.x - x, 2) + Math.pow(p1.y - y, 2));
|
|
511
|
+
const distanceFromPoint = Math.sqrt(Math.pow(p2.x - x, 2) + Math.pow(p2.y - y, 2));
|
|
512
|
+
if (distance === distanceToPoint + distanceFromPoint) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
function isPointInPolygon(point, polygon) {
|
|
519
|
+
if (!polygon.closed || polygon.points.length < 3)
|
|
520
|
+
return false;
|
|
521
|
+
const num_vertices = polygon.points.length;
|
|
522
|
+
const x = point.x;
|
|
523
|
+
const y = point.y;
|
|
524
|
+
let inside = false;
|
|
525
|
+
let p1 = polygon.points[0];
|
|
526
|
+
let p2;
|
|
527
|
+
for (let i = 1; i <= num_vertices; i++) {
|
|
528
|
+
p2 = polygon.points[i % num_vertices];
|
|
529
|
+
if (y > Math.min(p1.y, p2.y)) {
|
|
530
|
+
if (y <= Math.max(p1.y, p2.y)) {
|
|
531
|
+
if (x <= Math.max(p1.x, p2.x)) {
|
|
532
|
+
const x_intersection = ((y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x;
|
|
533
|
+
if (p1.x === p2.x || x <= x_intersection) {
|
|
534
|
+
inside = !inside;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
p1 = p2;
|
|
540
|
+
}
|
|
541
|
+
return inside;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const polygonKeyboardCombinations = [
|
|
545
|
+
{
|
|
546
|
+
key: "shift",
|
|
547
|
+
description: "add one point on perpendicular line",
|
|
548
|
+
},
|
|
549
|
+
// {
|
|
550
|
+
// key: "alt",
|
|
551
|
+
// description: "add parallel line",
|
|
552
|
+
// },
|
|
553
|
+
];
|
|
554
|
+
const moveTool = {
|
|
555
|
+
name: "move",
|
|
556
|
+
ariaLabel: "Move",
|
|
557
|
+
icon: "move",
|
|
558
|
+
cursor: "move",
|
|
559
|
+
keyboardShortcut: "m",
|
|
560
|
+
};
|
|
561
|
+
const roofTool = {
|
|
562
|
+
name: "roof",
|
|
563
|
+
ariaLabel: "Roof Area",
|
|
564
|
+
icon: "house",
|
|
565
|
+
cursor: "crosshair",
|
|
566
|
+
keyboardShortcut: "r",
|
|
567
|
+
keyboardCombinations: polygonKeyboardCombinations,
|
|
568
|
+
};
|
|
569
|
+
const obstructionTool = {
|
|
570
|
+
name: "obstruction",
|
|
571
|
+
ariaLabel: "Obstruction",
|
|
572
|
+
icon: "octagon-minus",
|
|
573
|
+
cursor: "crosshair",
|
|
574
|
+
keyboardShortcut: "o",
|
|
575
|
+
keyboardCombinations: polygonKeyboardCombinations,
|
|
576
|
+
};
|
|
577
|
+
const deleteTool = {
|
|
578
|
+
name: "delete",
|
|
579
|
+
ariaLabel: "Delete",
|
|
580
|
+
icon: "eraser",
|
|
581
|
+
cursor: "default",
|
|
582
|
+
keyboardShortcut: "d",
|
|
583
|
+
};
|
|
584
|
+
const tools = [
|
|
585
|
+
moveTool,
|
|
586
|
+
roofTool,
|
|
587
|
+
obstructionTool,
|
|
588
|
+
deleteTool,
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
const DEFAULT_SOLAR_EXPERT_CONFIG = {
|
|
592
|
+
openRoofColor: "#d6eeff",
|
|
593
|
+
closedRoofColor: "#ffffff",
|
|
594
|
+
openObstructionColor: "#f57242",
|
|
595
|
+
closedObstructionColor: "rgba(255, 132, 107, 0.8)",
|
|
596
|
+
roofPolygonFillColor: "rgba(92, 187, 255, 0.5)",
|
|
597
|
+
obstructionPolygonFillColor: "rgba(255, 132, 107, 0.5)",
|
|
598
|
+
roofPolygonHoverFillColor: "rgba(92, 187, 255, 0.8)",
|
|
599
|
+
obstructionPolygonHoverFillColor: "rgba(255, 132, 107, 0.8)",
|
|
600
|
+
roofPolygonSelectedFillColor: "rgba(92, 187, 255, 0.7)",
|
|
601
|
+
obstructionPolygonSelectedFillColor: "rgba(255, 132, 107, 0.7)",
|
|
602
|
+
};
|
|
603
|
+
const DEFAULT_SOLAR_PANEL_TYPE = {
|
|
604
|
+
widthMeters: 1.134,
|
|
605
|
+
heightMeters: 1.762,
|
|
606
|
+
kWattPeak: 600,
|
|
607
|
+
efficiency: 0.2245,
|
|
608
|
+
price: 77.68,
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
function getFillColor(selectedPolygon, hoveredPolygon, _id, polygon, config) {
|
|
612
|
+
const isHovered = hoveredPolygon?.type === polygon.type && hoveredPolygon?._id === _id;
|
|
613
|
+
if (isHovered) {
|
|
614
|
+
return polygon.type === "roof" ? config.roofPolygonHoverFillColor : config.obstructionPolygonHoverFillColor;
|
|
615
|
+
}
|
|
616
|
+
const isSelected = selectedPolygon?.type === polygon.type && selectedPolygon?._id === _id;
|
|
617
|
+
if (isSelected) {
|
|
618
|
+
return polygon.type === "roof" ? config.roofPolygonSelectedFillColor : config.obstructionPolygonSelectedFillColor;
|
|
619
|
+
}
|
|
620
|
+
if (polygon.type === "roof") {
|
|
621
|
+
return polygon.closed ? config.roofPolygonFillColor : undefined;
|
|
622
|
+
}
|
|
623
|
+
return polygon.closed ? config.obstructionPolygonFillColor : undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
var max = 'ffffffff-ffff-ffff-ffff-ffffffffffff';
|
|
627
|
+
|
|
628
|
+
var nil = '00000000-0000-0000-0000-000000000000';
|
|
629
|
+
|
|
630
|
+
var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
|
|
631
|
+
|
|
632
|
+
function validate(uuid) {
|
|
633
|
+
return typeof uuid === 'string' && REGEX.test(uuid);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parse(uuid) {
|
|
637
|
+
if (!validate(uuid)) {
|
|
638
|
+
throw TypeError('Invalid UUID');
|
|
639
|
+
}
|
|
640
|
+
var v;
|
|
641
|
+
var arr = new Uint8Array(16);
|
|
642
|
+
|
|
643
|
+
// Parse ########-....-....-....-............
|
|
644
|
+
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
|
|
645
|
+
arr[1] = v >>> 16 & 0xff;
|
|
646
|
+
arr[2] = v >>> 8 & 0xff;
|
|
647
|
+
arr[3] = v & 0xff;
|
|
648
|
+
|
|
649
|
+
// Parse ........-####-....-....-............
|
|
650
|
+
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
|
|
651
|
+
arr[5] = v & 0xff;
|
|
652
|
+
|
|
653
|
+
// Parse ........-....-####-....-............
|
|
654
|
+
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
|
|
655
|
+
arr[7] = v & 0xff;
|
|
656
|
+
|
|
657
|
+
// Parse ........-....-....-####-............
|
|
658
|
+
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
|
|
659
|
+
arr[9] = v & 0xff;
|
|
660
|
+
|
|
661
|
+
// Parse ........-....-....-....-############
|
|
662
|
+
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
|
663
|
+
arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff;
|
|
664
|
+
arr[11] = v / 0x100000000 & 0xff;
|
|
665
|
+
arr[12] = v >>> 24 & 0xff;
|
|
666
|
+
arr[13] = v >>> 16 & 0xff;
|
|
667
|
+
arr[14] = v >>> 8 & 0xff;
|
|
668
|
+
arr[15] = v & 0xff;
|
|
669
|
+
return arr;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Convert array of 16 byte values to UUID string format of the form:
|
|
674
|
+
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
|
675
|
+
*/
|
|
676
|
+
var byteToHex = [];
|
|
677
|
+
for (var i = 0; i < 256; ++i) {
|
|
678
|
+
byteToHex.push((i + 0x100).toString(16).slice(1));
|
|
679
|
+
}
|
|
680
|
+
function unsafeStringify(arr, offset = 0) {
|
|
681
|
+
// Note: Be careful editing this code! It's been tuned for performance
|
|
682
|
+
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
|
|
683
|
+
//
|
|
684
|
+
// Note to future-self: No, you can't remove the `toLowerCase()` call.
|
|
685
|
+
// REF: https://github.com/uuidjs/uuid/pull/677#issuecomment-1757351351
|
|
686
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
687
|
+
}
|
|
688
|
+
function stringify(arr, offset = 0) {
|
|
689
|
+
var uuid = unsafeStringify(arr, offset);
|
|
690
|
+
// Consistency check for valid UUID. If this throws, it's likely due to one
|
|
691
|
+
// of the following:
|
|
692
|
+
// - One or more input array values don't map to a hex octet (leading to
|
|
693
|
+
// "undefined" in the uuid)
|
|
694
|
+
// - Invalid input values for the RFC `version` or `variant` fields
|
|
695
|
+
if (!validate(uuid)) {
|
|
696
|
+
throw TypeError('Stringified UUID is invalid');
|
|
697
|
+
}
|
|
698
|
+
return uuid;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Unique ID creation requires a high quality random # generator. In the browser we therefore
|
|
702
|
+
// require the crypto API and do not support built-in fallback to lower quality random number
|
|
703
|
+
// generators (like Math.random()).
|
|
704
|
+
|
|
705
|
+
var getRandomValues;
|
|
706
|
+
var rnds8 = new Uint8Array(16);
|
|
707
|
+
function rng() {
|
|
708
|
+
// lazy load so that environments that need to polyfill have a chance to do so
|
|
709
|
+
if (!getRandomValues) {
|
|
710
|
+
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation.
|
|
711
|
+
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto);
|
|
712
|
+
if (!getRandomValues) {
|
|
713
|
+
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return getRandomValues(rnds8);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// **`v1()` - Generate time-based UUID**
|
|
720
|
+
//
|
|
721
|
+
// Inspired by https://github.com/LiosK/UUID.js
|
|
722
|
+
// and http://docs.python.org/library/uuid.html
|
|
723
|
+
|
|
724
|
+
var _nodeId;
|
|
725
|
+
var _clockseq;
|
|
726
|
+
|
|
727
|
+
// Previous uuid creation time
|
|
728
|
+
var _lastMSecs = 0;
|
|
729
|
+
var _lastNSecs = 0;
|
|
730
|
+
|
|
731
|
+
// See https://github.com/uuidjs/uuid for API details
|
|
732
|
+
function v1(options, buf, offset) {
|
|
733
|
+
var i = buf && offset || 0;
|
|
734
|
+
var b = buf || new Array(16);
|
|
735
|
+
options = options || {};
|
|
736
|
+
var node = options.node;
|
|
737
|
+
var clockseq = options.clockseq;
|
|
738
|
+
|
|
739
|
+
// v1 only: Use cached `node` and `clockseq` values
|
|
740
|
+
if (!options._v6) {
|
|
741
|
+
if (!node) {
|
|
742
|
+
node = _nodeId;
|
|
743
|
+
}
|
|
744
|
+
if (clockseq == null) {
|
|
745
|
+
clockseq = _clockseq;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Handle cases where we need entropy. We do this lazily to minimize issues
|
|
750
|
+
// related to insufficient system entropy. See #189
|
|
751
|
+
if (node == null || clockseq == null) {
|
|
752
|
+
var seedBytes = options.random || (options.rng || rng)();
|
|
753
|
+
|
|
754
|
+
// Randomize node
|
|
755
|
+
if (node == null) {
|
|
756
|
+
node = [seedBytes[0], seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]];
|
|
757
|
+
|
|
758
|
+
// v1 only: cache node value for reuse
|
|
759
|
+
if (!_nodeId && !options._v6) {
|
|
760
|
+
// per RFC4122 4.5: Set MAC multicast bit (v1 only)
|
|
761
|
+
node[0] |= 0x01; // Set multicast bit
|
|
762
|
+
|
|
763
|
+
_nodeId = node;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Randomize clockseq
|
|
768
|
+
if (clockseq == null) {
|
|
769
|
+
// Per 4.2.2, randomize (14 bit) clockseq
|
|
770
|
+
clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
|
|
771
|
+
if (_clockseq === undefined && !options._v6) {
|
|
772
|
+
_clockseq = clockseq;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// v1 & v6 timestamps are 100 nano-second units since the Gregorian epoch,
|
|
778
|
+
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so time is
|
|
779
|
+
// handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
|
780
|
+
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
|
781
|
+
var msecs = options.msecs !== undefined ? options.msecs : Date.now();
|
|
782
|
+
|
|
783
|
+
// Per 4.2.1.2, use count of uuid's generated during the current clock
|
|
784
|
+
// cycle to simulate higher resolution clock
|
|
785
|
+
var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
|
|
786
|
+
|
|
787
|
+
// Time since last uuid creation (in msecs)
|
|
788
|
+
var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000;
|
|
789
|
+
|
|
790
|
+
// Per 4.2.1.2, Bump clockseq on clock regression
|
|
791
|
+
if (dt < 0 && options.clockseq === undefined) {
|
|
792
|
+
clockseq = clockseq + 1 & 0x3fff;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
|
796
|
+
// time interval
|
|
797
|
+
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
|
|
798
|
+
nsecs = 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Per 4.2.1.2 Throw error if too many uuids are requested
|
|
802
|
+
if (nsecs >= 10000) {
|
|
803
|
+
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
|
|
804
|
+
}
|
|
805
|
+
_lastMSecs = msecs;
|
|
806
|
+
_lastNSecs = nsecs;
|
|
807
|
+
_clockseq = clockseq;
|
|
808
|
+
|
|
809
|
+
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
|
810
|
+
msecs += 12219292800000;
|
|
811
|
+
|
|
812
|
+
// `time_low`
|
|
813
|
+
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
|
814
|
+
b[i++] = tl >>> 24 & 0xff;
|
|
815
|
+
b[i++] = tl >>> 16 & 0xff;
|
|
816
|
+
b[i++] = tl >>> 8 & 0xff;
|
|
817
|
+
b[i++] = tl & 0xff;
|
|
818
|
+
|
|
819
|
+
// `time_mid`
|
|
820
|
+
var tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
|
|
821
|
+
b[i++] = tmh >>> 8 & 0xff;
|
|
822
|
+
b[i++] = tmh & 0xff;
|
|
823
|
+
|
|
824
|
+
// `time_high_and_version`
|
|
825
|
+
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
|
|
826
|
+
b[i++] = tmh >>> 16 & 0xff;
|
|
827
|
+
|
|
828
|
+
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
|
829
|
+
b[i++] = clockseq >>> 8 | 0x80;
|
|
830
|
+
|
|
831
|
+
// `clock_seq_low`
|
|
832
|
+
b[i++] = clockseq & 0xff;
|
|
833
|
+
|
|
834
|
+
// `node`
|
|
835
|
+
for (var n = 0; n < 6; ++n) {
|
|
836
|
+
b[i + n] = node[n];
|
|
837
|
+
}
|
|
838
|
+
return buf || unsafeStringify(b);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Convert a v1 UUID to a v6 UUID
|
|
843
|
+
*
|
|
844
|
+
* @param {string|Uint8Array} uuid - The v1 UUID to convert to v6
|
|
845
|
+
* @returns {string|Uint8Array} The v6 UUID as the same type as the `uuid` arg
|
|
846
|
+
* (string or Uint8Array)
|
|
847
|
+
*/
|
|
848
|
+
function v1ToV6(uuid) {
|
|
849
|
+
var v1Bytes = typeof uuid === 'string' ? parse(uuid) : uuid;
|
|
850
|
+
var v6Bytes = _v1ToV6(v1Bytes);
|
|
851
|
+
return typeof uuid === 'string' ? unsafeStringify(v6Bytes) : v6Bytes;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Do the field transformation needed for v1 -> v6
|
|
855
|
+
function _v1ToV6(v1Bytes, randomize = false) {
|
|
856
|
+
return Uint8Array.of((v1Bytes[6] & 0x0f) << 4 | v1Bytes[7] >> 4 & 0x0f, (v1Bytes[7] & 0x0f) << 4 | (v1Bytes[4] & 0xf0) >> 4, (v1Bytes[4] & 0x0f) << 4 | (v1Bytes[5] & 0xf0) >> 4, (v1Bytes[5] & 0x0f) << 4 | (v1Bytes[0] & 0xf0) >> 4, (v1Bytes[0] & 0x0f) << 4 | (v1Bytes[1] & 0xf0) >> 4, (v1Bytes[1] & 0x0f) << 4 | (v1Bytes[2] & 0xf0) >> 4, 0x60 | v1Bytes[2] & 0x0f, v1Bytes[3], v1Bytes[8], v1Bytes[9], v1Bytes[10], v1Bytes[11], v1Bytes[12], v1Bytes[13], v1Bytes[14], v1Bytes[15]);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function stringToBytes(str) {
|
|
860
|
+
str = unescape(encodeURIComponent(str)); // UTF8 escape
|
|
861
|
+
|
|
862
|
+
var bytes = [];
|
|
863
|
+
for (var i = 0; i < str.length; ++i) {
|
|
864
|
+
bytes.push(str.charCodeAt(i));
|
|
865
|
+
}
|
|
866
|
+
return bytes;
|
|
867
|
+
}
|
|
868
|
+
var DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
869
|
+
var URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
|
|
870
|
+
function v35(name, version, hashfunc) {
|
|
871
|
+
function generateUUID(value, namespace, buf, offset) {
|
|
872
|
+
var _namespace;
|
|
873
|
+
if (typeof value === 'string') {
|
|
874
|
+
value = stringToBytes(value);
|
|
875
|
+
}
|
|
876
|
+
if (typeof namespace === 'string') {
|
|
877
|
+
namespace = parse(namespace);
|
|
878
|
+
}
|
|
879
|
+
if (((_namespace = namespace) === null || _namespace === void 0 ? void 0 : _namespace.length) !== 16) {
|
|
880
|
+
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Compute hash of namespace and value, Per 4.3
|
|
884
|
+
// Future: Use spread syntax when supported on all platforms, e.g. `bytes =
|
|
885
|
+
// hashfunc([...namespace, ... value])`
|
|
886
|
+
var bytes = new Uint8Array(16 + value.length);
|
|
887
|
+
bytes.set(namespace);
|
|
888
|
+
bytes.set(value, namespace.length);
|
|
889
|
+
bytes = hashfunc(bytes);
|
|
890
|
+
bytes[6] = bytes[6] & 0x0f | version;
|
|
891
|
+
bytes[8] = bytes[8] & 0x3f | 0x80;
|
|
892
|
+
if (buf) {
|
|
893
|
+
offset = offset || 0;
|
|
894
|
+
for (var i = 0; i < 16; ++i) {
|
|
895
|
+
buf[offset + i] = bytes[i];
|
|
896
|
+
}
|
|
897
|
+
return buf;
|
|
898
|
+
}
|
|
899
|
+
return unsafeStringify(bytes);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Function#name is not settable on some platforms (#270)
|
|
903
|
+
try {
|
|
904
|
+
generateUUID.name = name;
|
|
905
|
+
} catch (err) {}
|
|
906
|
+
|
|
907
|
+
// For CommonJS default export support
|
|
908
|
+
generateUUID.DNS = DNS;
|
|
909
|
+
generateUUID.URL = URL;
|
|
910
|
+
return generateUUID;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/*
|
|
914
|
+
* Browser-compatible JavaScript MD5
|
|
915
|
+
*
|
|
916
|
+
* Modification of JavaScript MD5
|
|
917
|
+
* https://github.com/blueimp/JavaScript-MD5
|
|
918
|
+
*
|
|
919
|
+
* Copyright 2011, Sebastian Tschan
|
|
920
|
+
* https://blueimp.net
|
|
921
|
+
*
|
|
922
|
+
* Licensed under the MIT license:
|
|
923
|
+
* https://opensource.org/licenses/MIT
|
|
924
|
+
*
|
|
925
|
+
* Based on
|
|
926
|
+
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
|
927
|
+
* Digest Algorithm, as defined in RFC 1321.
|
|
928
|
+
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
|
929
|
+
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
|
930
|
+
* Distributed under the BSD License
|
|
931
|
+
* See http://pajhome.org.uk/crypt/md5 for more info.
|
|
932
|
+
*/
|
|
933
|
+
function md5(bytes) {
|
|
934
|
+
if (typeof bytes === 'string') {
|
|
935
|
+
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
|
|
936
|
+
|
|
937
|
+
bytes = new Uint8Array(msg.length);
|
|
938
|
+
for (var i = 0; i < msg.length; ++i) {
|
|
939
|
+
bytes[i] = msg.charCodeAt(i);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return md5ToHexEncodedArray(wordsToMd5(bytesToWords(bytes), bytes.length * 8));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/*
|
|
946
|
+
* Convert an array of little-endian words to an array of bytes
|
|
947
|
+
*/
|
|
948
|
+
function md5ToHexEncodedArray(input) {
|
|
949
|
+
var output = [];
|
|
950
|
+
var length32 = input.length * 32;
|
|
951
|
+
var hexTab = '0123456789abcdef';
|
|
952
|
+
for (var i = 0; i < length32; i += 8) {
|
|
953
|
+
var x = input[i >> 5] >>> i % 32 & 0xff;
|
|
954
|
+
var hex = parseInt(hexTab.charAt(x >>> 4 & 0x0f) + hexTab.charAt(x & 0x0f), 16);
|
|
955
|
+
output.push(hex);
|
|
956
|
+
}
|
|
957
|
+
return output;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Calculate output length with padding and bit length
|
|
962
|
+
*/
|
|
963
|
+
function getOutputLength(inputLength8) {
|
|
964
|
+
return (inputLength8 + 64 >>> 9 << 4) + 14 + 1;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/*
|
|
968
|
+
* Calculate the MD5 of an array of little-endian words, and a bit length.
|
|
969
|
+
*/
|
|
970
|
+
function wordsToMd5(x, len) {
|
|
971
|
+
/* append padding */
|
|
972
|
+
x[len >> 5] |= 0x80 << len % 32;
|
|
973
|
+
x[getOutputLength(len) - 1] = len;
|
|
974
|
+
var a = 1732584193;
|
|
975
|
+
var b = -271733879;
|
|
976
|
+
var c = -1732584194;
|
|
977
|
+
var d = 271733878;
|
|
978
|
+
for (var i = 0; i < x.length; i += 16) {
|
|
979
|
+
var olda = a;
|
|
980
|
+
var oldb = b;
|
|
981
|
+
var oldc = c;
|
|
982
|
+
var oldd = d;
|
|
983
|
+
a = md5ff(a, b, c, d, x[i], 7, -680876936);
|
|
984
|
+
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
|
|
985
|
+
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
|
|
986
|
+
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
|
|
987
|
+
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
|
|
988
|
+
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
|
|
989
|
+
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
|
|
990
|
+
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
|
|
991
|
+
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
|
|
992
|
+
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
|
|
993
|
+
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
|
|
994
|
+
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
|
|
995
|
+
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
|
|
996
|
+
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
|
|
997
|
+
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
|
|
998
|
+
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
|
|
999
|
+
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
|
|
1000
|
+
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
|
|
1001
|
+
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
|
|
1002
|
+
b = md5gg(b, c, d, a, x[i], 20, -373897302);
|
|
1003
|
+
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
|
|
1004
|
+
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
|
|
1005
|
+
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
|
|
1006
|
+
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
|
|
1007
|
+
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
|
|
1008
|
+
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
|
|
1009
|
+
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
|
|
1010
|
+
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
|
|
1011
|
+
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
|
|
1012
|
+
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
|
|
1013
|
+
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
|
|
1014
|
+
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
|
|
1015
|
+
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
|
|
1016
|
+
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
|
|
1017
|
+
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
|
|
1018
|
+
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
|
|
1019
|
+
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
|
|
1020
|
+
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
|
|
1021
|
+
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
|
|
1022
|
+
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
|
|
1023
|
+
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
|
|
1024
|
+
d = md5hh(d, a, b, c, x[i], 11, -358537222);
|
|
1025
|
+
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
|
|
1026
|
+
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
|
|
1027
|
+
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
|
|
1028
|
+
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
|
|
1029
|
+
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
|
|
1030
|
+
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
|
|
1031
|
+
a = md5ii(a, b, c, d, x[i], 6, -198630844);
|
|
1032
|
+
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
|
|
1033
|
+
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
|
|
1034
|
+
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
|
|
1035
|
+
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
|
|
1036
|
+
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
|
|
1037
|
+
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
|
|
1038
|
+
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
|
|
1039
|
+
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
|
|
1040
|
+
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
|
|
1041
|
+
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
|
|
1042
|
+
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
|
|
1043
|
+
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
|
|
1044
|
+
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
|
|
1045
|
+
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
|
|
1046
|
+
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
|
|
1047
|
+
a = safeAdd(a, olda);
|
|
1048
|
+
b = safeAdd(b, oldb);
|
|
1049
|
+
c = safeAdd(c, oldc);
|
|
1050
|
+
d = safeAdd(d, oldd);
|
|
1051
|
+
}
|
|
1052
|
+
return [a, b, c, d];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/*
|
|
1056
|
+
* Convert an array bytes to an array of little-endian words
|
|
1057
|
+
* Characters >255 have their high-byte silently ignored.
|
|
1058
|
+
*/
|
|
1059
|
+
function bytesToWords(input) {
|
|
1060
|
+
if (input.length === 0) {
|
|
1061
|
+
return [];
|
|
1062
|
+
}
|
|
1063
|
+
var length8 = input.length * 8;
|
|
1064
|
+
var output = new Uint32Array(getOutputLength(length8));
|
|
1065
|
+
for (var i = 0; i < length8; i += 8) {
|
|
1066
|
+
output[i >> 5] |= (input[i / 8] & 0xff) << i % 32;
|
|
1067
|
+
}
|
|
1068
|
+
return output;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/*
|
|
1072
|
+
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
|
1073
|
+
* to work around bugs in some JS interpreters.
|
|
1074
|
+
*/
|
|
1075
|
+
function safeAdd(x, y) {
|
|
1076
|
+
var lsw = (x & 0xffff) + (y & 0xffff);
|
|
1077
|
+
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
|
1078
|
+
return msw << 16 | lsw & 0xffff;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/*
|
|
1082
|
+
* Bitwise rotate a 32-bit number to the left.
|
|
1083
|
+
*/
|
|
1084
|
+
function bitRotateLeft(num, cnt) {
|
|
1085
|
+
return num << cnt | num >>> 32 - cnt;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/*
|
|
1089
|
+
* These functions implement the four basic operations the algorithm uses.
|
|
1090
|
+
*/
|
|
1091
|
+
function md5cmn(q, a, b, x, s, t) {
|
|
1092
|
+
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
|
|
1093
|
+
}
|
|
1094
|
+
function md5ff(a, b, c, d, x, s, t) {
|
|
1095
|
+
return md5cmn(b & c | ~b & d, a, b, x, s, t);
|
|
1096
|
+
}
|
|
1097
|
+
function md5gg(a, b, c, d, x, s, t) {
|
|
1098
|
+
return md5cmn(b & d | c & ~d, a, b, x, s, t);
|
|
1099
|
+
}
|
|
1100
|
+
function md5hh(a, b, c, d, x, s, t) {
|
|
1101
|
+
return md5cmn(b ^ c ^ d, a, b, x, s, t);
|
|
1102
|
+
}
|
|
1103
|
+
function md5ii(a, b, c, d, x, s, t) {
|
|
1104
|
+
return md5cmn(c ^ (b | ~d), a, b, x, s, t);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
var v3 = v35('v3', 0x30, md5);
|
|
1108
|
+
|
|
1109
|
+
var randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);
|
|
1110
|
+
var native = {
|
|
1111
|
+
randomUUID
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
function v4(options, buf, offset) {
|
|
1115
|
+
if (native.randomUUID && !buf && !options) {
|
|
1116
|
+
return native.randomUUID();
|
|
1117
|
+
}
|
|
1118
|
+
options = options || {};
|
|
1119
|
+
var rnds = options.random || (options.rng || rng)();
|
|
1120
|
+
|
|
1121
|
+
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
|
1122
|
+
rnds[6] = rnds[6] & 0x0f | 0x40;
|
|
1123
|
+
rnds[8] = rnds[8] & 0x3f | 0x80;
|
|
1124
|
+
|
|
1125
|
+
// Copy bytes to buffer, if provided
|
|
1126
|
+
if (buf) {
|
|
1127
|
+
offset = offset || 0;
|
|
1128
|
+
for (var i = 0; i < 16; ++i) {
|
|
1129
|
+
buf[offset + i] = rnds[i];
|
|
1130
|
+
}
|
|
1131
|
+
return buf;
|
|
1132
|
+
}
|
|
1133
|
+
return unsafeStringify(rnds);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Adapted from Chris Veness' SHA1 code at
|
|
1137
|
+
// http://www.movable-type.co.uk/scripts/sha1.html
|
|
1138
|
+
function f(s, x, y, z) {
|
|
1139
|
+
switch (s) {
|
|
1140
|
+
case 0:
|
|
1141
|
+
return x & y ^ ~x & z;
|
|
1142
|
+
case 1:
|
|
1143
|
+
return x ^ y ^ z;
|
|
1144
|
+
case 2:
|
|
1145
|
+
return x & y ^ x & z ^ y & z;
|
|
1146
|
+
case 3:
|
|
1147
|
+
return x ^ y ^ z;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function ROTL(x, n) {
|
|
1151
|
+
return x << n | x >>> 32 - n;
|
|
1152
|
+
}
|
|
1153
|
+
function sha1(bytes) {
|
|
1154
|
+
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
|
|
1155
|
+
var H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
|
|
1156
|
+
if (typeof bytes === 'string') {
|
|
1157
|
+
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
|
|
1158
|
+
|
|
1159
|
+
bytes = [];
|
|
1160
|
+
for (var i = 0; i < msg.length; ++i) {
|
|
1161
|
+
bytes.push(msg.charCodeAt(i));
|
|
1162
|
+
}
|
|
1163
|
+
} else if (!Array.isArray(bytes)) {
|
|
1164
|
+
// Convert Array-like to Array
|
|
1165
|
+
bytes = Array.prototype.slice.call(bytes);
|
|
1166
|
+
}
|
|
1167
|
+
bytes.push(0x80);
|
|
1168
|
+
var l = bytes.length / 4 + 2;
|
|
1169
|
+
var N = Math.ceil(l / 16);
|
|
1170
|
+
var M = new Array(N);
|
|
1171
|
+
for (var _i = 0; _i < N; ++_i) {
|
|
1172
|
+
var arr = new Uint32Array(16);
|
|
1173
|
+
for (var j = 0; j < 16; ++j) {
|
|
1174
|
+
arr[j] = bytes[_i * 64 + j * 4] << 24 | bytes[_i * 64 + j * 4 + 1] << 16 | bytes[_i * 64 + j * 4 + 2] << 8 | bytes[_i * 64 + j * 4 + 3];
|
|
1175
|
+
}
|
|
1176
|
+
M[_i] = arr;
|
|
1177
|
+
}
|
|
1178
|
+
M[N - 1][14] = (bytes.length - 1) * 8 / Math.pow(2, 32);
|
|
1179
|
+
M[N - 1][14] = Math.floor(M[N - 1][14]);
|
|
1180
|
+
M[N - 1][15] = (bytes.length - 1) * 8 & 0xffffffff;
|
|
1181
|
+
for (var _i2 = 0; _i2 < N; ++_i2) {
|
|
1182
|
+
var W = new Uint32Array(80);
|
|
1183
|
+
for (var t = 0; t < 16; ++t) {
|
|
1184
|
+
W[t] = M[_i2][t];
|
|
1185
|
+
}
|
|
1186
|
+
for (var _t = 16; _t < 80; ++_t) {
|
|
1187
|
+
W[_t] = ROTL(W[_t - 3] ^ W[_t - 8] ^ W[_t - 14] ^ W[_t - 16], 1);
|
|
1188
|
+
}
|
|
1189
|
+
var a = H[0];
|
|
1190
|
+
var b = H[1];
|
|
1191
|
+
var c = H[2];
|
|
1192
|
+
var d = H[3];
|
|
1193
|
+
var e = H[4];
|
|
1194
|
+
for (var _t2 = 0; _t2 < 80; ++_t2) {
|
|
1195
|
+
var s = Math.floor(_t2 / 20);
|
|
1196
|
+
var T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[_t2] >>> 0;
|
|
1197
|
+
e = d;
|
|
1198
|
+
d = c;
|
|
1199
|
+
c = ROTL(b, 30) >>> 0;
|
|
1200
|
+
b = a;
|
|
1201
|
+
a = T;
|
|
1202
|
+
}
|
|
1203
|
+
H[0] = H[0] + a >>> 0;
|
|
1204
|
+
H[1] = H[1] + b >>> 0;
|
|
1205
|
+
H[2] = H[2] + c >>> 0;
|
|
1206
|
+
H[3] = H[3] + d >>> 0;
|
|
1207
|
+
H[4] = H[4] + e >>> 0;
|
|
1208
|
+
}
|
|
1209
|
+
return [H[0] >> 24 & 0xff, H[0] >> 16 & 0xff, H[0] >> 8 & 0xff, H[0] & 0xff, H[1] >> 24 & 0xff, H[1] >> 16 & 0xff, H[1] >> 8 & 0xff, H[1] & 0xff, H[2] >> 24 & 0xff, H[2] >> 16 & 0xff, H[2] >> 8 & 0xff, H[2] & 0xff, H[3] >> 24 & 0xff, H[3] >> 16 & 0xff, H[3] >> 8 & 0xff, H[3] & 0xff, H[4] >> 24 & 0xff, H[4] >> 16 & 0xff, H[4] >> 8 & 0xff, H[4] & 0xff];
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
var v5 = v35('v5', 0x50, sha1);
|
|
1213
|
+
|
|
1214
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
1215
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
1216
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
1217
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
1218
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
*
|
|
1222
|
+
* @param {object} options
|
|
1223
|
+
* @param {Uint8Array=} buf
|
|
1224
|
+
* @param {number=} offset
|
|
1225
|
+
* @returns
|
|
1226
|
+
*/
|
|
1227
|
+
function v6(options = {}, buf, offset = 0) {
|
|
1228
|
+
// v6 is v1 with different field layout, so we start with a v1 UUID, albeit
|
|
1229
|
+
// with slightly different behavior around how the clock_seq and node fields
|
|
1230
|
+
// are randomized, which is why we call v1 with _v6: true.
|
|
1231
|
+
var bytes = v1(_objectSpread(_objectSpread({}, options), {}, {
|
|
1232
|
+
_v6: true
|
|
1233
|
+
}), new Uint8Array(16));
|
|
1234
|
+
|
|
1235
|
+
// Reorder the fields to v6 layout.
|
|
1236
|
+
bytes = v1ToV6(bytes);
|
|
1237
|
+
|
|
1238
|
+
// Return as a byte array if requested
|
|
1239
|
+
if (buf) {
|
|
1240
|
+
for (var i = 0; i < 16; i++) {
|
|
1241
|
+
buf[offset + i] = bytes[i];
|
|
1242
|
+
}
|
|
1243
|
+
return buf;
|
|
1244
|
+
}
|
|
1245
|
+
return unsafeStringify(bytes);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Convert a v6 UUID to a v1 UUID
|
|
1250
|
+
*
|
|
1251
|
+
* @param {string|Uint8Array} uuid - The v6 UUID to convert to v6
|
|
1252
|
+
* @returns {string|Uint8Array} The v1 UUID as the same type as the `uuid` arg
|
|
1253
|
+
* (string or Uint8Array)
|
|
1254
|
+
*/
|
|
1255
|
+
function v6ToV1(uuid) {
|
|
1256
|
+
var v6Bytes = typeof uuid === 'string' ? parse(uuid) : uuid;
|
|
1257
|
+
var v1Bytes = _v6ToV1(v6Bytes);
|
|
1258
|
+
return typeof uuid === 'string' ? unsafeStringify(v1Bytes) : v1Bytes;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Do the field transformation needed for v6 -> v1
|
|
1262
|
+
function _v6ToV1(v6Bytes) {
|
|
1263
|
+
return Uint8Array.of((v6Bytes[3] & 0x0f) << 4 | v6Bytes[4] >> 4 & 0x0f, (v6Bytes[4] & 0x0f) << 4 | (v6Bytes[5] & 0xf0) >> 4, (v6Bytes[5] & 0x0f) << 4 | v6Bytes[6] & 0x0f, v6Bytes[7], (v6Bytes[1] & 0x0f) << 4 | (v6Bytes[2] & 0xf0) >> 4, (v6Bytes[2] & 0x0f) << 4 | (v6Bytes[3] & 0xf0) >> 4, 0x10 | (v6Bytes[0] & 0xf0) >> 4, (v6Bytes[0] & 0x0f) << 4 | (v6Bytes[1] & 0xf0) >> 4, v6Bytes[8], v6Bytes[9], v6Bytes[10], v6Bytes[11], v6Bytes[12], v6Bytes[13], v6Bytes[14], v6Bytes[15]);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* UUID V7 - Unix Epoch time-based UUID
|
|
1268
|
+
*
|
|
1269
|
+
* The IETF has published RFC9562, introducing 3 new UUID versions (6,7,8). This
|
|
1270
|
+
* implementation of V7 is based on the accepted, though not yet approved,
|
|
1271
|
+
* revisions.
|
|
1272
|
+
*
|
|
1273
|
+
* RFC 9562:https://www.rfc-editor.org/rfc/rfc9562.html Universally Unique
|
|
1274
|
+
* IDentifiers (UUIDs)
|
|
1275
|
+
|
|
1276
|
+
*
|
|
1277
|
+
* Sample V7 value:
|
|
1278
|
+
* https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv7-value
|
|
1279
|
+
*
|
|
1280
|
+
* Monotonic Bit Layout: RFC rfc9562.6.2 Method 1, Dedicated Counter Bits ref:
|
|
1281
|
+
* https://www.rfc-editor.org/rfc/rfc9562.html#section-6.2-5.1
|
|
1282
|
+
*
|
|
1283
|
+
* 0 1 2 3 0 1 2 3 4 5 6
|
|
1284
|
+
* 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
1285
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
1286
|
+
* | unix_ts_ms |
|
|
1287
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
1288
|
+
* | unix_ts_ms | ver | seq_hi |
|
|
1289
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
1290
|
+
* |var| seq_low | rand |
|
|
1291
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
1292
|
+
* | rand |
|
|
1293
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
1294
|
+
*
|
|
1295
|
+
* seq is a 31 bit serialized counter; comprised of 12 bit seq_hi and 19 bit
|
|
1296
|
+
* seq_low, and randomly initialized upon timestamp change. 31 bit counter size
|
|
1297
|
+
* was selected as any bitwise operations in node are done as _signed_ 32 bit
|
|
1298
|
+
* ints. we exclude the sign bit.
|
|
1299
|
+
*/
|
|
1300
|
+
|
|
1301
|
+
var _seqLow = null;
|
|
1302
|
+
var _seqHigh = null;
|
|
1303
|
+
var _msecs = 0;
|
|
1304
|
+
function v7(options, buf, offset) {
|
|
1305
|
+
options = options || {};
|
|
1306
|
+
|
|
1307
|
+
// initialize buffer and pointer
|
|
1308
|
+
var i = buf && offset || 0;
|
|
1309
|
+
var b = buf || new Uint8Array(16);
|
|
1310
|
+
|
|
1311
|
+
// rnds is Uint8Array(16) filled with random bytes
|
|
1312
|
+
var rnds = options.random || (options.rng || rng)();
|
|
1313
|
+
|
|
1314
|
+
// milliseconds since unix epoch, 1970-01-01 00:00
|
|
1315
|
+
var msecs = options.msecs !== undefined ? options.msecs : Date.now();
|
|
1316
|
+
|
|
1317
|
+
// seq is user provided 31 bit counter
|
|
1318
|
+
var seq = options.seq !== undefined ? options.seq : null;
|
|
1319
|
+
|
|
1320
|
+
// initialize local seq high/low parts
|
|
1321
|
+
var seqHigh = _seqHigh;
|
|
1322
|
+
var seqLow = _seqLow;
|
|
1323
|
+
|
|
1324
|
+
// check if clock has advanced and user has not provided msecs
|
|
1325
|
+
if (msecs > _msecs && options.msecs === undefined) {
|
|
1326
|
+
_msecs = msecs;
|
|
1327
|
+
|
|
1328
|
+
// unless user provided seq, reset seq parts
|
|
1329
|
+
if (seq !== null) {
|
|
1330
|
+
seqHigh = null;
|
|
1331
|
+
seqLow = null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// if we have a user provided seq
|
|
1336
|
+
if (seq !== null) {
|
|
1337
|
+
// trim provided seq to 31 bits of value, avoiding overflow
|
|
1338
|
+
if (seq > 0x7fffffff) {
|
|
1339
|
+
seq = 0x7fffffff;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// split provided seq into high/low parts
|
|
1343
|
+
seqHigh = seq >>> 19 & 0xfff;
|
|
1344
|
+
seqLow = seq & 0x7ffff;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// randomly initialize seq
|
|
1348
|
+
if (seqHigh === null || seqLow === null) {
|
|
1349
|
+
seqHigh = rnds[6] & 0x7f;
|
|
1350
|
+
seqHigh = seqHigh << 8 | rnds[7];
|
|
1351
|
+
seqLow = rnds[8] & 0x3f; // pad for var
|
|
1352
|
+
seqLow = seqLow << 8 | rnds[9];
|
|
1353
|
+
seqLow = seqLow << 5 | rnds[10] >>> 3;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// increment seq if within msecs window
|
|
1357
|
+
if (msecs + 10000 > _msecs && seq === null) {
|
|
1358
|
+
if (++seqLow > 0x7ffff) {
|
|
1359
|
+
seqLow = 0;
|
|
1360
|
+
if (++seqHigh > 0xfff) {
|
|
1361
|
+
seqHigh = 0;
|
|
1362
|
+
|
|
1363
|
+
// increment internal _msecs. this allows us to continue incrementing
|
|
1364
|
+
// while staying monotonic. Note, once we hit 10k milliseconds beyond system
|
|
1365
|
+
// clock, we will reset breaking monotonicity (after (2^31)*10000 generations)
|
|
1366
|
+
_msecs++;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} else {
|
|
1370
|
+
// resetting; we have advanced more than
|
|
1371
|
+
// 10k milliseconds beyond system clock
|
|
1372
|
+
_msecs = msecs;
|
|
1373
|
+
}
|
|
1374
|
+
_seqHigh = seqHigh;
|
|
1375
|
+
_seqLow = seqLow;
|
|
1376
|
+
|
|
1377
|
+
// [bytes 0-5] 48 bits of local timestamp
|
|
1378
|
+
b[i++] = _msecs / 0x10000000000 & 0xff;
|
|
1379
|
+
b[i++] = _msecs / 0x100000000 & 0xff;
|
|
1380
|
+
b[i++] = _msecs / 0x1000000 & 0xff;
|
|
1381
|
+
b[i++] = _msecs / 0x10000 & 0xff;
|
|
1382
|
+
b[i++] = _msecs / 0x100 & 0xff;
|
|
1383
|
+
b[i++] = _msecs & 0xff;
|
|
1384
|
+
|
|
1385
|
+
// [byte 6] - set 4 bits of version (7) with first 4 bits seq_hi
|
|
1386
|
+
b[i++] = seqHigh >>> 4 & 0x0f | 0x70;
|
|
1387
|
+
|
|
1388
|
+
// [byte 7] remaining 8 bits of seq_hi
|
|
1389
|
+
b[i++] = seqHigh & 0xff;
|
|
1390
|
+
|
|
1391
|
+
// [byte 8] - variant (2 bits), first 6 bits seq_low
|
|
1392
|
+
b[i++] = seqLow >>> 13 & 0x3f | 0x80;
|
|
1393
|
+
|
|
1394
|
+
// [byte 9] 8 bits seq_low
|
|
1395
|
+
b[i++] = seqLow >>> 5 & 0xff;
|
|
1396
|
+
|
|
1397
|
+
// [byte 10] remaining 5 bits seq_low, 3 bits random
|
|
1398
|
+
b[i++] = seqLow << 3 & 0xff | rnds[10] & 0x07;
|
|
1399
|
+
|
|
1400
|
+
// [bytes 11-15] always random
|
|
1401
|
+
b[i++] = rnds[11];
|
|
1402
|
+
b[i++] = rnds[12];
|
|
1403
|
+
b[i++] = rnds[13];
|
|
1404
|
+
b[i++] = rnds[14];
|
|
1405
|
+
b[i++] = rnds[15];
|
|
1406
|
+
return buf || unsafeStringify(b);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function version(uuid) {
|
|
1410
|
+
if (!validate(uuid)) {
|
|
1411
|
+
throw TypeError('Invalid UUID');
|
|
1412
|
+
}
|
|
1413
|
+
return parseInt(uuid.slice(14, 15), 16);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function calculatePolygonArea(points) {
|
|
1417
|
+
let area = 0;
|
|
1418
|
+
for (let i = 0; i < points.length; i++) {
|
|
1419
|
+
const j = (i + 1) % points.length;
|
|
1420
|
+
area += points[i].x * points[j].y;
|
|
1421
|
+
area -= points[j].x * points[i].y;
|
|
1422
|
+
}
|
|
1423
|
+
return Math.abs(area / 2);
|
|
1424
|
+
}
|
|
1425
|
+
;
|
|
1426
|
+
function azimuthToCardinal(azimuth) {
|
|
1427
|
+
// Convert angle to cardinal direction
|
|
1428
|
+
if (azimuth >= 337.5 || azimuth < 22.5)
|
|
1429
|
+
return "North";
|
|
1430
|
+
if (azimuth >= 22.5 && azimuth < 67.5)
|
|
1431
|
+
return "Northeast";
|
|
1432
|
+
if (azimuth >= 67.5 && azimuth < 112.5)
|
|
1433
|
+
return "East";
|
|
1434
|
+
if (azimuth >= 112.5 && azimuth < 157.5)
|
|
1435
|
+
return "Southeast";
|
|
1436
|
+
if (azimuth >= 157.5 && azimuth < 202.5)
|
|
1437
|
+
return "South";
|
|
1438
|
+
if (azimuth >= 202.5 && azimuth < 247.5)
|
|
1439
|
+
return "Southwest";
|
|
1440
|
+
if (azimuth >= 247.5 && azimuth < 292.5)
|
|
1441
|
+
return "West";
|
|
1442
|
+
return "Northwest";
|
|
1443
|
+
}
|
|
1444
|
+
function calculatePolygonOrientation(points) {
|
|
1445
|
+
if (points.length < 3)
|
|
1446
|
+
return 0;
|
|
1447
|
+
// Calculate the average direction of the polygon
|
|
1448
|
+
let totalAngle = 0;
|
|
1449
|
+
for (let i = 0; i < points.length; i++) {
|
|
1450
|
+
const j = (i + 1) % points.length;
|
|
1451
|
+
const angle = Math.atan2(points[j].y - points[i].y, points[j].x - points[i].x);
|
|
1452
|
+
totalAngle += angle;
|
|
1453
|
+
}
|
|
1454
|
+
const avgAngle = (totalAngle / points.length) * (180 / Math.PI);
|
|
1455
|
+
const normalizedAngle = (avgAngle + 360) % 360;
|
|
1456
|
+
return normalizedAngle;
|
|
1457
|
+
}
|
|
1458
|
+
;
|
|
1459
|
+
function calculatePolygonAngle(points) {
|
|
1460
|
+
if (points.length < 3)
|
|
1461
|
+
return 0;
|
|
1462
|
+
// Calculate the average angle of the polygon
|
|
1463
|
+
let totalAngle = 0;
|
|
1464
|
+
for (let i = 0; i < points.length; i++) {
|
|
1465
|
+
const j = (i + 1) % points.length;
|
|
1466
|
+
const k = (i + 2) % points.length;
|
|
1467
|
+
const v1 = {
|
|
1468
|
+
x: points[j].x - points[i].x,
|
|
1469
|
+
y: points[j].y - points[i].y,
|
|
1470
|
+
};
|
|
1471
|
+
const v2 = {
|
|
1472
|
+
x: points[k].x - points[j].x,
|
|
1473
|
+
y: points[k].y - points[j].y,
|
|
1474
|
+
};
|
|
1475
|
+
const dot = v1.x * v2.x + v1.y * v2.y;
|
|
1476
|
+
const mag1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
|
|
1477
|
+
const mag2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
|
|
1478
|
+
const angle = Math.acos(dot / (mag1 * mag2));
|
|
1479
|
+
totalAngle += angle;
|
|
1480
|
+
}
|
|
1481
|
+
return (totalAngle / points.length) * (180 / Math.PI);
|
|
1482
|
+
}
|
|
1483
|
+
;
|
|
1484
|
+
function latLngToPixel(bounds, canvas, latLng) {
|
|
1485
|
+
const ctx = canvas.getContext('2d');
|
|
1486
|
+
if (!ctx) {
|
|
1487
|
+
throw new Error('Canvas context not found');
|
|
1488
|
+
}
|
|
1489
|
+
const latToPixel = (lat) => {
|
|
1490
|
+
return canvas.height * (1 - (lat - bounds.south) / (bounds.north - bounds.south));
|
|
1491
|
+
};
|
|
1492
|
+
const lngToPixel = (lng) => {
|
|
1493
|
+
return canvas.width * (lng - bounds.west) / (bounds.east - bounds.west);
|
|
1494
|
+
};
|
|
1495
|
+
return {
|
|
1496
|
+
x: lngToPixel(latLng.longitude),
|
|
1497
|
+
y: latToPixel(latLng.latitude),
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function getPixelInMeters(rgbTiff) {
|
|
1501
|
+
const latDiff = rgbTiff.bounds.north - rgbTiff.bounds.south;
|
|
1502
|
+
// const lngDiff = rgbTiff.bounds.east - rgbTiff.bounds.west;
|
|
1503
|
+
// const pixelWidth = rgbTiff.width;
|
|
1504
|
+
const pixelHeight = rgbTiff.height;
|
|
1505
|
+
const pixelInMeters = latDiff * 111320 / pixelHeight;
|
|
1506
|
+
// const latAvg = (rgbTiff.bounds.north + rgbTiff.bounds.south) / 2;
|
|
1507
|
+
// const pixelInMetersLng = lngDiff * 111320 * Math.cos(latAvg * Math.PI / 180) / pixelWidth;
|
|
1508
|
+
return pixelInMeters;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Get the intersections of a line parallel to the x-axis of the projected coordinate system with a polygon.
|
|
1513
|
+
* @param projectedPoint the point to draw the line from
|
|
1514
|
+
* @param projectedPolygon the polygon to get the intersections with
|
|
1515
|
+
* @returns the intersections of the line with the polygon
|
|
1516
|
+
*/
|
|
1517
|
+
function getIntersections(projectedPoint, projectedPolygon) {
|
|
1518
|
+
const intersections = [];
|
|
1519
|
+
if (projectedPolygon.points.length === 0) {
|
|
1520
|
+
return intersections;
|
|
1521
|
+
}
|
|
1522
|
+
// For each edge of the polygon
|
|
1523
|
+
for (let i = 0; i < projectedPolygon.points.length; i++) {
|
|
1524
|
+
const p1 = projectedPolygon.points[i];
|
|
1525
|
+
const p2 = projectedPolygon.points[(i + 1) % projectedPolygon.points.length];
|
|
1526
|
+
// Calculate intersection point
|
|
1527
|
+
const t = (projectedPoint.y - p1.y) / (p2.y - p1.y);
|
|
1528
|
+
const x = p1.x + t * (p2.x - p1.x);
|
|
1529
|
+
if (t > 0 && t <= 1) {
|
|
1530
|
+
intersections.push({ x, y: projectedPoint.y });
|
|
1531
|
+
}
|
|
1532
|
+
if (t === 0) {
|
|
1533
|
+
intersections.push({ x: p1.x, y: projectedPoint.y });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
// Sort intersections by y-coordinate
|
|
1537
|
+
return intersections.sort((a, b) => a.x - b.x);
|
|
1538
|
+
}
|
|
1539
|
+
function projectPoint(point, azimuth) {
|
|
1540
|
+
const angle = azimuth * (Math.PI / 180);
|
|
1541
|
+
const x = point.x * Math.cos(angle) - point.y * Math.sin(angle);
|
|
1542
|
+
const y = point.x * Math.sin(angle) + point.y * Math.cos(angle);
|
|
1543
|
+
return { x, y };
|
|
1544
|
+
}
|
|
1545
|
+
function projectPolygon(polygon, azimuth) {
|
|
1546
|
+
return {
|
|
1547
|
+
points: polygon.points.map((point) => projectPoint(point, azimuth)),
|
|
1548
|
+
type: polygon.type,
|
|
1549
|
+
closed: polygon.closed,
|
|
1550
|
+
_id: polygon._id,
|
|
1551
|
+
details: polygon.details,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function offsetPolygon(polygon, offset) {
|
|
1555
|
+
if (polygon.points.length === 0) {
|
|
1556
|
+
return { polygon: polygon, offset: { x: 0, y: 0 } };
|
|
1557
|
+
}
|
|
1558
|
+
let offsetX = 0;
|
|
1559
|
+
let offsetY = 0;
|
|
1560
|
+
if (!offset) {
|
|
1561
|
+
const minX = Math.min(...polygon.points.map((point) => point.x));
|
|
1562
|
+
const minY = Math.min(...polygon.points.map((point) => point.y));
|
|
1563
|
+
const maxX = Math.max(...polygon.points.map((point) => point.x));
|
|
1564
|
+
const maxY = Math.max(...polygon.points.map((point) => point.y));
|
|
1565
|
+
offsetX = minX + (maxX - minX) / 2;
|
|
1566
|
+
offsetY = minY + (maxY - minY) / 2;
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
offsetX = offset.x;
|
|
1570
|
+
offsetY = offset.y;
|
|
1571
|
+
}
|
|
1572
|
+
const newPoints = polygon.points.map((point) => ({
|
|
1573
|
+
x: point.x - offsetX,
|
|
1574
|
+
y: point.y - offsetY,
|
|
1575
|
+
}));
|
|
1576
|
+
return { polygon: { ...polygon, points: newPoints }, offset: { x: offsetX, y: offsetY } };
|
|
1577
|
+
}
|
|
1578
|
+
function equalsPoints(point1, point2) {
|
|
1579
|
+
return point1.x === point2.x && point1.y === point2.y;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Checks if two lines intersect.
|
|
1583
|
+
* if the lines intersect in a start or end point they are not considered as intersecting
|
|
1584
|
+
* @param line1 The first line.
|
|
1585
|
+
* @param line2 The second line.
|
|
1586
|
+
* @returns True if the lines intersect, false otherwise.
|
|
1587
|
+
*/
|
|
1588
|
+
function intersects(line1, line2) {
|
|
1589
|
+
// Get the points of each line
|
|
1590
|
+
const p1 = line1.start;
|
|
1591
|
+
const p2 = line1.end;
|
|
1592
|
+
const p3 = line2.start;
|
|
1593
|
+
const p4 = line2.end;
|
|
1594
|
+
if (equalsPoints(p1, p3) || equalsPoints(p1, p4) || equalsPoints(p2, p3) || equalsPoints(p2, p4)) {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
// Calculate the cross products
|
|
1598
|
+
const d1 = (p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x);
|
|
1599
|
+
const d2 = (p4.x - p3.x) * (p2.y - p3.y) - (p4.y - p3.y) * (p2.x - p3.x);
|
|
1600
|
+
const d3 = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
|
|
1601
|
+
const d4 = (p2.x - p1.x) * (p4.y - p1.y) - (p2.y - p1.y) * (p4.x - p1.x);
|
|
1602
|
+
// Check if the lines intersect
|
|
1603
|
+
return (d1 * d2 < 0) && (d3 * d4 < 0);
|
|
1604
|
+
}
|
|
1605
|
+
function undoOffsetPolygon(polygon, offset) {
|
|
1606
|
+
return {
|
|
1607
|
+
...polygon,
|
|
1608
|
+
points: polygon.points.map((point) => ({
|
|
1609
|
+
x: point.x + offset.x,
|
|
1610
|
+
y: point.y + offset.y,
|
|
1611
|
+
})),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function segmentPolygon(polygon) {
|
|
1615
|
+
const segments = [];
|
|
1616
|
+
const segmentsMap = {};
|
|
1617
|
+
for (const point of polygon.points) {
|
|
1618
|
+
const intersections = getIntersections(point, polygon);
|
|
1619
|
+
for (const [index, intersection] of intersections.slice(0, -1).entries()) {
|
|
1620
|
+
const nextIntersection = intersections[index + 1];
|
|
1621
|
+
const halfPoint = {
|
|
1622
|
+
x: (intersection.x + nextIntersection.x) / 2,
|
|
1623
|
+
y: (intersection.y + nextIntersection.y) / 2,
|
|
1624
|
+
};
|
|
1625
|
+
const isInner = isPointInPolygon(halfPoint, polygon);
|
|
1626
|
+
const isOnBorder = isPointOnBorder(halfPoint, polygon);
|
|
1627
|
+
const key = `${intersection.x}-${intersection.y}-${nextIntersection.x}-${nextIntersection.y}`;
|
|
1628
|
+
if (segmentsMap[key]) {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
segments.push({ start: intersection, end: nextIntersection, type: isOnBorder ? "border" : (isInner ? "inner" : "outer") });
|
|
1632
|
+
segmentsMap[key] = true;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return segments;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* for each segment this method will find all the following segments that are connected to it
|
|
1639
|
+
*
|
|
1640
|
+
* first the method subdivides the segments into y levels (all segments that have the same y coordinate are in the same level)
|
|
1641
|
+
* then it will check for each segment in each level if there is a following segment that is connected to it by checking it the
|
|
1642
|
+
* start point of the top segment to the end point of the bottom segment does not intersect with any of the polygon borders and the center point of the connection is in the polygon
|
|
1643
|
+
*
|
|
1644
|
+
* @param segments
|
|
1645
|
+
* @param roof
|
|
1646
|
+
* @returns
|
|
1647
|
+
*/
|
|
1648
|
+
function matchSegments(segments, roof) {
|
|
1649
|
+
const matches = [];
|
|
1650
|
+
const map = {};
|
|
1651
|
+
for (const segment of segments) {
|
|
1652
|
+
map[`${segment.start.x}-${segment.start.y}-${segment.end.x}-${segment.end.y}`] = segment;
|
|
1653
|
+
}
|
|
1654
|
+
// Group segments by y-coordinate (level)
|
|
1655
|
+
const levels = {};
|
|
1656
|
+
segments.forEach(segment => {
|
|
1657
|
+
const y = segment.start.y;
|
|
1658
|
+
if (!levels[y]) {
|
|
1659
|
+
levels[y] = [];
|
|
1660
|
+
}
|
|
1661
|
+
levels[y].push(segment);
|
|
1662
|
+
});
|
|
1663
|
+
// Sort levels by y-coordinate
|
|
1664
|
+
const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b);
|
|
1665
|
+
// For each level except the last one
|
|
1666
|
+
for (let i = 0; i < sortedLevels.length - 1; i++) {
|
|
1667
|
+
const currentLevel = levels[sortedLevels[i]];
|
|
1668
|
+
const nextLevel = levels[sortedLevels[i + 1]];
|
|
1669
|
+
// For each segment in current level
|
|
1670
|
+
for (const currentSegment of currentLevel) {
|
|
1671
|
+
if (currentSegment.type === "outer") {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const matchedSegments = [];
|
|
1675
|
+
// For each segment in next level
|
|
1676
|
+
for (const nextSegment of nextLevel) {
|
|
1677
|
+
// Check if segments can be connected
|
|
1678
|
+
const connectionStart = currentSegment.start;
|
|
1679
|
+
const connectionEnd = nextSegment.end;
|
|
1680
|
+
let intersected = false;
|
|
1681
|
+
for (let i = 0; i < roof.points.length; i++) {
|
|
1682
|
+
const line = {
|
|
1683
|
+
start: roof.points[i],
|
|
1684
|
+
end: roof.points[(i + 1) % roof.points.length]
|
|
1685
|
+
};
|
|
1686
|
+
if (intersects(line, { start: connectionStart, end: connectionEnd })) {
|
|
1687
|
+
intersected = true;
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (intersected) {
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
// Check if center point of connection is inside polygon
|
|
1695
|
+
const centerPoint = {
|
|
1696
|
+
x: (connectionStart.x + connectionEnd.x) / 2,
|
|
1697
|
+
y: (connectionStart.y + connectionEnd.y) / 2
|
|
1698
|
+
};
|
|
1699
|
+
if (isPointInPolygon(centerPoint, roof)) {
|
|
1700
|
+
matchedSegments.push(nextSegment);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (matchedSegments.length > 0) {
|
|
1704
|
+
matches.push({
|
|
1705
|
+
segment: `${currentSegment.start.x}-${currentSegment.start.y}-${currentSegment.end.x}-${currentSegment.end.y}`,
|
|
1706
|
+
matchedSegments: matchedSegments.map(segment => `${segment.start.x}-${segment.start.y}-${segment.end.x}-${segment.end.y}`)
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return { matches, map };
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Fits panels to a roof polygon.
|
|
1715
|
+
*
|
|
1716
|
+
* Starts with positioning a solar panel row (a rectangle with the height of the solar panel and the width in the direction of the perpendicular of
|
|
1717
|
+
* the azimuth of the roof) at each corner of the roof.
|
|
1718
|
+
* For each corner there are four possible cases (the width is the width of the whole roof in the direction of the perpendicular of the azimuth):
|
|
1719
|
+
* 1. The row has height of the solar panel height and sits below the point in the direction of the azimuth of the roof.
|
|
1720
|
+
* 2. The row has height of the solar panel height and sits above the point in the direction of the azimuth of the roof.
|
|
1721
|
+
* 3. The row has the height of the solar panel width (solar panels are rotated) and sits below the point in the direction of the azimuth of the roof.
|
|
1722
|
+
* 4. The row has the height of the solar panel width (solar panels are rotated) and sits above the point in the direction of the azimuth of the roof.
|
|
1723
|
+
*
|
|
1724
|
+
* For each of these cases the function then places the solar panel rows directly above and below the current row.
|
|
1725
|
+
*
|
|
1726
|
+
* after the function has positioned all the rows it places the solar panels in the spaces between the rows.
|
|
1727
|
+
* For each of the four scenarios of each corner the number of panels are counted and the optimal placement is chosen.
|
|
1728
|
+
*
|
|
1729
|
+
* @param roof - The roof polygon.
|
|
1730
|
+
* @param panelType - The type of panel to fit.
|
|
1731
|
+
* @param borderOffset - The offset of the panel from the border of the roof. If negative, the panel can be partially overhanging the roof.
|
|
1732
|
+
* @param azimuth - The azimuth of the roof.
|
|
1733
|
+
* @returns The positioned panels.
|
|
1734
|
+
*/
|
|
1735
|
+
function fitPanelsToRoof(roof, panelType, borderOffset = 0, azimuth = 0) {
|
|
1736
|
+
if (!roof || !panelType || !roof.points || !roof.details) {
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
console.log(borderOffset, azimuth);
|
|
1740
|
+
return [];
|
|
1741
|
+
// const minX = Math.min(...roof.points.map(p => p.x));
|
|
1742
|
+
// const maxX = Math.max(...roof.points.map(p => p.x));
|
|
1743
|
+
// const minY = Math.min(...roof.points.map(p => p.y));
|
|
1744
|
+
// const maxY = Math.max(...roof.points.map(p => p.y));
|
|
1745
|
+
// const projectedRoof = projectPolygon(roof, azimuth);
|
|
1746
|
+
// const projectedOffsetRoof = offsetPolygon(projectedRoof);
|
|
1747
|
+
// const segments = segmentPolygon(projectedOffsetRoof.polygon);
|
|
1748
|
+
// const {matches, map} = matchSegments(segments, projectedOffsetRoof.polygon);
|
|
1749
|
+
// const matchesMap: Record<string, LineSegment[]> = {};
|
|
1750
|
+
// for (const match of matches) {
|
|
1751
|
+
// matchesMap[match.segment] = match.matchedSegments.map(segment => map[segment]);
|
|
1752
|
+
// }
|
|
1753
|
+
// let panels: PositionedSolarPanel[] = [];
|
|
1754
|
+
// let undidPanels: PositionedSolarPanel[] = [];
|
|
1755
|
+
// for (const segment of segments) {
|
|
1756
|
+
// if (segment.type === "outer") {
|
|
1757
|
+
// continue;
|
|
1758
|
+
// }
|
|
1759
|
+
// const matches = matchesMap[`${segment.start.x}-${segment.start.y}-${segment.end.x}-${segment.end.y}`];
|
|
1760
|
+
// if (!matches || matches.length === 0) {
|
|
1761
|
+
// continue;
|
|
1762
|
+
// }
|
|
1763
|
+
// // Combine segments until we reach panel height
|
|
1764
|
+
// let combinedSegments: Array<Array<LineSegment>> = [[segment]];
|
|
1765
|
+
// let currentHeight = 0;
|
|
1766
|
+
// let currentMatches = matches;
|
|
1767
|
+
// // while (currentHeight < panelType.heightMeters && currentMatches.length > 0) {
|
|
1768
|
+
// // const nextSegment = currentMatches[0];
|
|
1769
|
+
// // combinedSegments.push(nextSegment);
|
|
1770
|
+
// // currentHeight += Math.abs(nextSegment.start.y - segment.start.y);
|
|
1771
|
+
// // currentMatches = matchesMap[`${nextSegment.start.x}-${nextSegment.start.y}-${nextSegment.end.x}-${nextSegment.end.y}`] || [];
|
|
1772
|
+
// // }
|
|
1773
|
+
// if (currentHeight < panelType.heightMeters) {
|
|
1774
|
+
// continue;
|
|
1775
|
+
// }
|
|
1776
|
+
// // Calculate minimum width
|
|
1777
|
+
// // let minEnd = Math.max(...combinedSegments.map(s => s.end.x));
|
|
1778
|
+
// // let maxStart = Math.min(...combinedSegments.map(s => s.start.x));
|
|
1779
|
+
// // let width = minEnd - maxStart;
|
|
1780
|
+
// // if (width >= panelType.widthMeters) {
|
|
1781
|
+
// // // Find highest segment for starting position
|
|
1782
|
+
// // const highestSegment = combinedSegments.reduce((prev, curr) =>
|
|
1783
|
+
// // curr.start.y < prev.start.y ? curr : prev
|
|
1784
|
+
// // );
|
|
1785
|
+
// // // Place panels in a grid
|
|
1786
|
+
// // const panelSpacing = panelType.widthMeters + borderOffset;
|
|
1787
|
+
// // const rowSpacing = panelType.heightMeters + borderOffset;
|
|
1788
|
+
// // let currentX = maxStart;
|
|
1789
|
+
// // let currentY = highestSegment.start.y;
|
|
1790
|
+
// // while (currentX + panelType.widthMeters <= minEnd) {
|
|
1791
|
+
// // while (currentY + panelType.heightMeters <= highestSegment.start.y + currentHeight) {
|
|
1792
|
+
// // const panelCenter = {
|
|
1793
|
+
// // x: currentX + panelType.widthMeters / 2,
|
|
1794
|
+
// // y: currentY + panelType.heightMeters / 2
|
|
1795
|
+
// // };
|
|
1796
|
+
// // if (isPointInPolygon(panelCenter, projectedOffsetRoof.polygon)) {
|
|
1797
|
+
// // panels.push({
|
|
1798
|
+
// // panel: panelType,
|
|
1799
|
+
// // pixelPosition: panelCenter
|
|
1800
|
+
// // });
|
|
1801
|
+
// // }
|
|
1802
|
+
// // currentY += rowSpacing;
|
|
1803
|
+
// // }
|
|
1804
|
+
// // currentX += panelSpacing;
|
|
1805
|
+
// // currentY = highestSegment.start.y;
|
|
1806
|
+
// // }
|
|
1807
|
+
// // }
|
|
1808
|
+
// }
|
|
1809
|
+
// for (const panel of panels) {
|
|
1810
|
+
// const undoOffset = {
|
|
1811
|
+
// x: panel.pixelPosition.x + projectedOffsetRoof.offset.x,
|
|
1812
|
+
// y: panel.pixelPosition.y + projectedOffsetRoof.offset.y,
|
|
1813
|
+
// };
|
|
1814
|
+
// const projectedPanel = projectPoint(undoOffset, azimuth);
|
|
1815
|
+
// undidPanels.push({
|
|
1816
|
+
// panel: panel.panel,
|
|
1817
|
+
// pixelPosition: projectedPanel,
|
|
1818
|
+
// });
|
|
1819
|
+
// }
|
|
1820
|
+
// return panels;
|
|
1821
|
+
// project the polygon points such that the azimuth is 0 degrees
|
|
1822
|
+
// this means that the solar panel are aligned perfectly with the coordinate system
|
|
1823
|
+
//
|
|
1824
|
+
// 1. below point and height is height
|
|
1825
|
+
// for each point do the following
|
|
1826
|
+
// draw a line along the y-axis of the projected coordinate system (this is the perpendicular of the azimuth)
|
|
1827
|
+
// find all the intersection of that line with the roof polygon
|
|
1828
|
+
// get all points that are within the height of the solar panel in the direction of the azimuth
|
|
1829
|
+
//
|
|
1830
|
+
// for each point generate a line along the y-axis of the projected coordinate system (this is the perpendicular of the azimuth)
|
|
1831
|
+
// find all points on that line that intersect with the roof polygon bounds
|
|
1832
|
+
// if the number of points is even
|
|
1833
|
+
//
|
|
1834
|
+
// const positionedPanels: PositionedSolarPanel[] = [];
|
|
1835
|
+
// let bestPanelCount = 0;
|
|
1836
|
+
// let bestPanelArrangement: PositionedSolarPanel[] = [];
|
|
1837
|
+
// // Get roof details
|
|
1838
|
+
// const { azimuth } = roof.details;
|
|
1839
|
+
// const perpendicularAzimuth = (azimuth + 90) % 360;
|
|
1840
|
+
// for (const point of roof.points) {
|
|
1841
|
+
// const row = createPanelRow(point, false, false);
|
|
1842
|
+
// positionedPanels.push(...row);
|
|
1843
|
+
// }
|
|
1844
|
+
// Helper function to create a panel row
|
|
1845
|
+
// const createPanelRow = (startPoint: Point, isRotated: boolean, isAbove: boolean): PositionedSolarPanel[] => {
|
|
1846
|
+
// const rowPanels: PositionedSolarPanel[] = [];
|
|
1847
|
+
// const panelWidth = isRotated ? panelType.heightMeters : panelType.widthMeters;
|
|
1848
|
+
// const panelHeight = isRotated ? panelType.widthMeters : panelType.heightMeters;
|
|
1849
|
+
// // Calculate row direction based on azimuth
|
|
1850
|
+
// const rowAngle = perpendicularAzimuth * (Math.PI / 180);
|
|
1851
|
+
// const rowDirX = Math.cos(rowAngle);
|
|
1852
|
+
// const rowDirY = Math.sin(rowAngle);
|
|
1853
|
+
// // Calculate panel spacing direction based on azimuth
|
|
1854
|
+
// const panelAngle = azimuth * (Math.PI / 180);
|
|
1855
|
+
// const panelDirX = Math.cos(panelAngle);
|
|
1856
|
+
// const panelDirY = Math.sin(panelAngle);
|
|
1857
|
+
// // Calculate row width (distance from start to end of roof in perpendicular direction)
|
|
1858
|
+
// let maxRowWidth = 0;
|
|
1859
|
+
// for (const point of roof.points) {
|
|
1860
|
+
// const dx = point.x - startPoint.x;
|
|
1861
|
+
// const dy = point.y - startPoint.y;
|
|
1862
|
+
// const width = Math.abs(dx * rowDirX + dy * rowDirY);
|
|
1863
|
+
// maxRowWidth = Math.max(maxRowWidth, width);
|
|
1864
|
+
// }
|
|
1865
|
+
// // Place panels along the row
|
|
1866
|
+
// const numPanels = Math.floor(maxRowWidth / panelWidth);
|
|
1867
|
+
// for (let i = 0; i < numPanels; i++) {
|
|
1868
|
+
// const panelX = startPoint.x + i * panelWidth * rowDirX;
|
|
1869
|
+
// const panelY = startPoint.y + i * panelWidth * rowDirY;
|
|
1870
|
+
// // Check if panel center is within roof polygon
|
|
1871
|
+
// const panelCenter = {
|
|
1872
|
+
// x: panelX + (panelWidth / 2) * rowDirX + (panelHeight / 2) * panelDirX,
|
|
1873
|
+
// y: panelY + (panelWidth / 2) * rowDirY + (panelHeight / 2) * panelDirY
|
|
1874
|
+
// };
|
|
1875
|
+
// if (isPointInPolygon(panelCenter, roof)) {
|
|
1876
|
+
// rowPanels.push({
|
|
1877
|
+
// panel: panelType,
|
|
1878
|
+
// pixelPosition: {
|
|
1879
|
+
// x: panelCenter.x,
|
|
1880
|
+
// y: panelCenter.y
|
|
1881
|
+
// }
|
|
1882
|
+
// });
|
|
1883
|
+
// }
|
|
1884
|
+
// }
|
|
1885
|
+
// return rowPanels;
|
|
1886
|
+
// };
|
|
1887
|
+
// // Try each corner and arrangement
|
|
1888
|
+
// for (const corner of roof.points) {
|
|
1889
|
+
// // Try all four arrangements for this corner
|
|
1890
|
+
// const arrangements = [
|
|
1891
|
+
// { isRotated: false, isAbove: false },
|
|
1892
|
+
// { isRotated: false, isAbove: true },
|
|
1893
|
+
// { isRotated: true, isAbove: false },
|
|
1894
|
+
// { isRotated: true, isAbove: true }
|
|
1895
|
+
// ];
|
|
1896
|
+
// for (const arrangement of arrangements) {
|
|
1897
|
+
// const currentPanels: PositionedSolarPanel[] = [];
|
|
1898
|
+
// const { isRotated, isAbove } = arrangement;
|
|
1899
|
+
// // Create initial row
|
|
1900
|
+
// const initialRow = createPanelRow(corner, isRotated, isAbove);
|
|
1901
|
+
// currentPanels.push(...initialRow);
|
|
1902
|
+
// // Add rows above and below
|
|
1903
|
+
// const panelHeight = isRotated ? panelType.widthMeters : panelType.heightMeters;
|
|
1904
|
+
// const rowSpacing = panelHeight + borderOffset;
|
|
1905
|
+
// // Calculate direction for adding rows
|
|
1906
|
+
// const rowAngle = azimuth * (Math.PI / 180);
|
|
1907
|
+
// const rowDirX = Math.cos(rowAngle);
|
|
1908
|
+
// const rowDirY = Math.sin(rowAngle);
|
|
1909
|
+
// // Add rows in both directions
|
|
1910
|
+
// let rowOffset = rowSpacing;
|
|
1911
|
+
// while (true) {
|
|
1912
|
+
// const newRowPoint = {
|
|
1913
|
+
// x: corner.x + rowOffset * rowDirX,
|
|
1914
|
+
// y: corner.y + rowOffset * rowDirY
|
|
1915
|
+
// };
|
|
1916
|
+
// const newRow = createPanelRow(newRowPoint, isRotated, isAbove);
|
|
1917
|
+
// if (newRow.length === 0) break;
|
|
1918
|
+
// currentPanels.push(...newRow);
|
|
1919
|
+
// rowOffset += rowSpacing;
|
|
1920
|
+
// }
|
|
1921
|
+
// rowOffset = -rowSpacing;
|
|
1922
|
+
// while (true) {
|
|
1923
|
+
// const newRowPoint = {
|
|
1924
|
+
// x: corner.x + rowOffset * rowDirX,
|
|
1925
|
+
// y: corner.y + rowOffset * rowDirY
|
|
1926
|
+
// };
|
|
1927
|
+
// const newRow = createPanelRow(newRowPoint, isRotated, isAbove);
|
|
1928
|
+
// if (newRow.length === 0) break;
|
|
1929
|
+
// currentPanels.push(...newRow);
|
|
1930
|
+
// rowOffset -= rowSpacing;
|
|
1931
|
+
// }
|
|
1932
|
+
// // Update best arrangement if this one has more panels
|
|
1933
|
+
// if (currentPanels.length > bestPanelCount) {
|
|
1934
|
+
// bestPanelCount = currentPanels.length;
|
|
1935
|
+
// bestPanelArrangement = currentPanels;
|
|
1936
|
+
// }
|
|
1937
|
+
// }
|
|
1938
|
+
// }
|
|
1939
|
+
// return bestPanelArrangement;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Uses the bounding box of the roof and the roof segment bounding boxes to find the best fitting roof segment.
|
|
1943
|
+
* @param roof the roof shape
|
|
1944
|
+
* @param roofSegmentStats the stats of the roof segments
|
|
1945
|
+
* @returns the best fitting roof segment
|
|
1946
|
+
*/
|
|
1947
|
+
function getBestFittingRoofSegment(roof, roofSegmentStats, bounds, canvas) {
|
|
1948
|
+
const polygonBounds = {
|
|
1949
|
+
minX: Math.min(...roof.points.map((p) => p.x)),
|
|
1950
|
+
maxX: Math.max(...roof.points.map((p) => p.x)),
|
|
1951
|
+
minY: Math.min(...roof.points.map((p) => p.y)),
|
|
1952
|
+
maxY: Math.max(...roof.points.map((p) => p.y)),
|
|
1953
|
+
};
|
|
1954
|
+
let bestSegment = null;
|
|
1955
|
+
let bestIoU = 0;
|
|
1956
|
+
for (const segment of roofSegmentStats) {
|
|
1957
|
+
const sw = latLngToPixel(bounds, canvas, {
|
|
1958
|
+
latitude: segment.boundingBox.sw.latitude,
|
|
1959
|
+
longitude: segment.boundingBox.sw.longitude,
|
|
1960
|
+
});
|
|
1961
|
+
const ne = latLngToPixel(bounds, canvas, {
|
|
1962
|
+
latitude: segment.boundingBox.ne.latitude,
|
|
1963
|
+
longitude: segment.boundingBox.ne.longitude,
|
|
1964
|
+
});
|
|
1965
|
+
const segmentBounds = {
|
|
1966
|
+
minX: sw.x,
|
|
1967
|
+
maxX: ne.x,
|
|
1968
|
+
minY: ne.y,
|
|
1969
|
+
maxY: sw.y,
|
|
1970
|
+
};
|
|
1971
|
+
// Calculate intersection
|
|
1972
|
+
const interLeft = Math.max(polygonBounds.minX, segmentBounds.minX);
|
|
1973
|
+
const interRight = Math.min(polygonBounds.maxX, segmentBounds.maxX);
|
|
1974
|
+
const interTop = Math.max(polygonBounds.minY, segmentBounds.minY);
|
|
1975
|
+
const interBottom = Math.min(polygonBounds.maxY, segmentBounds.maxY);
|
|
1976
|
+
const interWidth = interRight - interLeft;
|
|
1977
|
+
const interHeight = interBottom - interTop;
|
|
1978
|
+
let intersectionArea = 0;
|
|
1979
|
+
if (interWidth > 0 && interHeight > 0) {
|
|
1980
|
+
intersectionArea = interWidth * interHeight;
|
|
1981
|
+
}
|
|
1982
|
+
const polygonArea = (polygonBounds.maxX - polygonBounds.minX) * (polygonBounds.maxY - polygonBounds.minY);
|
|
1983
|
+
const segmentArea = (segmentBounds.maxX - segmentBounds.minX) * (segmentBounds.maxY - segmentBounds.minY);
|
|
1984
|
+
const unionArea = polygonArea + segmentArea - intersectionArea;
|
|
1985
|
+
const iou = unionArea > 0 ? intersectionArea / unionArea : 0;
|
|
1986
|
+
if (iou > bestIoU) {
|
|
1987
|
+
bestIoU = iou;
|
|
1988
|
+
bestSegment = segment;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return bestSegment;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function generateGrid(minMax, solarPanel, horizontal, anker, rowSpacing, columnSpacing) {
|
|
1995
|
+
const grid = [];
|
|
1996
|
+
const width = horizontal ? solarPanel.heightMeters : solarPanel.widthMeters;
|
|
1997
|
+
const widthCount = Math.ceil((minMax.maxX - minMax.minX) / width);
|
|
1998
|
+
const height = horizontal ? solarPanel.widthMeters : solarPanel.heightMeters;
|
|
1999
|
+
const heightCount = Math.ceil((minMax.maxY - minMax.minY) / height);
|
|
2000
|
+
const widthOffset = width / 2;
|
|
2001
|
+
const heightOffset = height / 2;
|
|
2002
|
+
// calculate the starting point based on the anker
|
|
2003
|
+
const startX = anker.x - (Math.ceil((anker.x - minMax.minX) / width) * width);
|
|
2004
|
+
const startY = anker.y - (Math.ceil((anker.y - minMax.minY) / height) * height);
|
|
2005
|
+
for (let i = 0; i < heightCount; i++) {
|
|
2006
|
+
const row = [];
|
|
2007
|
+
for (let j = 0; j < widthCount; j++) {
|
|
2008
|
+
const x = startX + j * width + j * columnSpacing + widthOffset;
|
|
2009
|
+
const y = startY + i * height + i * rowSpacing + heightOffset;
|
|
2010
|
+
const positionedSolarPanel = {
|
|
2011
|
+
panel: solarPanel,
|
|
2012
|
+
pixelPosition: {
|
|
2013
|
+
x: x,
|
|
2014
|
+
y: y
|
|
2015
|
+
},
|
|
2016
|
+
horizontal: horizontal
|
|
2017
|
+
};
|
|
2018
|
+
row.push(positionedSolarPanel);
|
|
2019
|
+
}
|
|
2020
|
+
grid.push(row);
|
|
2021
|
+
}
|
|
2022
|
+
return grid;
|
|
2023
|
+
}
|
|
2024
|
+
function getSolarPanelLines(positionedSolarPanel, horizontal) {
|
|
2025
|
+
const width = horizontal ? positionedSolarPanel.panel.heightMeters : positionedSolarPanel.panel.widthMeters;
|
|
2026
|
+
const height = horizontal ? positionedSolarPanel.panel.widthMeters : positionedSolarPanel.panel.heightMeters;
|
|
2027
|
+
const p1 = {
|
|
2028
|
+
x: positionedSolarPanel.pixelPosition.x - width / 2,
|
|
2029
|
+
y: positionedSolarPanel.pixelPosition.y - height / 2
|
|
2030
|
+
};
|
|
2031
|
+
const p2 = {
|
|
2032
|
+
x: positionedSolarPanel.pixelPosition.x - width / 2,
|
|
2033
|
+
y: positionedSolarPanel.pixelPosition.y + height / 2
|
|
2034
|
+
};
|
|
2035
|
+
const p3 = {
|
|
2036
|
+
x: positionedSolarPanel.pixelPosition.x + width / 2,
|
|
2037
|
+
y: positionedSolarPanel.pixelPosition.y + height / 2
|
|
2038
|
+
};
|
|
2039
|
+
const p4 = {
|
|
2040
|
+
x: positionedSolarPanel.pixelPosition.x + width / 2,
|
|
2041
|
+
y: positionedSolarPanel.pixelPosition.y - height / 2
|
|
2042
|
+
};
|
|
2043
|
+
const lines = [
|
|
2044
|
+
{
|
|
2045
|
+
start: p1,
|
|
2046
|
+
end: p2
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
start: p2,
|
|
2050
|
+
end: p3
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
start: p3,
|
|
2054
|
+
end: p4
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
start: p4,
|
|
2058
|
+
end: p1
|
|
2059
|
+
}
|
|
2060
|
+
];
|
|
2061
|
+
return lines;
|
|
2062
|
+
}
|
|
2063
|
+
function solarPanelInObstacle(positionedSolarPanel, obstacle, horizontal) {
|
|
2064
|
+
const lines = getSolarPanelLines(positionedSolarPanel, horizontal);
|
|
2065
|
+
if (isPointInPolygon(positionedSolarPanel.pixelPosition, obstacle)) {
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
for (let i = 0; i < obstacle.points.length; i++) {
|
|
2069
|
+
const point = obstacle.points[i];
|
|
2070
|
+
const nextPoint = obstacle.points[(i + 1) % obstacle.points.length];
|
|
2071
|
+
const border = {
|
|
2072
|
+
start: point,
|
|
2073
|
+
end: nextPoint
|
|
2074
|
+
};
|
|
2075
|
+
for (const line of lines) {
|
|
2076
|
+
if (intersects(line, border)) {
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
function solarPanelInPolygon(positionedSolarPanel, polygon, horizontal) {
|
|
2084
|
+
const lines = getSolarPanelLines(positionedSolarPanel, horizontal);
|
|
2085
|
+
for (let i = 0; i < polygon.points.length; i++) {
|
|
2086
|
+
const point = polygon.points[i];
|
|
2087
|
+
const nextPoint = polygon.points[(i + 1) % polygon.points.length];
|
|
2088
|
+
const border = {
|
|
2089
|
+
start: point,
|
|
2090
|
+
end: nextPoint
|
|
2091
|
+
};
|
|
2092
|
+
for (const line of lines) {
|
|
2093
|
+
if (intersects(line, border)) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return isPointInPolygon(positionedSolarPanel.pixelPosition, polygon);
|
|
2099
|
+
}
|
|
2100
|
+
function isPanelObstructed(positionedSolarPanel, obstacles, horizontal) {
|
|
2101
|
+
for (const obstacle of obstacles) {
|
|
2102
|
+
if (solarPanelInObstacle(positionedSolarPanel, obstacle.polygon, horizontal)) {
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
function getSolarPanelsInPolygon(grid, polygon, horizontal, obstacles) {
|
|
2109
|
+
const solarPanels = [];
|
|
2110
|
+
for (const row of grid) {
|
|
2111
|
+
let newRow = [];
|
|
2112
|
+
for (const positionedSolarPanel of row) {
|
|
2113
|
+
const isObstructed = isPanelObstructed(positionedSolarPanel, obstacles, horizontal);
|
|
2114
|
+
if (solarPanelInPolygon(positionedSolarPanel, polygon, horizontal) && !isObstructed) {
|
|
2115
|
+
newRow.push(positionedSolarPanel);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
solarPanels.push(newRow);
|
|
2119
|
+
}
|
|
2120
|
+
return solarPanels;
|
|
2121
|
+
}
|
|
2122
|
+
function getOptimalSolarPosition(roof, obstacles, solarPanel, angle) {
|
|
2123
|
+
const minX = Math.min(...roof.points.map(p => p.x));
|
|
2124
|
+
const maxX = Math.max(...roof.points.map(p => p.x));
|
|
2125
|
+
const minY = Math.min(...roof.points.map(p => p.y));
|
|
2126
|
+
const maxY = Math.max(...roof.points.map(p => p.y));
|
|
2127
|
+
const minMax = {
|
|
2128
|
+
minX: minX,
|
|
2129
|
+
maxX: maxX,
|
|
2130
|
+
minY: minY,
|
|
2131
|
+
maxY: maxY
|
|
2132
|
+
};
|
|
2133
|
+
console.log("solar", solarPanel);
|
|
2134
|
+
const horizontalSolarPanel = {
|
|
2135
|
+
...solarPanel,
|
|
2136
|
+
widthMeters: solarPanel.widthMeters * Math.cos(angle * Math.PI / 180),
|
|
2137
|
+
heightMeters: solarPanel.heightMeters
|
|
2138
|
+
};
|
|
2139
|
+
const verticalSolarPanel = {
|
|
2140
|
+
...solarPanel,
|
|
2141
|
+
widthMeters: solarPanel.widthMeters,
|
|
2142
|
+
heightMeters: solarPanel.heightMeters * Math.cos(angle * Math.PI / 180)
|
|
2143
|
+
};
|
|
2144
|
+
let solarPanels = [];
|
|
2145
|
+
let numberOfPanels = 0;
|
|
2146
|
+
for (const point of roof.points) {
|
|
2147
|
+
for (const horizontal of [false]) { // TODO removed true
|
|
2148
|
+
const grid = generateGrid(minMax, horizontal ? horizontalSolarPanel : verticalSolarPanel, horizontal, point, ROW_SPACING, COLUMN_SPACING);
|
|
2149
|
+
const solarPanelsInPolygon = getSolarPanelsInPolygon(grid, roof, horizontal, obstacles);
|
|
2150
|
+
const numberOfPanelsInPolygon = solarPanelsInPolygon.reduce((acc, row) => acc + row.length, 0);
|
|
2151
|
+
if (numberOfPanelsInPolygon > numberOfPanels) {
|
|
2152
|
+
numberOfPanels = numberOfPanelsInPolygon;
|
|
2153
|
+
solarPanels = solarPanelsInPolygon;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
return solarPanels;
|
|
2159
|
+
}
|
|
2160
|
+
function projectObstacles(obstacles, azimuth, offset, inset) {
|
|
2161
|
+
const result = [];
|
|
2162
|
+
for (const obstacle of obstacles) {
|
|
2163
|
+
const offsetObstacle = offsetPolygon(obstacle, offset);
|
|
2164
|
+
const projectedObstacle = projectPolygon(offsetObstacle.polygon, -azimuth);
|
|
2165
|
+
const insetObstacle = insetPolygon(projectedObstacle, -inset);
|
|
2166
|
+
const minX = Math.min(...insetObstacle.points.map(p => p.x));
|
|
2167
|
+
const maxX = Math.max(...insetObstacle.points.map(p => p.x));
|
|
2168
|
+
const minY = Math.min(...insetObstacle.points.map(p => p.y));
|
|
2169
|
+
const maxY = Math.max(...insetObstacle.points.map(p => p.y));
|
|
2170
|
+
const boundingBox = {
|
|
2171
|
+
minX: minX,
|
|
2172
|
+
maxX: maxX,
|
|
2173
|
+
minY: minY,
|
|
2174
|
+
maxY: maxY
|
|
2175
|
+
};
|
|
2176
|
+
result.push({
|
|
2177
|
+
polygon: projectedObstacle,
|
|
2178
|
+
boundingBox: boundingBox
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
return result;
|
|
2182
|
+
}
|
|
2183
|
+
function isClockwise(points) {
|
|
2184
|
+
let sum = 0;
|
|
2185
|
+
for (let i = 0; i < points.length; i++) {
|
|
2186
|
+
const curr = points[i];
|
|
2187
|
+
const next = points[(i + 1) % points.length];
|
|
2188
|
+
sum += (next.x - curr.x) * (next.y + curr.y);
|
|
2189
|
+
}
|
|
2190
|
+
return sum > 0;
|
|
2191
|
+
}
|
|
2192
|
+
function insetPolygon(polygon, inset) {
|
|
2193
|
+
const points = polygon.points;
|
|
2194
|
+
const isClockwisePolygon = isClockwise(points);
|
|
2195
|
+
inset = inset * Math.sqrt(2) * (isClockwisePolygon ? -1 : 1);
|
|
2196
|
+
const insetPoints = [];
|
|
2197
|
+
for (let i = 0; i < points.length; i++) {
|
|
2198
|
+
const prev = points[(i - 1 + points.length) % points.length];
|
|
2199
|
+
const curr = points[i];
|
|
2200
|
+
const next = points[(i + 1) % points.length];
|
|
2201
|
+
// Calculate vectors for the two edges meeting at current point
|
|
2202
|
+
const v1 = {
|
|
2203
|
+
x: curr.x - prev.x,
|
|
2204
|
+
y: curr.y - prev.y
|
|
2205
|
+
};
|
|
2206
|
+
const v2 = {
|
|
2207
|
+
x: next.x - curr.x,
|
|
2208
|
+
y: next.y - curr.y
|
|
2209
|
+
};
|
|
2210
|
+
// Calculate perpendicular vectors (90 degrees clockwise)
|
|
2211
|
+
const perp1 = {
|
|
2212
|
+
x: -v1.y,
|
|
2213
|
+
y: v1.x
|
|
2214
|
+
};
|
|
2215
|
+
const perp2 = {
|
|
2216
|
+
x: -v2.y,
|
|
2217
|
+
y: v2.x
|
|
2218
|
+
};
|
|
2219
|
+
// Normalize the perpendicular vectors
|
|
2220
|
+
const length1 = Math.sqrt(perp1.x * perp1.x + perp1.y * perp1.y);
|
|
2221
|
+
const length2 = Math.sqrt(perp2.x * perp2.x + perp2.y * perp2.y);
|
|
2222
|
+
perp1.x /= length1;
|
|
2223
|
+
perp1.y /= length1;
|
|
2224
|
+
perp2.x /= length2;
|
|
2225
|
+
perp2.y /= length2;
|
|
2226
|
+
// Calculate the bisector vector
|
|
2227
|
+
const bisector = {
|
|
2228
|
+
x: perp1.x + perp2.x,
|
|
2229
|
+
y: perp1.y + perp2.y
|
|
2230
|
+
};
|
|
2231
|
+
// Normalize the bisector
|
|
2232
|
+
const bisectorLength = Math.sqrt(bisector.x * bisector.x + bisector.y * bisector.y);
|
|
2233
|
+
bisector.x /= bisectorLength;
|
|
2234
|
+
bisector.y /= bisectorLength;
|
|
2235
|
+
// Calculate the inset point
|
|
2236
|
+
const insetPoint = {
|
|
2237
|
+
x: curr.x + bisector.x * inset,
|
|
2238
|
+
y: curr.y + bisector.y * inset
|
|
2239
|
+
};
|
|
2240
|
+
insetPoints.push(insetPoint);
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
points: insetPoints,
|
|
2244
|
+
type: polygon.type,
|
|
2245
|
+
closed: polygon.closed,
|
|
2246
|
+
_id: polygon._id,
|
|
2247
|
+
details: polygon.details
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function getOptimalSolarPositionFully(roof, obstacles, solarPanel, azimuth, inset, angle) {
|
|
2251
|
+
const insetRoof = insetPolygon(roof, inset);
|
|
2252
|
+
const offset = offsetPolygon(insetRoof);
|
|
2253
|
+
const projectedOffset = projectPolygon(offset.polygon, -azimuth);
|
|
2254
|
+
const projectedObstacles = projectObstacles(obstacles, azimuth, offset.offset, inset);
|
|
2255
|
+
const horizontalSolarPanels = getOptimalSolarPosition(projectedOffset, projectedObstacles, solarPanel, angle);
|
|
2256
|
+
const unprojectedPanels = [];
|
|
2257
|
+
for (const panel of horizontalSolarPanels.flat()) {
|
|
2258
|
+
const unprojectedPanel = projectPoint(panel.pixelPosition, azimuth);
|
|
2259
|
+
const offsetPosition = {
|
|
2260
|
+
x: unprojectedPanel.x + offset.offset.x,
|
|
2261
|
+
y: unprojectedPanel.y + offset.offset.y
|
|
2262
|
+
};
|
|
2263
|
+
unprojectedPanels.push({
|
|
2264
|
+
panel: panel.panel,
|
|
2265
|
+
pixelPosition: offsetPosition,
|
|
2266
|
+
horizontal: panel.horizontal
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
return unprojectedPanels;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const outputCss = "/*! tailwindcss v4.1.7 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-white: #fff;\n --spacing: 0.25rem;\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --radius-4xl: 2rem;\n --animate-spin: spin 1s linear infinite;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1 {\n top: calc(var(--spacing) * 1);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-3 {\n left: calc(var(--spacing) * 3);\n }\n .z-1 {\n z-index: 1;\n }\n .z-10 {\n z-index: 10;\n }\n .z-20 {\n z-index: 20;\n }\n .m-auto {\n margin: auto;\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .mb-6 {\n margin-bottom: calc(var(--spacing) * 6);\n }\n .flex {\n display: flex;\n }\n .grid {\n display: grid;\n }\n .table {\n display: table;\n }\n .h-16 {\n height: calc(var(--spacing) * 16);\n }\n .h-full {\n height: 100%;\n }\n .w-16 {\n width: calc(var(--spacing) * 16);\n }\n .w-52 {\n width: calc(var(--spacing) * 52);\n }\n .w-full {\n width: 100%;\n }\n .flex-1 {\n flex: 1;\n }\n .border-collapse {\n border-collapse: collapse;\n }\n .-translate-y-1 {\n --tw-translate-y: calc(var(--spacing) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .resize {\n resize: both;\n }\n .flex-col {\n flex-direction: column;\n }\n .flex-row {\n flex-direction: row;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-start {\n justify-content: flex-start;\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .space-y-4 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-4xl {\n border-radius: var(--radius-4xl);\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t-2 {\n border-top-style: var(--tw-border-style);\n border-top-width: 2px;\n }\n .border-b-2 {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 2px;\n }\n .border-\\[\\#271200\\] {\n border-color: #271200;\n }\n .border-gray-300 {\n border-color: var(--color-gray-300);\n }\n .bg-\\[\\#271200\\] {\n background-color: #271200;\n }\n .bg-\\[\\#f3ebda\\] {\n background-color: #f3ebda;\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .p-3 {\n padding: calc(var(--spacing) * 3);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .px-2 {\n padding-inline: calc(var(--spacing) * 2);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .pt-7 {\n padding-top: calc(var(--spacing) * 7);\n }\n .pr-4 {\n padding-right: calc(var(--spacing) * 4);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pl-10 {\n padding-left: calc(var(--spacing) * 10);\n }\n .text-center {\n text-align: center;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-600 {\n color: var(--color-gray-600);\n }\n .text-white {\n color: var(--color-white);\n }\n .underline {\n text-decoration-line: underline;\n }\n .shadow {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .outline {\n outline-style: var(--tw-outline-style);\n outline-width: 1px;\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .hover\\:bg-\\[\\#964500\\] {\n &:hover {\n @media (hover: hover) {\n background-color: #964500;\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-blue-500 {\n &:focus {\n --tw-ring-color: var(--color-blue-500);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-outline-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-font-weight: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-outline-style: solid;\n --tw-duration: initial;\n }\n }\n}\n";
|
|
2273
|
+
|
|
2274
|
+
const FACTOR = 1;
|
|
2275
|
+
const MapDraw = class {
|
|
2276
|
+
constructor(hostRef) {
|
|
2277
|
+
registerInstance(this, hostRef);
|
|
2278
|
+
}
|
|
2279
|
+
apiKey = "";
|
|
2280
|
+
latitude = null;
|
|
2281
|
+
longitude = null;
|
|
2282
|
+
config = DEFAULT_SOLAR_EXPERT_CONFIG;
|
|
2283
|
+
solarPanel = DEFAULT_SOLAR_PANEL_TYPE;
|
|
2284
|
+
zoom = 1;
|
|
2285
|
+
loadingState = "empty";
|
|
2286
|
+
rgbTiff = null;
|
|
2287
|
+
draggedPointIndex = null;
|
|
2288
|
+
hoveredPointIndex = null;
|
|
2289
|
+
hoveredPolygon = null;
|
|
2290
|
+
shiftKeyPressed = false;
|
|
2291
|
+
altKeyPressed = false;
|
|
2292
|
+
mousePoint = null;
|
|
2293
|
+
currentTool = roofTool;
|
|
2294
|
+
roofPolygons = {};
|
|
2295
|
+
roofPolygonsSolarPanels = {};
|
|
2296
|
+
obstructionPolygons = {};
|
|
2297
|
+
selectedPolygon = null;
|
|
2298
|
+
buildingInsights = null;
|
|
2299
|
+
pixelInMeters = 0.2;
|
|
2300
|
+
showAzimuth = true; // TODO: make this false
|
|
2301
|
+
get el() { return getElement(this); }
|
|
2302
|
+
canvasElement;
|
|
2303
|
+
polygonCanvas;
|
|
2304
|
+
polygonCtx;
|
|
2305
|
+
getCurrentPolygon() {
|
|
2306
|
+
if (!this.selectedPolygon) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
const { _id, type } = this.selectedPolygon;
|
|
2310
|
+
return type === "roof"
|
|
2311
|
+
? this.roofPolygons[_id]
|
|
2312
|
+
: this.obstructionPolygons[_id];
|
|
2313
|
+
}
|
|
2314
|
+
componentDidLoad() {
|
|
2315
|
+
if (this.latitude && this.longitude) {
|
|
2316
|
+
this.loadingState = "loading";
|
|
2317
|
+
}
|
|
2318
|
+
requestAnimationFrame(() => {
|
|
2319
|
+
const rect = this.canvasElement.getBoundingClientRect();
|
|
2320
|
+
if (this.canvasElement) {
|
|
2321
|
+
this.canvasElement.width = rect.width;
|
|
2322
|
+
this.canvasElement.height = rect.height;
|
|
2323
|
+
this.drawMap();
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
sizeCanvas(width, height) {
|
|
2328
|
+
if (this.polygonCanvas) {
|
|
2329
|
+
this.polygonCanvas.width = width;
|
|
2330
|
+
this.polygonCanvas.height = height;
|
|
2331
|
+
this.polygonCtx = this.polygonCanvas.getContext("2d");
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async getBuildingInsights() {
|
|
2335
|
+
if (!this.latitude || !this.longitude) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (this.buildingInsights) {
|
|
2339
|
+
const sameLocation = this.buildingInsights.center.latitude === this.latitude &&
|
|
2340
|
+
this.buildingInsights.center.longitude === this.longitude;
|
|
2341
|
+
if (sameLocation) {
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
this.obstructionPolygons = {};
|
|
2345
|
+
this.roofPolygons = {};
|
|
2346
|
+
this.selectedPolygon = null;
|
|
2347
|
+
this.hoveredPolygon = null;
|
|
2348
|
+
this.canvasElement.getContext("2d")?.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
|
|
2349
|
+
this.loadingState = "loading";
|
|
2350
|
+
}
|
|
2351
|
+
this.buildingInsights = await fetchSolarData(this.latitude, this.longitude, this.apiKey);
|
|
2352
|
+
if (!this.buildingInsights) {
|
|
2353
|
+
alert("No building insights found. Please enter them manually.");
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
async getBuildingImages() {
|
|
2357
|
+
if (!this.latitude || !this.longitude) {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
if (this.buildingInsights) {
|
|
2361
|
+
const sameLocation = this.buildingInsights.center.latitude === this.latitude &&
|
|
2362
|
+
this.buildingInsights.center.longitude === this.longitude;
|
|
2363
|
+
if (sameLocation) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
this.rgbTiff = await getBuildingImages(this.latitude, this.longitude, this.apiKey);
|
|
2368
|
+
this.pixelInMeters = getPixelInMeters(this.rgbTiff);
|
|
2369
|
+
// this.drawMap();
|
|
2370
|
+
}
|
|
2371
|
+
async drawMap() {
|
|
2372
|
+
if (!this.canvasElement || !this.rgbTiff || !this.buildingInsights)
|
|
2373
|
+
return;
|
|
2374
|
+
this.sizeCanvas(this.rgbTiff.width, this.rgbTiff.height);
|
|
2375
|
+
renderCombinedWithZoom({
|
|
2376
|
+
rgb: this.rgbTiff,
|
|
2377
|
+
zoom: this.zoom,
|
|
2378
|
+
canvas: this.canvasElement,
|
|
2379
|
+
});
|
|
2380
|
+
this.loadingState = "loaded";
|
|
2381
|
+
// this.polygonCanvas.width = this.canvasElement.width;
|
|
2382
|
+
// this.polygonCanvas.height = this.canvasElement.height;
|
|
2383
|
+
}
|
|
2384
|
+
handleMouseMove(event) {
|
|
2385
|
+
if (!this.polygonCanvas || !this.polygonCtx)
|
|
2386
|
+
return;
|
|
2387
|
+
const rect = this.polygonCanvas.getBoundingClientRect();
|
|
2388
|
+
const x = event.clientX - rect.left;
|
|
2389
|
+
const y = event.clientY - rect.top;
|
|
2390
|
+
this.mousePoint = { x, y };
|
|
2391
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2392
|
+
// Check for point hover
|
|
2393
|
+
const newHoveredPointIndex = currentPolygon?.points.findIndex((point) => {
|
|
2394
|
+
const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2));
|
|
2395
|
+
return distance < 10;
|
|
2396
|
+
});
|
|
2397
|
+
// Check for polygon hover
|
|
2398
|
+
let newHoveredPolygon = null;
|
|
2399
|
+
for (const polygon of Object.values(this.obstructionPolygons)) {
|
|
2400
|
+
if (isPointInPolygon({ x, y }, polygon)) {
|
|
2401
|
+
newHoveredPolygon = { _id: polygon._id, type: "obstruction" };
|
|
2402
|
+
break;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
// obstruction polygons get priority
|
|
2406
|
+
if (!newHoveredPolygon) {
|
|
2407
|
+
for (const polygon of Object.values(this.roofPolygons)) {
|
|
2408
|
+
if (isPointInPolygon({ x, y }, polygon)) {
|
|
2409
|
+
newHoveredPolygon = { _id: polygon._id, type: "roof" };
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
// Only redraw if hover state changed
|
|
2415
|
+
if (newHoveredPointIndex !== this.hoveredPointIndex ||
|
|
2416
|
+
JSON.stringify(newHoveredPolygon) !==
|
|
2417
|
+
JSON.stringify(this.hoveredPolygon)) {
|
|
2418
|
+
this.hoveredPointIndex = newHoveredPointIndex;
|
|
2419
|
+
this.hoveredPolygon = newHoveredPolygon;
|
|
2420
|
+
this.drawPolygons();
|
|
2421
|
+
}
|
|
2422
|
+
// Handle dragging
|
|
2423
|
+
if (this.draggedPointIndex !== null && currentPolygon &&
|
|
2424
|
+
this.currentTool.name === "move") {
|
|
2425
|
+
currentPolygon.points[this.draggedPointIndex] = { x, y };
|
|
2426
|
+
this.drawPolygons();
|
|
2427
|
+
}
|
|
2428
|
+
if (this.shiftKeyPressed) {
|
|
2429
|
+
this.drawPolygons();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
drawPolygons() {
|
|
2433
|
+
if (!this.polygonCanvas || !this.polygonCtx)
|
|
2434
|
+
return;
|
|
2435
|
+
// Clear the canvas and redraw the map
|
|
2436
|
+
this.polygonCtx.clearRect(0, 0, this.polygonCanvas.width, this.polygonCanvas.height);
|
|
2437
|
+
// draw roof polygons
|
|
2438
|
+
for (const polygon of Object.values(this.roofPolygons)) {
|
|
2439
|
+
const fillColor = getFillColor(this.selectedPolygon, this.hoveredPolygon, polygon._id, polygon, this.config);
|
|
2440
|
+
drawPolygon({
|
|
2441
|
+
polygonCtx: this.polygonCtx,
|
|
2442
|
+
polygonCanvas: this.polygonCanvas,
|
|
2443
|
+
polygon,
|
|
2444
|
+
strokeColor: polygon.closed
|
|
2445
|
+
? this.config.closedRoofColor
|
|
2446
|
+
: this.config.openRoofColor,
|
|
2447
|
+
fillColor,
|
|
2448
|
+
pixelInMeters: this.pixelInMeters,
|
|
2449
|
+
shiftKeyPressed: this.shiftKeyPressed,
|
|
2450
|
+
mousePoint: this.mousePoint,
|
|
2451
|
+
});
|
|
2452
|
+
const solarPanels = this.roofPolygonsSolarPanels[polygon._id];
|
|
2453
|
+
const convertedSolarPanel = {
|
|
2454
|
+
...this.solarPanel,
|
|
2455
|
+
widthMeters: this.solarPanel.widthMeters / this.pixelInMeters,
|
|
2456
|
+
heightMeters: this.solarPanel.heightMeters / this.pixelInMeters,
|
|
2457
|
+
};
|
|
2458
|
+
if (solarPanels) {
|
|
2459
|
+
for (const panel of solarPanels) {
|
|
2460
|
+
renderSolarPanel(panel, this.polygonCtx, convertedSolarPanel.widthMeters, convertedSolarPanel.heightMeters, polygon.details?.azimuth, polygon.details?.pitch);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
// draw obstruction polygons
|
|
2465
|
+
for (const polygon of Object.values(this.obstructionPolygons)) {
|
|
2466
|
+
const fillColor = getFillColor(this.selectedPolygon, this.hoveredPolygon, polygon._id, polygon, this.config);
|
|
2467
|
+
drawPolygon({
|
|
2468
|
+
polygonCtx: this.polygonCtx,
|
|
2469
|
+
polygonCanvas: this.polygonCanvas,
|
|
2470
|
+
polygon,
|
|
2471
|
+
pixelInMeters: this.pixelInMeters,
|
|
2472
|
+
shiftKeyPressed: this.shiftKeyPressed,
|
|
2473
|
+
mousePoint: this.mousePoint,
|
|
2474
|
+
strokeColor: polygon.closed
|
|
2475
|
+
? this.config.closedObstructionColor
|
|
2476
|
+
: this.config.openObstructionColor,
|
|
2477
|
+
fillColor,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
// if (this.showAzimuth && this.selectedPolygon) {
|
|
2481
|
+
// const currentPolygon = this.getCurrentPolygon();
|
|
2482
|
+
// renderAzimuth(currentPolygon.details?.azimuth, this.polygonCtx);
|
|
2483
|
+
// }
|
|
2484
|
+
}
|
|
2485
|
+
deletePolygon(_id) {
|
|
2486
|
+
if (this.selectedPolygon?._id === _id) {
|
|
2487
|
+
this.selectedPolygon = null;
|
|
2488
|
+
}
|
|
2489
|
+
if (this.roofPolygons[_id]) {
|
|
2490
|
+
delete this.roofPolygons[_id];
|
|
2491
|
+
delete this.roofPolygonsSolarPanels[_id];
|
|
2492
|
+
}
|
|
2493
|
+
if (this.obstructionPolygons[_id]) {
|
|
2494
|
+
delete this.obstructionPolygons[_id];
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
handleKeyDown(event) {
|
|
2498
|
+
if (event.target instanceof HTMLInputElement) {
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
// Handle shift key for perpendicular points
|
|
2502
|
+
if (event.shiftKey && !this.shiftKeyPressed) {
|
|
2503
|
+
this.shiftKeyPressed = true;
|
|
2504
|
+
this.drawPolygons();
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (event.altKey && !this.altKeyPressed) {
|
|
2508
|
+
this.altKeyPressed = true;
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
if ((event.key === "Delete" || event.key === "Backspace") &&
|
|
2512
|
+
this.selectedPolygon) {
|
|
2513
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2514
|
+
if (currentPolygon.closed) {
|
|
2515
|
+
this.deletePolygon(currentPolygon._id);
|
|
2516
|
+
this.drawPolygons();
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
currentPolygon.points.pop();
|
|
2520
|
+
this.drawPolygons();
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
// Handle tool selection shortcuts
|
|
2524
|
+
const pressedKey = event.key.toLowerCase();
|
|
2525
|
+
const tool = tools.find((t) => t.keyboardShortcut?.toLowerCase() === pressedKey);
|
|
2526
|
+
if (tool) {
|
|
2527
|
+
this.currentTool = tool;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
handleKeyUp(event) {
|
|
2531
|
+
if (!event.shiftKey && this.shiftKeyPressed) {
|
|
2532
|
+
this.shiftKeyPressed = false;
|
|
2533
|
+
this.drawPolygons();
|
|
2534
|
+
}
|
|
2535
|
+
if (!event.altKey && this.altKeyPressed) {
|
|
2536
|
+
this.altKeyPressed = false;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
handleMouseDown(event) {
|
|
2540
|
+
if (!this.polygonCanvas || !this.polygonCtx)
|
|
2541
|
+
return;
|
|
2542
|
+
if (!this.polygonCanvas.contains(event.target))
|
|
2543
|
+
return;
|
|
2544
|
+
// get the mouse position in the polygon canvas
|
|
2545
|
+
const rect = this.polygonCanvas.getBoundingClientRect();
|
|
2546
|
+
let x = event.clientX - rect.left;
|
|
2547
|
+
let y = event.clientY - rect.top;
|
|
2548
|
+
if (this.currentTool.name === "delete") {
|
|
2549
|
+
if (!this.hoveredPolygon) {
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const { _id, type } = this.hoveredPolygon;
|
|
2553
|
+
if (type === "roof") {
|
|
2554
|
+
delete this.roofPolygons[_id];
|
|
2555
|
+
delete this.roofPolygonsSolarPanels[_id];
|
|
2556
|
+
}
|
|
2557
|
+
else if (type === "obstruction") {
|
|
2558
|
+
delete this.obstructionPolygons[_id];
|
|
2559
|
+
}
|
|
2560
|
+
this.hoveredPolygon = null;
|
|
2561
|
+
if (this.selectedPolygon?._id === _id) {
|
|
2562
|
+
this.selectedPolygon = null;
|
|
2563
|
+
}
|
|
2564
|
+
this.drawPolygons();
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (this.currentTool.name === "move") {
|
|
2568
|
+
// Check if we're clicking on a point of the selected polygon
|
|
2569
|
+
if (this.selectedPolygon) {
|
|
2570
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2571
|
+
const pointIndex = currentPolygon?.points.findIndex((point) => {
|
|
2572
|
+
const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2));
|
|
2573
|
+
return distance < 10;
|
|
2574
|
+
});
|
|
2575
|
+
if (pointIndex !== undefined && pointIndex !== -1) {
|
|
2576
|
+
this.draggedPointIndex = pointIndex;
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (this.hoveredPolygon) {
|
|
2580
|
+
this.selectedPolygon = this.hoveredPolygon;
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
this.selectedPolygon = null;
|
|
2585
|
+
this.drawPolygons();
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (!this.selectedPolygon || this.getCurrentPolygon()?.closed) {
|
|
2589
|
+
// no polygon is active, create a new one
|
|
2590
|
+
const newPolygon = {
|
|
2591
|
+
_id: v4(),
|
|
2592
|
+
points: [{ x, y }],
|
|
2593
|
+
type: this.currentTool.name,
|
|
2594
|
+
closed: false,
|
|
2595
|
+
};
|
|
2596
|
+
let _id;
|
|
2597
|
+
if (this.currentTool.name === "roof") {
|
|
2598
|
+
this.roofPolygons = {
|
|
2599
|
+
...this.roofPolygons,
|
|
2600
|
+
[newPolygon._id]: newPolygon,
|
|
2601
|
+
};
|
|
2602
|
+
_id = newPolygon._id;
|
|
2603
|
+
}
|
|
2604
|
+
else if (this.currentTool.name === "obstruction") {
|
|
2605
|
+
this.obstructionPolygons = {
|
|
2606
|
+
...this.obstructionPolygons,
|
|
2607
|
+
[newPolygon._id]: newPolygon,
|
|
2608
|
+
};
|
|
2609
|
+
_id = newPolygon._id;
|
|
2610
|
+
}
|
|
2611
|
+
this.selectedPolygon = { _id, type: this.currentTool.name };
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const { _id, type } = this.selectedPolygon;
|
|
2615
|
+
const currentPolygon = type === "roof"
|
|
2616
|
+
? this.roofPolygons[_id]
|
|
2617
|
+
: this.obstructionPolygons[_id];
|
|
2618
|
+
if (currentPolygon.closed) {
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
// If we have at least one point, check if we're clicking near the first point
|
|
2622
|
+
if (currentPolygon.points.length > 2) {
|
|
2623
|
+
const firstPoint = currentPolygon.points[0];
|
|
2624
|
+
const distance = Math.sqrt(Math.pow(x - firstPoint.x, 2) + Math.pow(y - firstPoint.y, 2));
|
|
2625
|
+
if (distance < 10) {
|
|
2626
|
+
this.closePolygon();
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
// Handle shift key for perpendicular points
|
|
2631
|
+
if (event.shiftKey && currentPolygon?.points.length >= 2) {
|
|
2632
|
+
const projectedPoint = projectPointPerpendicularToLine({
|
|
2633
|
+
x,
|
|
2634
|
+
y,
|
|
2635
|
+
lastPoint: currentPolygon.points[currentPolygon.points.length - 1],
|
|
2636
|
+
secondLastPoint: currentPolygon.points[currentPolygon.points.length - 2],
|
|
2637
|
+
});
|
|
2638
|
+
x = projectedPoint.x;
|
|
2639
|
+
y = projectedPoint.y;
|
|
2640
|
+
}
|
|
2641
|
+
currentPolygon.points = [...currentPolygon.points, { x, y }];
|
|
2642
|
+
this.drawPolygons();
|
|
2643
|
+
}
|
|
2644
|
+
handleMouseUp() {
|
|
2645
|
+
this.draggedPointIndex = null;
|
|
2646
|
+
}
|
|
2647
|
+
closePolygon() {
|
|
2648
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2649
|
+
if (currentPolygon.points.length >= 3 &&
|
|
2650
|
+
(this.currentTool.name === "roof" ||
|
|
2651
|
+
this.currentTool.name === "obstruction")) {
|
|
2652
|
+
currentPolygon.closed = true;
|
|
2653
|
+
}
|
|
2654
|
+
if (currentPolygon.type === "roof") {
|
|
2655
|
+
// Calculate basic polygon details
|
|
2656
|
+
const metersInPixels = 1 / this.pixelInMeters;
|
|
2657
|
+
const area = calculatePolygonArea(currentPolygon.points) /
|
|
2658
|
+
(metersInPixels * metersInPixels);
|
|
2659
|
+
const bestMatch = getBestFittingRoofSegment(currentPolygon, this.buildingInsights?.solarPotential.roofSegmentStats, this.rgbTiff.bounds, this.canvasElement);
|
|
2660
|
+
if (bestMatch) {
|
|
2661
|
+
currentPolygon.details = {
|
|
2662
|
+
area,
|
|
2663
|
+
azimuth: bestMatch.azimuthDegrees,
|
|
2664
|
+
pitch: bestMatch.pitchDegrees,
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
else {
|
|
2668
|
+
currentPolygon.details = {
|
|
2669
|
+
area,
|
|
2670
|
+
azimuth: 0,
|
|
2671
|
+
pitch: 0,
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
this.drawPolygons();
|
|
2676
|
+
this.currentTool = moveTool;
|
|
2677
|
+
}
|
|
2678
|
+
calculateSolarPanels() {
|
|
2679
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2680
|
+
if (!currentPolygon.details) {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (this.roofPolygonsSolarPanels[currentPolygon._id]) {
|
|
2684
|
+
delete this.roofPolygonsSolarPanels[currentPolygon._id];
|
|
2685
|
+
}
|
|
2686
|
+
const convertedSolarPanel = {
|
|
2687
|
+
...this.solarPanel,
|
|
2688
|
+
widthMeters: this.solarPanel.widthMeters / this.pixelInMeters,
|
|
2689
|
+
heightMeters: this.solarPanel.heightMeters / this.pixelInMeters,
|
|
2690
|
+
};
|
|
2691
|
+
const inset = BORDER_INSET / this.pixelInMeters;
|
|
2692
|
+
const solarPanels = getOptimalSolarPositionFully(currentPolygon, Object.values(this.obstructionPolygons), convertedSolarPanel, currentPolygon.details?.azimuth, inset, currentPolygon.details?.pitch);
|
|
2693
|
+
this.roofPolygonsSolarPanels = {
|
|
2694
|
+
...this.roofPolygonsSolarPanels,
|
|
2695
|
+
[currentPolygon._id]: solarPanels,
|
|
2696
|
+
};
|
|
2697
|
+
this.drawPolygons();
|
|
2698
|
+
}
|
|
2699
|
+
handlePitchChange(event) {
|
|
2700
|
+
console.log("handlePitchChange");
|
|
2701
|
+
const target = event.target;
|
|
2702
|
+
const pitch = parseFloat(target.value);
|
|
2703
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2704
|
+
if (currentPolygon) {
|
|
2705
|
+
this.roofPolygons[currentPolygon._id].details.pitch = pitch;
|
|
2706
|
+
}
|
|
2707
|
+
if (currentPolygon.type === "roof" &&
|
|
2708
|
+
this.roofPolygonsSolarPanels[currentPolygon._id]) {
|
|
2709
|
+
this.calculateSolarPanels();
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
handleAzimuthChange(event) {
|
|
2714
|
+
console.log("handleAzimuthChange");
|
|
2715
|
+
const target = event.target;
|
|
2716
|
+
const azimuth = parseFloat(target.value);
|
|
2717
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2718
|
+
if (currentPolygon) {
|
|
2719
|
+
this.roofPolygons[currentPolygon._id].details.azimuth = azimuth;
|
|
2720
|
+
}
|
|
2721
|
+
if (currentPolygon.type === "roof" &&
|
|
2722
|
+
this.roofPolygonsSolarPanels[currentPolygon._id]) {
|
|
2723
|
+
this.calculateSolarPanels();
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
handleToolSelect(tool) {
|
|
2728
|
+
this.currentTool = tool;
|
|
2729
|
+
}
|
|
2730
|
+
render() {
|
|
2731
|
+
const currentTool = tools.find((tool) => tool.name === this.currentTool.name);
|
|
2732
|
+
const currentPolygon = this.getCurrentPolygon();
|
|
2733
|
+
return (h("div", { key: '563fc00541c7988ceeae97faeeafa0ae98bb2595', class: "flex flex-col justify-center items-center w-full h-full gap-4" }, h("div", { key: '13c087a2b267360989df9165a065cba2f1e5b7a6', class: "flex gap-4 rounded-4xl bg-[#f3ebda] p-4" }, tools.map((tool) => (h("button", { class: `px-4 py-2 rounded-4xl transition-colors duration-200 hover:bg-[#964500] ${this.currentTool.name === tool.name
|
|
2734
|
+
? "bg-[#271200] text-white hover:bg-[#964500]"
|
|
2735
|
+
: "bg-[#f3ebda] hover:bg-[#964500]"}`, "aria-label": tool.ariaLabel, onClick: () => this.handleToolSelect(tool) }, h("i", { "data-lucide": tool.icon }))))), h("div", { key: 'ffc89d75efaf196cae646353a04846d83f93db25', class: "flex w-full gap-4 items-start justify-center h-full" }, h("div", { key: '476fada243140756e07c4a1f52b0e187e1ca8c8a', class: "w-52 bg-[#f3ebda] rounded-4xl p-4" }, h("h3", { key: '2342023f09347e69a4400f29afcdb397323c37bd', class: "text-lg font-semibold mb-4 text-center" }, "Information"), currentPolygon?.details
|
|
2736
|
+
? (h("div", { class: "space-y-4" }, h("div", null, h("h4", { class: "text-sm font-medium text-gray-600" }, "Area"), h("p", { class: "text-lg" }, currentPolygon.details?.area
|
|
2737
|
+
.toFixed(2), " m\u00B2")), h("div", null, h("h4", { class: "text-sm font-medium text-gray-600" }, "Orientation"), h("div", { class: "flex gap-2 flex-row justify-start" }, h("input", { class: "text-lg w-full", onKeyDown: (e) => {
|
|
2738
|
+
if (e.key === "Enter") {
|
|
2739
|
+
this.handleAzimuthChange(e);
|
|
2740
|
+
e.target
|
|
2741
|
+
.blur();
|
|
2742
|
+
}
|
|
2743
|
+
}, value: currentPolygon.details
|
|
2744
|
+
?.azimuth }), "(", azimuthToCardinal(currentPolygon.details
|
|
2745
|
+
?.azimuth), ")")), h("div", null, h("h4", { class: "text-sm font-medium text-gray-600" }, "Angle"), h("div", { class: "flex gap-2 flex-row justify-start" }, h("input", { class: "text-lg w-full", onKeyDown: (e) => {
|
|
2746
|
+
if (e.key === "Enter") {
|
|
2747
|
+
this.handlePitchChange(e);
|
|
2748
|
+
e.target
|
|
2749
|
+
.blur();
|
|
2750
|
+
}
|
|
2751
|
+
}, value: currentPolygon.details
|
|
2752
|
+
?.pitch }), "\u00B0")), h("div", { class: "flex flex-col gap-2" }, h("button", { class: "px-4 py-2 bg-[#271200] text-white rounded-4xl hover:bg-[#964500] transition-colors duration-200", onClick: () => this.calculateSolarPanels() }, "Calculate Solar Panels"), this
|
|
2753
|
+
.roofPolygonsSolarPanels[currentPolygon._id] && (h("p", { class: "text-lg text-center" }, this
|
|
2754
|
+
.roofPolygonsSolarPanels[currentPolygon._id].length, " panels")))))
|
|
2755
|
+
: (h("p", { class: "text-gray-500 text-center" }, "No polygon selected"))), h("div", { key: '0dd5c474e7aa42e62938a0161615bd91ec514f4b', class: "relative flex-1 w-full h-full rounded-4xl flex items-center justify-center", style: {
|
|
2756
|
+
width: `${this.rgbTiff?.width * FACTOR || 0}px`,
|
|
2757
|
+
height: `${this.rgbTiff?.height * FACTOR || 0}px`,
|
|
2758
|
+
} }, (this.latitude && this.longitude &&
|
|
2759
|
+
this.loadingState !== "loaded") && (h("div", { key: '426443edf9bb64b9be51213b831ff607455c2dbc', class: "absolute inset-0 flex items-center justify-center bg-white bg-opacity-75 z-20 pt-7 rounded-4xl" }, h("div", { key: '8c90cb0b16317fa32d66dd100a53c66d2eb17f17', class: "animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-[#271200]" }))), h("canvas", { key: '17902a4cd9e4ab7d1040051d6e020f4b157c3381', ref: (el) => this.canvasElement = el, class: `absolute top-0 left-0 z-1 rounded-4xl m-auto`, style: {
|
|
2760
|
+
cursor: this.currentTool.cursor,
|
|
2761
|
+
} }), h("canvas", { key: '33085ea829cd1a0fa41496e80ab8f1b4388d37a1', ref: (el) => this.polygonCanvas = el, class: `absolute top-0 left-0 z-10 rounded-4xl m-auto`, style: {
|
|
2762
|
+
cursor: this.currentTool.cursor,
|
|
2763
|
+
} })), h("div", { key: '5b7d1e538b3f7a0fd45382e185a25a1171ecf779', class: "w-52 bg-[#f3ebda] rounded-4xl p-4" }, h("h3", { key: '093369adeb939445fca2680fe4d2ebbe5f7d7a5f', class: "text-lg font-semibold mb-4 text-center" }, "Keyboard Shortcuts"), currentTool.keyboardCombinations && (h("div", { key: '405221a4095e148d121b63eba5fd91ba4b422d8e', class: "mb-6" }, h("h4", { key: '6f3c0a185e319631fa68bfb8c3ebdf25ab8d5f82', class: "text-sm font-medium text-gray-600 mb-2 text-center" }, currentTool.ariaLabel, " Tool"), currentTool.keyboardCombinations?.map((combo) => (h("div", { class: "flex items-center gap-2 mb-2" }, h("kbd", { class: "px-2 py-1 bg-white rounded text-sm" }, combo.key), h("span", { class: "text-sm text-gray-600" }, combo.description)))))), h("div", { key: 'edf0e2a52e0b8908dda0b9e63508e22b25e1cd59' }, h("h4", { key: 'df6f4eb73f425cdf20ca87f36f2665ef1b822f84', class: "text-sm font-medium text-gray-600 mb-2 text-center" }, "Tool Selection"), tools.map((tool) => (h("div", { class: "flex items-center gap-2 mb-2" }, h("kbd", { class: "px-2 py-1 bg-white rounded text-sm" }, tool.keyboardShortcut), h("span", { class: "text-sm text-gray-600" }, tool.ariaLabel)))))))));
|
|
2764
|
+
}
|
|
2765
|
+
static get watchers() { return {
|
|
2766
|
+
"latitude": ["getBuildingInsights", "getBuildingImages"],
|
|
2767
|
+
"longitude": ["getBuildingInsights", "getBuildingImages"],
|
|
2768
|
+
"rgbTiff": ["drawMap"],
|
|
2769
|
+
"buildingInsights": ["drawMap"]
|
|
2770
|
+
}; }
|
|
2771
|
+
};
|
|
2772
|
+
MapDraw.style = outputCss;
|
|
2773
|
+
|
|
2774
|
+
export { MapDraw as map_draw };
|
|
2775
|
+
//# sourceMappingURL=map-draw.entry.esm.js.map
|
|
2776
|
+
|
|
2777
|
+
//# sourceMappingURL=map-draw.entry.js.map
|