@necrolab/dashboard 0.5.22 → 0.5.23
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/package.json +1 -3
- package/src/components/Filter/Filter.vue +13 -0
- package/src/libs/Filter.js +89 -11
- package/src/libs/tm-renderer/axs/renderer.js +97 -0
- package/src/libs/tm-renderer/axs/requests.js +37 -0
- package/src/libs/tm-renderer/base-renderer.js +171 -0
- package/src/libs/tm-renderer/dependencies/logger.js +186 -0
- package/src/libs/tm-renderer/dependencies/node.persist.js +100 -0
- package/src/libs/tm-renderer/dependencies/persist.js +23 -0
- package/src/libs/tm-renderer/dependencies/web.persist.js +64 -0
- package/src/libs/tm-renderer/factory.js +47 -0
- package/src/libs/tm-renderer/index.js +21 -0
- package/src/libs/tm-renderer/request-utils.js +101 -0
- package/src/libs/tm-renderer/tm/renderer.js +568 -0
- package/src/libs/tm-renderer/tm/requests.js +47 -0
- package/src/libs/tm-renderer/tm/tm-utils.js +40 -0
- package/src/libs/tm-renderer/utils.js +90 -0
- package/src/views/FilterBuilder.vue +146 -56
- package/vite.config.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@necrolab/dashboard",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "rm -rf dist && vite build && npx workbox-cli generateSW workbox-config.cjs",
|
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
"bot": "node dev-server.js",
|
|
9
9
|
"expose": "node dev-server.js",
|
|
10
10
|
"postinstall": "node postinstall.js",
|
|
11
|
-
"updaterenderer": "npm i @necrolab/tm-renderer@latest",
|
|
12
11
|
"preview": "vite preview",
|
|
13
12
|
"lint": "npx eslint src/ --fix"
|
|
14
13
|
},
|
|
15
14
|
"dependencies": {
|
|
16
15
|
"@msgpack/msgpack": "^3.0.0-beta2",
|
|
17
|
-
"@necrolab/tm-renderer": "^0.1.12",
|
|
18
16
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
19
17
|
"@vueuse/core": "^11.3.0",
|
|
20
18
|
"autoprefixer": "^10.4.21",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
<div class="grid grid-cols-12 items-center py-3 px-2 sm:px-4 gap-2 sm:gap-3">
|
|
11
11
|
<div class="col-span-9 sm:col-span-10">
|
|
12
12
|
<div class="flex-gap-2 items-center sm:gap-3 cursor-pointer flex-1" @click="handleFilterClick(filter.id)">
|
|
13
|
+
<div
|
|
14
|
+
class="filter-color-dot flex-shrink-0"
|
|
15
|
+
:style="{ backgroundColor: color }">
|
|
16
|
+
</div>
|
|
13
17
|
<div class="filter-type-badge flex-shrink-0">
|
|
14
18
|
<component :is="getFilterIcon()" class="w-3 h-3 sm:w-4 sm:h-4" />
|
|
15
19
|
</div>
|
|
@@ -169,6 +173,10 @@ const props = defineProps({
|
|
|
169
173
|
filterBuilder: {
|
|
170
174
|
type: Object,
|
|
171
175
|
required: true
|
|
176
|
+
},
|
|
177
|
+
color: {
|
|
178
|
+
type: String,
|
|
179
|
+
default: "#7bc999"
|
|
172
180
|
}
|
|
173
181
|
});
|
|
174
182
|
|
|
@@ -307,6 +315,11 @@ props.filterBuilder.onUpdate(() => {
|
|
|
307
315
|
@apply border-t border-t-dark-625/20;
|
|
308
316
|
}
|
|
309
317
|
|
|
318
|
+
.filter-color-dot {
|
|
319
|
+
@apply w-2.5 h-2.5 rounded-full flex-shrink-0;
|
|
320
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
321
|
+
}
|
|
322
|
+
|
|
310
323
|
.filter-type-badge {
|
|
311
324
|
@apply w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-dark-400 flex items-center justify-center flex-shrink-0;
|
|
312
325
|
}
|
package/src/libs/Filter.js
CHANGED
|
@@ -10,7 +10,48 @@ const colors = {
|
|
|
10
10
|
UNSELECTABLE: "#311432"
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Color palette for unique filter colors - distinct but professional
|
|
14
|
+
const filterColorPalette = [
|
|
15
|
+
"#7bc999", // Mint green
|
|
16
|
+
"#6ba8d6", // Steel blue
|
|
17
|
+
"#d4a876", // Tan/gold
|
|
18
|
+
"#a88bc9", // Soft purple
|
|
19
|
+
"#e89ba3", // Coral pink
|
|
20
|
+
"#7dc9c9", // Turquoise
|
|
21
|
+
"#c9a87b", // Warm tan
|
|
22
|
+
"#8bc99a", // Light green
|
|
23
|
+
"#9ba8c9", // Periwinkle
|
|
24
|
+
"#c9b88b" // Khaki
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const getFilterColor = (filterIndex, isExpanded, isExcluded) => {
|
|
28
|
+
if (isExcluded) {
|
|
29
|
+
return isExpanded ? colors.EXCLUDED_EXPANDED : colors.EXCLUDED;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use palette colors for normal filters
|
|
33
|
+
const baseColor = filterColorPalette[filterIndex % filterColorPalette.length];
|
|
34
|
+
|
|
35
|
+
if (!isExpanded) {
|
|
36
|
+
return baseColor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Make expanded filters brighter
|
|
40
|
+
const hex = baseColor.replace('#', '');
|
|
41
|
+
const r = parseInt(hex.substr(0, 2), 16);
|
|
42
|
+
const g = parseInt(hex.substr(2, 2), 16);
|
|
43
|
+
const b = parseInt(hex.substr(4, 2), 16);
|
|
44
|
+
|
|
45
|
+
// Lighten by 20%
|
|
46
|
+
const lighter = (c) => Math.min(255, Math.floor(c + (255 - c) * 0.3));
|
|
47
|
+
return `#${lighter(r).toString(16).padStart(2, '0')}${lighter(g).toString(16).padStart(2, '0')}${lighter(b).toString(16).padStart(2, '0')}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const BASE_CSS = `
|
|
51
|
+
.svg-wrapper path[generaladmission]:hover {fill: ${colors.HIGHLIGHT};pointer-events:all;cursor:pointer;}
|
|
52
|
+
.svg-wrapper path {pointer-events: all; cursor: pointer; touch-action: manipulation;}
|
|
53
|
+
.svg-wrapper {touch-action: manipulation; -webkit-user-select: none; user-select: none;}
|
|
54
|
+
`;
|
|
14
55
|
|
|
15
56
|
const getAttributeValue = (element, attributeName) => {
|
|
16
57
|
try {
|
|
@@ -211,9 +252,12 @@ export default class FilterBuilder {
|
|
|
211
252
|
}
|
|
212
253
|
|
|
213
254
|
addRowHandlers() {
|
|
255
|
+
let lastHighlightedSection = null;
|
|
256
|
+
|
|
214
257
|
for (let i = 0; i < this.rows.length; i++) {
|
|
215
258
|
const row = this.rows[i];
|
|
216
|
-
|
|
259
|
+
|
|
260
|
+
const clickHandler = () => {
|
|
217
261
|
const parsed = {
|
|
218
262
|
section: getAttributeValue(row, "section"),
|
|
219
263
|
row: getAttributeValue(row, "row"),
|
|
@@ -245,6 +289,13 @@ export default class FilterBuilder {
|
|
|
245
289
|
});
|
|
246
290
|
};
|
|
247
291
|
|
|
292
|
+
// Support both click and touch events
|
|
293
|
+
row.onclick = clickHandler;
|
|
294
|
+
row.ontouchend = (e) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
clickHandler();
|
|
297
|
+
};
|
|
298
|
+
|
|
248
299
|
row.onmouseover = () => {
|
|
249
300
|
const parsed = {
|
|
250
301
|
section: getAttributeValue(row, "section"),
|
|
@@ -252,6 +303,18 @@ export default class FilterBuilder {
|
|
|
252
303
|
id: getAttributeValue(row, "id")
|
|
253
304
|
};
|
|
254
305
|
|
|
306
|
+
const sectionKey = `${parsed.section}`;
|
|
307
|
+
|
|
308
|
+
// Skip if already highlighting this section - prevents flickering
|
|
309
|
+
if (lastHighlightedSection === sectionKey) return;
|
|
310
|
+
|
|
311
|
+
// Only clear if switching sections
|
|
312
|
+
if (lastHighlightedSection && lastHighlightedSection !== sectionKey) {
|
|
313
|
+
this.clearHighlight();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lastHighlightedSection = sectionKey;
|
|
317
|
+
|
|
255
318
|
const matchingFilter = this.filters.find(
|
|
256
319
|
(f) =>
|
|
257
320
|
this.isForCurrentEvent(f) &&
|
|
@@ -266,6 +329,7 @@ export default class FilterBuilder {
|
|
|
266
329
|
};
|
|
267
330
|
|
|
268
331
|
row.onmouseout = () => {
|
|
332
|
+
lastHighlightedSection = null;
|
|
269
333
|
if (!this.isDragging) this.clearHighlight();
|
|
270
334
|
};
|
|
271
335
|
}
|
|
@@ -360,7 +424,7 @@ export default class FilterBuilder {
|
|
|
360
424
|
this.cssClasses = BASE_CSS;
|
|
361
425
|
const gaSectionNameMapping = this.getSectionNameMapping();
|
|
362
426
|
|
|
363
|
-
this.filters.forEach((filter) => {
|
|
427
|
+
this.filters.forEach((filter, filterIndex) => {
|
|
364
428
|
if (!this.isForCurrentEvent(filter)) return;
|
|
365
429
|
|
|
366
430
|
// Try to find the short name (attribute name) from the real name (stored in filter)
|
|
@@ -377,12 +441,12 @@ export default class FilterBuilder {
|
|
|
377
441
|
|
|
378
442
|
const type = this.getFilterType(filter);
|
|
379
443
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
444
|
+
// Use unique color for each filter
|
|
445
|
+
const color = getFilterColor(
|
|
446
|
+
filterIndex,
|
|
447
|
+
this.expandedFilter === filter.id,
|
|
448
|
+
filter.exclude
|
|
449
|
+
);
|
|
386
450
|
|
|
387
451
|
switch (type) {
|
|
388
452
|
case this.filterTypes.NORMAL:
|
|
@@ -442,7 +506,7 @@ export default class FilterBuilder {
|
|
|
442
506
|
const label = labels[i];
|
|
443
507
|
const section = getAttributeValue(label, "section");
|
|
444
508
|
|
|
445
|
-
|
|
509
|
+
const labelClickHandler = () => {
|
|
446
510
|
// filter exists already for section - return
|
|
447
511
|
const matchingFilter = this.filters.find((f) => {
|
|
448
512
|
if (!this.isForCurrentEvent(f)) return false;
|
|
@@ -490,6 +554,13 @@ export default class FilterBuilder {
|
|
|
490
554
|
log("Could not add labelHandler", section, e);
|
|
491
555
|
}
|
|
492
556
|
};
|
|
557
|
+
|
|
558
|
+
// Support both click and touch
|
|
559
|
+
label.onclick = labelClickHandler;
|
|
560
|
+
label.ontouchend = (e) => {
|
|
561
|
+
e.preventDefault();
|
|
562
|
+
labelClickHandler();
|
|
563
|
+
};
|
|
493
564
|
}
|
|
494
565
|
}
|
|
495
566
|
|
|
@@ -506,7 +577,7 @@ export default class FilterBuilder {
|
|
|
506
577
|
const section = getAttributeValue(path, "name");
|
|
507
578
|
const sectionName = gaSectionNameMapping[section];
|
|
508
579
|
|
|
509
|
-
|
|
580
|
+
const gaClickHandler = () => {
|
|
510
581
|
// filter exists already for section
|
|
511
582
|
const matchingFilter = this.filters.find((f) => f.section === sectionName && this.isForCurrentEvent(f));
|
|
512
583
|
if (matchingFilter) {
|
|
@@ -525,6 +596,13 @@ export default class FilterBuilder {
|
|
|
525
596
|
event: this.currentEventId
|
|
526
597
|
});
|
|
527
598
|
};
|
|
599
|
+
|
|
600
|
+
// Support both click and touch
|
|
601
|
+
path.onclick = gaClickHandler;
|
|
602
|
+
path.ontouchend = (e) => {
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
gaClickHandler();
|
|
605
|
+
};
|
|
528
606
|
}
|
|
529
607
|
}
|
|
530
608
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import BaseRenderer from "../base-renderer.js";
|
|
2
|
+
import { getEventData, getEventDataUrl } from "./requests.js";
|
|
3
|
+
|
|
4
|
+
class AXSRenderer extends BaseRenderer {
|
|
5
|
+
constructor(eventId, options = {}) {
|
|
6
|
+
super(eventId, options);
|
|
7
|
+
this.config = this.getDefaultConfig();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async render() {
|
|
11
|
+
if (!this.storage) {
|
|
12
|
+
this.logger.Error("Cannot render without storage");
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const data = await this.getEventData();
|
|
18
|
+
this.logger.Debug("Event data loaded");
|
|
19
|
+
|
|
20
|
+
if (this.fs) {
|
|
21
|
+
this.fs.writeFileSync("out.json", JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let svgBody = "";
|
|
25
|
+
|
|
26
|
+
data.n.forEach((subsection) => {
|
|
27
|
+
let sectionData = "";
|
|
28
|
+
switch (subsection.h.t) {
|
|
29
|
+
case "seat":
|
|
30
|
+
subsection.n.forEach((seat) => {
|
|
31
|
+
sectionData += this.createElement("circle", {
|
|
32
|
+
cx: seat.c[0],
|
|
33
|
+
cy: seat.c[1],
|
|
34
|
+
id: seat.i,
|
|
35
|
+
r: "0.5",
|
|
36
|
+
fill: "#fff",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
case "section":
|
|
41
|
+
subsection.n.forEach((polygon) => {
|
|
42
|
+
sectionData += this.createElement(
|
|
43
|
+
"g",
|
|
44
|
+
{
|
|
45
|
+
id: polygon.i,
|
|
46
|
+
transform: `translate(${polygon.c[0]} ${polygon.c[1]})`,
|
|
47
|
+
},
|
|
48
|
+
this.createElement("path", {
|
|
49
|
+
d: polygon.pt,
|
|
50
|
+
fill: "rgba(100,100,100,0.5)",
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
svgBody += this.createElement("g", {}, sectionData);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.svg = this.createElement(
|
|
62
|
+
"svg",
|
|
63
|
+
{
|
|
64
|
+
width: "1440",
|
|
65
|
+
height: "816",
|
|
66
|
+
viewBox: "0 0 1200 680",
|
|
67
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
68
|
+
version: "1.1",
|
|
69
|
+
},
|
|
70
|
+
svgBody,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (this.fs) {
|
|
74
|
+
this.fs.writeFileSync("test.svg", this.svg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
this.logger.Error(`Couldn't render ${this.eventId} ${e.message}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getEventData() {
|
|
85
|
+
return await getEventData(this.eventId, this.options.proxy, this.logger);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getEventDataUrl() {
|
|
89
|
+
return await getEventDataUrl(this.eventId, this.options.proxy, this.logger);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async fetchBackgroundImage() {
|
|
93
|
+
throw new Error("Background image fetching not implemented for AXS renderer");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default AXSRenderer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getJsonDataWithHeaders } from "../request-utils.js";
|
|
2
|
+
|
|
3
|
+
function buildAXSEventDataUrl(eventId, data) {
|
|
4
|
+
const base = data.url.replace("*", "");
|
|
5
|
+
const policy = data.keys["CloudFront-Policy"];
|
|
6
|
+
const sig = data.keys["CloudFront-Signature"];
|
|
7
|
+
const keypair = data.keys["CloudFront-Key-Pair-Id"];
|
|
8
|
+
const version = "2.6.16";
|
|
9
|
+
return `${base}maps/main/master_full.json?Policy=${policy}_&Signature=${sig}&Key-Pair-Id=${keypair}&v=${version}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getEventDataUrl(eventId, proxy, logger) {
|
|
13
|
+
const url = `https://contentdistribution.3ddvapis.com/api/v1/dvm/token/venue/${eventId}`;
|
|
14
|
+
const customHeaders = {
|
|
15
|
+
"referer": "https://tickets-de.axs.com/",
|
|
16
|
+
"user-agent":
|
|
17
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return await getJsonDataWithHeaders(url, customHeaders, proxy, 0, logger);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getEventData(eventId, proxy, logger) {
|
|
24
|
+
const urlData = await getEventDataUrl(eventId, proxy, logger);
|
|
25
|
+
if (!urlData) {
|
|
26
|
+
throw new Error("Failed to get event data URL");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const url = buildAXSEventDataUrl(eventId, urlData);
|
|
30
|
+
return await getJsonDataWithHeaders(url, {}, proxy, 0, logger);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
getEventData,
|
|
35
|
+
getEventDataUrl,
|
|
36
|
+
buildAXSEventDataUrl,
|
|
37
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createLogger } from "./dependencies/logger.js";
|
|
2
|
+
import * as utils from "./utils.js";
|
|
3
|
+
|
|
4
|
+
class BaseRenderer {
|
|
5
|
+
constructor(eventId, options = {}) {
|
|
6
|
+
this.eventId = eventId;
|
|
7
|
+
this.options = {
|
|
8
|
+
proxy: null,
|
|
9
|
+
country: null,
|
|
10
|
+
...options,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
this.logger = createLogger([this.constructor.name, this.eventId]);
|
|
14
|
+
this.storage = null;
|
|
15
|
+
this.sharp = null;
|
|
16
|
+
this.fs = null;
|
|
17
|
+
this.svg = null;
|
|
18
|
+
this.bgImg = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init(dependencies) {
|
|
22
|
+
this.storage = dependencies.storage;
|
|
23
|
+
this.sharp = dependencies.sharp;
|
|
24
|
+
this.fs = dependencies.fs;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setHighlightedSeats(placeIds, color = null) {
|
|
29
|
+
if (!this.highlighted) {
|
|
30
|
+
this.highlighted = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const seatsToAdd = (placeIds || []).map((id) => ({
|
|
34
|
+
id,
|
|
35
|
+
color: color || this.getDefaultConfig().highlightedSeatColor,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
this.highlighted.push(...seatsToAdd);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setCustomConfig(config) {
|
|
43
|
+
this.config = {
|
|
44
|
+
...this.getDefaultConfig(),
|
|
45
|
+
...config,
|
|
46
|
+
};
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getDefaultConfig() {
|
|
51
|
+
return {
|
|
52
|
+
// Basic rendering options
|
|
53
|
+
outputWidth: 2048,
|
|
54
|
+
includeDetailedAttributes: false,
|
|
55
|
+
logFullError: true,
|
|
56
|
+
|
|
57
|
+
// Colors
|
|
58
|
+
seatColor: "#0557ae",
|
|
59
|
+
nonAvSeatColor: "#dadcde",
|
|
60
|
+
highlightedSeatColor: "#d0006f",
|
|
61
|
+
GAColor: "#61c6c2",
|
|
62
|
+
nonAvGAColor: "#b4babe",
|
|
63
|
+
grayedOutSection: "#a3a3a3",
|
|
64
|
+
|
|
65
|
+
// Text styling
|
|
66
|
+
labelColor: "#000",
|
|
67
|
+
labelColorOnGA: "#fff",
|
|
68
|
+
textOpacity: 0.8,
|
|
69
|
+
textOpacityOnGA: 1,
|
|
70
|
+
fontSize: "1em",
|
|
71
|
+
font: "sans-serif",
|
|
72
|
+
useBig: true,
|
|
73
|
+
|
|
74
|
+
// Rendering behavior
|
|
75
|
+
dontRenderUnusedSections: false,
|
|
76
|
+
dontRenderSeats: false,
|
|
77
|
+
seatsAsPins: false,
|
|
78
|
+
renderRowBlocks: false,
|
|
79
|
+
increaseHighlightedSeatSize: 6,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async render() {
|
|
84
|
+
throw new Error("render() method must be implemented by subclass");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async renderSeatmap(priceData) {
|
|
88
|
+
// Store price data for subclasses to use
|
|
89
|
+
this.priceData = priceData;
|
|
90
|
+
throw new Error("renderSeatmap() method must be implemented by subclass");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async outputToSVG(path) {
|
|
94
|
+
if (!this.svg || !this.fs) {
|
|
95
|
+
this.logger.Error("No SVG data available or fs not initialized");
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
this.fs.writeFileSync(path, this.svg);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async outputToPNG(filePath) {
|
|
103
|
+
if (!this.svg || !this.sharp) {
|
|
104
|
+
this.logger.Error("No SVG data available or sharp not initialized");
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
const s = await this._png();
|
|
108
|
+
await s.toFile(filePath);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async outputToPNGBuffer() {
|
|
113
|
+
if (!this.svg || !this.sharp) {
|
|
114
|
+
return Buffer.from([0x00]);
|
|
115
|
+
}
|
|
116
|
+
const s = await this._png();
|
|
117
|
+
return s.toBuffer();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _png() {
|
|
121
|
+
let background = await this.storage.getItem(`renderer.background.${this.eventId}`);
|
|
122
|
+
if (!background) {
|
|
123
|
+
try {
|
|
124
|
+
background = await this._fetchSaveBgImage();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this.logger.Error(e.message);
|
|
127
|
+
}
|
|
128
|
+
} else if (background) {
|
|
129
|
+
if (typeof background === "string" && background.startsWith("{")) {
|
|
130
|
+
background = JSON.parse(background);
|
|
131
|
+
}
|
|
132
|
+
background = Buffer.from(background);
|
|
133
|
+
const bgData = await this.sharp(background).metadata();
|
|
134
|
+
if (bgData.width !== this.config.outputWidth) {
|
|
135
|
+
this.config.outputWidth = bgData.width;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const svgBuffer = Buffer.from(this.svg);
|
|
140
|
+
const png = await this.sharp(svgBuffer).png().resize(this.config.outputWidth).toBuffer();
|
|
141
|
+
|
|
142
|
+
if (background) {
|
|
143
|
+
const foreground = await this.sharp(png).toBuffer();
|
|
144
|
+
return this.sharp(background).composite([
|
|
145
|
+
{
|
|
146
|
+
input: foreground,
|
|
147
|
+
gravity: "southeast",
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
} else {
|
|
151
|
+
return this.sharp(png);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _fetchSaveBgImage() {
|
|
156
|
+
const bgImgData = await this.fetchBackgroundImage();
|
|
157
|
+
const background = await this.sharp(bgImgData).png().resize(this.config.outputWidth).toBuffer();
|
|
158
|
+
await this.storage.setItem(`renderer.background.${this.eventId}`, background);
|
|
159
|
+
return background;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
createElement(tag, attrs, body = "") {
|
|
163
|
+
return utils.createElement(tag, attrs, body);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async fetchBackgroundImage() {
|
|
167
|
+
throw new Error("fetchBackgroundImage() method must be implemented by subclass");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default BaseRenderer;
|