ngx-venue-map
Version:
Interactive venue seating map component for Angular applications
1,908 lines • 87 kB
JavaScript
import * as i0 from '@angular/core';
import { input, model, Component, Injectable, inject, signal, computed, effect, output, ViewChild, ViewEncapsulation } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { throttle, debounce, cloneDeep, merge } from 'lodash-es';
import { v4 } from 'uuid';
import { BehaviorSubject } from 'rxjs';
import { fabric } from 'fabric';
// ============================================================================
// ENUMS
// ============================================================================
var VenueMapType;
(function (VenueMapType) {
VenueMapType["GRADE"] = "grade";
VenueMapType["GRADE_PRICE"] = "gradePrice";
})(VenueMapType || (VenueMapType = {}));
// =====s=======================================================================
const DEFAULT_SIZE_SHAPE = {
width: 0,
height: 0,
padding: 0,
stroke: 0,
offsetLeft: 0,
offsetTop: 0,
cornerRadius: 0,
};
const DEFAULT_MAP_CONFIG = {
circle: {
...DEFAULT_SIZE_SHAPE,
width: 50,
height: 50,
stroke: 2,
padding: 10,
},
fontSize: 20,
bubble: {
...DEFAULT_SIZE_SHAPE,
width: 30,
height: 20,
},
corner: {
...DEFAULT_SIZE_SHAPE,
width: 20,
},
arrow: {
...DEFAULT_SIZE_SHAPE,
width: 15,
height: 10,
padding: 12,
},
offsetHeight: 106,
minimap: {
scale: 0.12,
opacity: 0.5,
},
zoom: {
min: 1,
max: 2.5,
step: 0.2,
showMiniMapAtLevel: 1.2,
}
};
// Default theme colors that match the use cases
const DEFAULT_VENUE_MAP_THEME = {
// Canvas and background colors
canvasBackgroundColor: '#ffffff',
// Grade visualization colors
gradeStrokeColor: '#ffffff',
gradeTextColor: '#ffffff',
// Price visualization colors
priceBackgroundColor: '#ffffff',
priceTextColor: '#000000',
priceBubbleStrokeColor: '#ffffff',
// Interactive elements
miniMapViewportColor: '#EE2039',
// Seat colors
availableSeatColor: '#ffffff',
unavailableSeatColor: '#969696',
selectedSeatColor: '#0071EAB2',
selectedSeatBorderColor: '#0071EA',
availableSeatBorderColor: '#0071EA',
disabledSeatColor: '#2F2F2F',
// Special seat types
premiumSeatColor: '#B60000',
premiumSeatHoverColor: '#B60000B2',
goldSeatColor: '#8F7400',
// UI colors
shadowColor: 'rgba(0,0,0,0.22)',
darkTextColor: '#767676',
};
const MULTIPLIER_GRADE_ICON = 1.5;
class SwitchComponent {
handleOnChange = input();
className = input('');
checked = model(false);
onChange(event) {
const target = event.target;
this.checked.set(target.checked);
this.handleOnChange()?.(target.checked);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: SwitchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.5", type: SwitchComponent, isStandalone: true, selector: "app-switch", inputs: { handleOnChange: { classPropertyName: "handleOnChange", publicName: "handleOnChange", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange" }, ngImport: i0, template: "<label class=\"switch\">\n <input\n type=\"checkbox\"\n [checked]=\"checked()\"\n (change)=\"onChange($event)\"\n class=\"sr-only switch-input\"\n />\n <div class=\"switch-indicator\" [ngClass]=\"className()\"></div>\n</label>\n", styles: [".switch{display:flex;align-items:center;cursor:pointer}.switch-input:checked~.switch-indicator{background-color:#707086}.switch-input:checked~.switch-indicator:after{transform:translate(100%) rotate(0) skew(0) skewY(0) scaleX(1) scaleY(1);background-image:url(\"data:image/svg+xml,%3Csvg%20width%3D'10'%20height%3D'8'%20viewBox%3D'0%200%2010%208'%20fill%3D'none'%20xmlns%3D'http%3A//www.w3.org/2000/svg'%3E%3Cpath%20fill-rule%3D'evenodd'%20clip-rule%3D'evenodd'%20d%3D'M8.3589%200.298767L3.54253%205.03255L1.64707%203.16783L0.285767%204.49762L3.54236%207.7014L9.71297%201.63087L8.3589%200.298767Z'%20fill%3D'%23112'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:center;background-size:10px 8px;inset-inline-start:6px}.switch-input:checked~.switch-indicator.bg-black{background-color:#112}.switch-input:checked~.switch-indicator.bg-black:after{background-image:url(data:image/svg+xml,%3Csvg%20width%3D%2212%22%20height%3D%2210%22%20viewBox%3D%220%200%2012%2010%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cpath%20d%3D%22M4.0001%207.7799L1.2201%204.9999L0.273438%205.9399L4.0001%209.66656L12.0001%201.66656L11.0601%200.726562L4.0001%207.7799Z%22%20fill%3D%22%23111122%22/%3E%3C/svg%3E)}.switch-indicator{position:relative;border-radius:1rem;transition:transform;background-color:#969696;width:3rem;height:1.5rem}.switch-indicator:after{content:\"\";position:absolute;transition:transform .15s cubic-bezier(.4,0,.2,1);background-color:#fff;border-width:1px;border-color:#fff;width:1.25rem;height:1.25rem;border-radius:100%;top:.125rem;inset-inline-start:2px}.switch-indicator-medium{width:3.25rem;height:2rem}.switch-indicator-medium:after{width:1.5rem;height:1.5rem;top:.25rem;left:.25rem}.switch-input:checked~.switch-indicator-medium:after{transform:translate(calc(100% - 6px)) rotate(0) skew(0) skewY(0) scaleX(1) scaleY(1)}.switch .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: SwitchComponent, decorators: [{
type: Component,
args: [{ selector: 'app-switch', standalone: true, imports: [FormsModule, CommonModule], template: "<label class=\"switch\">\n <input\n type=\"checkbox\"\n [checked]=\"checked()\"\n (change)=\"onChange($event)\"\n class=\"sr-only switch-input\"\n />\n <div class=\"switch-indicator\" [ngClass]=\"className()\"></div>\n</label>\n", styles: [".switch{display:flex;align-items:center;cursor:pointer}.switch-input:checked~.switch-indicator{background-color:#707086}.switch-input:checked~.switch-indicator:after{transform:translate(100%) rotate(0) skew(0) skewY(0) scaleX(1) scaleY(1);background-image:url(\"data:image/svg+xml,%3Csvg%20width%3D'10'%20height%3D'8'%20viewBox%3D'0%200%2010%208'%20fill%3D'none'%20xmlns%3D'http%3A//www.w3.org/2000/svg'%3E%3Cpath%20fill-rule%3D'evenodd'%20clip-rule%3D'evenodd'%20d%3D'M8.3589%200.298767L3.54253%205.03255L1.64707%203.16783L0.285767%204.49762L3.54236%207.7014L9.71297%201.63087L8.3589%200.298767Z'%20fill%3D'%23112'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:center;background-size:10px 8px;inset-inline-start:6px}.switch-input:checked~.switch-indicator.bg-black{background-color:#112}.switch-input:checked~.switch-indicator.bg-black:after{background-image:url(data:image/svg+xml,%3Csvg%20width%3D%2212%22%20height%3D%2210%22%20viewBox%3D%220%200%2012%2010%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cpath%20d%3D%22M4.0001%207.7799L1.2201%204.9999L0.273438%205.9399L4.0001%209.66656L12.0001%201.66656L11.0601%200.726562L4.0001%207.7799Z%22%20fill%3D%22%23111122%22/%3E%3C/svg%3E)}.switch-indicator{position:relative;border-radius:1rem;transition:transform;background-color:#969696;width:3rem;height:1.5rem}.switch-indicator:after{content:\"\";position:absolute;transition:transform .15s cubic-bezier(.4,0,.2,1);background-color:#fff;border-width:1px;border-color:#fff;width:1.25rem;height:1.25rem;border-radius:100%;top:.125rem;inset-inline-start:2px}.switch-indicator-medium{width:3.25rem;height:2rem}.switch-indicator-medium:after{width:1.5rem;height:1.5rem;top:.25rem;left:.25rem}.switch-input:checked~.switch-indicator-medium:after{transform:translate(calc(100% - 6px)) rotate(0) skew(0) skewY(0) scaleX(1) scaleY(1)}.switch .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}\n"] }]
}] });
/**
* Service responsible for rendering venue map images and backgrounds.
* Handles both single images and multi-part image loading.
* For multi-part images, follows a file naming pattern: col_row.ext
* Example: 0_0.png, 0_1.png, 1_0.png, 1_1.png
* Also manages background image loading and canvas setup for venue maps.
*/
class VenueMapRenderService {
// ============================================================================
// MULTI-PART IMAGE METHODS
// ============================================================================
/**
* Determines if the path represents multiple image parts
* @param path - Single path string or array of paths
* @returns true if multiple parts should be rendered
*/
isMultiPartImage(path) {
return Array.isArray(path);
}
/**
* Generates image paths based on pattern col_row.ext
* @param basePath - Base path template (e.g., '/images/map/parts/{col}_{row}.png')
* @param cols - Number of columns
* @param rows - Number of rows
* @returns Array of image paths
*/
generateImagePaths(basePath, cols, rows) {
const paths = [];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const path = basePath
.replace('{col}', col.toString())
.replace('{row}', row.toString());
paths.push(path);
}
}
return paths;
}
/**
* Parses column and row from filename pattern col_row.ext
* @param filename - Filename to parse (e.g., '0_1.png')
* @returns Object with col and row numbers, or null if pattern doesn't match
*/
parseImageCoordinates(filename) {
// Extract filename without path
const name = filename.split('/').pop() || filename;
// Remove extension
const nameWithoutExt = name.split('.')[0];
// Match pattern: col_row
const match = nameWithoutExt.match(/^(\d+)_(\d+)$/);
if (match) {
return {
col: parseInt(match[1], 10),
row: parseInt(match[2], 10)
};
}
return null;
}
/**
* Calculates grid dimensions from array of image paths
* @param paths - Array of image paths
* @returns Object with cols and rows dimensions
*/
calculateGridDimensions(paths) {
let maxCol = 0;
let maxRow = 0;
for (const path of paths) {
const coords = this.parseImageCoordinates(path);
if (coords) {
maxCol = Math.max(maxCol, coords.col);
maxRow = Math.max(maxRow, coords.row);
}
}
return {
cols: maxCol + 1,
rows: maxRow + 1
};
}
/**
* Loads multiple images and creates a composite fabric.Image
* @param paths - Array of image paths
* @param totalWidth - Total width of the composite image
* @param totalHeight - Total height of the composite image
* @param callback - Callback function with the composite image
*/
loadMultiPartImage(paths, totalWidth, totalHeight, callback) {
const { cols, rows } = this.calculateGridDimensions(paths);
if (cols === 0 || rows === 0) {
console.error('Invalid grid dimensions calculated from paths');
return;
}
const partWidth = totalWidth / cols;
const partHeight = totalHeight / rows;
// Create canvas for compositing
const compositeCanvas = new fabric.StaticCanvas(null, {
width: totalWidth,
height: totalHeight
});
let loadedCount = 0;
const totalParts = paths.length;
// Load each image part
paths.forEach(path => {
fabric.Image.fromURL(path, (img) => {
if (!img) {
console.error(`Failed to load image: ${path}`);
loadedCount++;
if (loadedCount === totalParts) {
this.finalizeCompositeImage(compositeCanvas, callback);
}
return;
}
const coords = this.parseImageCoordinates(path);
if (!coords) {
console.error(`Could not parse coordinates from path: ${path}`);
loadedCount++;
if (loadedCount === totalParts) {
this.finalizeCompositeImage(compositeCanvas, callback);
}
return;
}
// Position the image part
const x = coords.col * partWidth;
const y = coords.row * partHeight;
img.set({
left: x,
top: y,
scaleX: partWidth / (img.width || 1),
scaleY: partHeight / (img.height || 1),
selectable: false,
evented: false
});
compositeCanvas.add(img);
loadedCount++;
if (loadedCount === totalParts) {
this.finalizeCompositeImage(compositeCanvas, callback);
}
}, { crossOrigin: 'anonymous' });
});
}
/**
* Finalizes the composite image and returns it via callback
* @param compositeCanvas - Canvas containing all image parts
* @param callback - Callback function with the final composite image
*/
finalizeCompositeImage(compositeCanvas, callback) {
// Render the canvas and create a new fabric.Image from it
const dataURL = compositeCanvas.toDataURL({
format: 'png',
quality: 1.0
});
fabric.Image.fromURL(dataURL, (compositeImage) => {
if (compositeImage) {
compositeImage.set({
selectable: false,
evented: false,
objectCaching: false
});
callback(compositeImage);
}
}, { crossOrigin: 'anonymous' });
}
/**
* Loads a single image
* @param path - Image path
* @param callback - Callback function with the loaded image
*/
loadSingleImage(path, callback) {
fabric.Image.fromURL(path, callback, { crossOrigin: 'anonymous' });
}
// ============================================================================
// VENUE IMAGE RENDERING METHODS
// ============================================================================
/**
* Loads background image for the venue map canvas
* @param canvas - Fabric canvas instance
* @param mapConfig - Map configuration
* @param viewDimensions - View dimensions
* @param pathImg - Image path (single or array)
* @param callback - Callback function with mapWidth
*/
loadBackgroundImage(canvas, mapConfig, viewDimensions, pathImg, callback) {
if (!canvas)
return;
const { path } = mapConfig;
const targetPath = pathImg ?? path;
const backgroundImage = canvas.backgroundImage;
if (backgroundImage && backgroundImage instanceof fabric.Image) {
canvas.remove(backgroundImage);
}
// Check if it's a multi-part image
if (this.isMultiPartImage(targetPath)) {
this.loadMultiPartBackgroundImage(canvas, mapConfig, viewDimensions, targetPath, callback);
}
else {
this.loadSingleBackgroundImage(canvas, mapConfig, viewDimensions, targetPath, callback);
}
}
/**
* Loads a single background image
*/
loadSingleBackgroundImage(canvas, mapConfig, viewDimensions, path, callback) {
fabric.Image.fromURL(path, img => {
if (!img)
return;
const result = this.setDataImageToCanvas(canvas, mapConfig, viewDimensions, img);
callback?.(result.mapWidth);
}, { crossOrigin: 'anonymous' });
}
/**
* Loads multiple image parts and creates a composite background
*/
loadMultiPartBackgroundImage(canvas, mapConfig, viewDimensions, paths, callback) {
const { width, height } = mapConfig;
this.loadMultiPartImage(paths, width, height, (compositeImage) => {
if (!compositeImage)
return;
const result = this.setDataImageToCanvas(canvas, mapConfig, viewDimensions, compositeImage);
callback?.(result.mapWidth);
});
}
/**
* Sets image data to canvas and configures canvas properties
*/
setDataImageToCanvas(canvas, mapConfig, viewDimensions, img) {
mapConfig.width = img.width;
mapConfig.height = img.height;
viewDimensions.scale = viewDimensions.height / img.height;
const mapWidth = Math.max(this.scaleValue(img.width, viewDimensions.scale), viewDimensions.width);
img.scaleToWidth(this.scaleValue(img.width, viewDimensions.scale));
img.scaleToHeight(this.scaleValue(img.height, viewDimensions.scale));
img.set({ selectable: false, evented: false, objectCaching: false });
canvas.setWidth(viewDimensions.width);
canvas.setHeight(viewDimensions.height);
canvas.setBackgroundImage(img, () => { });
return { mapWidth };
}
/**
* Scales a value based on the current scale factor
*/
scaleValue(value, scale) {
return value * scale;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapRenderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapRenderService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapRenderService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Pure utility functions for venue map operations that don't require service dependencies.
* These functions are used across different services for scaling operations.
*/
/**
* Scales a single value based on the provided scale factor.
*/
function scaleValue(value, scale) {
return value * scale;
}
/**
* Scales a point coordinate based on the provided scale factor.
*/
function scalePoint(point, scale) {
return {
x: point.x * scale,
y: point.y * scale,
};
}
/**
* Scales an array of points based on the provided scale factor.
*/
function scalePoints(points, scale) {
return points.map(point => scalePoint(point, scale));
}
class VenueMapMiniMapService {
// ============================================================================
// PROPERTIES
// ============================================================================
miniMapCanvas;
miniMapRect;
// ============================================================================
// PUBLIC METHODS - MINIMAP INITIALIZATION
// ============================================================================
/**
* Initializes the mini map canvas with the specified element
*/
initializeMiniMap(mapWidth, viewDimensions, mapConfig) {
const miniMapScale = mapConfig.minimap.scale;
const containerHeight = viewDimensions.height;
this.miniMapCanvas = new fabric.StaticCanvas('miniMap', {
width: scaleValue(mapWidth, viewDimensions.scale) * miniMapScale,
height: containerHeight * miniMapScale,
});
return this.miniMapCanvas;
}
/**
* Renders the mini map using data from the main canvas
*/
renderMiniMap(mainCanvas) {
if (!this.miniMapCanvas || !mainCanvas)
return;
const mainCanvasImage = mainCanvas.toDataURL({
format: 'png',
quality: 1.0,
});
fabric.Image.fromURL(mainCanvasImage, (img) => {
if (!img)
return;
img.scaleToWidth(this.miniMapCanvas.getWidth());
img.scaleToHeight(this.miniMapCanvas.getHeight());
this.miniMapCanvas.setBackgroundImage(img, () => { });
}, { crossOrigin: 'anonymous' });
this.miniMapCanvas.renderAll();
}
/**
* Displays the viewport rectangle on the mini map
*/
showViewportRect(viewportTransform, canvasWidth, canvasHeight, currentZoom, viewDimensions, mapConfig, theme) {
if (this.miniMapRect)
this.miniMapCanvas?.remove(this.miniMapRect);
const minimapConfig = mapConfig.minimap;
const scale = minimapConfig.scale;
const position = this.findPositionViewport(viewportTransform, currentZoom, canvasWidth, canvasHeight, viewDimensions, scale);
this.miniMapRect = new fabric.Rect({
...position,
fill: theme.miniMapViewportColor,
selectable: false,
opacity: minimapConfig.opacity,
});
this.miniMapRect.set('id', 'viewportZoom');
this.miniMapCanvas?.add(this.miniMapRect);
}
/**
* Finds the position and dimensions of the viewport rectangle on the mini map
*/
findPositionViewport(viewportTransform, currentZoom, canvasWidth, canvasHeight, viewDimensions, scale = 1) {
const canvasWidthScaled = (canvasWidth * scale) / currentZoom;
const canvasHeightScaled = (canvasHeight * scale) / currentZoom;
const width = (viewDimensions.width * scale) / currentZoom;
const height = canvasHeightScaled;
const left = (-(viewportTransform?.[4] ?? 0) * scale) / currentZoom +
(canvasWidthScaled - width) / 2;
const top = (-(viewportTransform?.[5] ?? 0) * scale) / currentZoom;
return { top, left, width, height };
}
// ============================================================================
// UTILITY METHODS
// ============================================================================
/**
* Cleans up resources when service is no longer needed
*/
cleanup() {
if (this.miniMapCanvas) {
this.miniMapCanvas.clear();
this.miniMapCanvas.dispose();
this.miniMapCanvas = undefined;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapMiniMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapMiniMapService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapMiniMapService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
// Constants
const LONG_TOUCH_DURATION = 200;
const ZOOM_THRESHOLD = 1.2;
const CLICK_THRESHOLD = 5;
const ZOOM_ANIMATION_DURATION = 500;
const DEBOUNCE_DELAY$1 = 250;
const THROTTLE_DELAY = 100;
class VenueMapCanvasService {
// ============================================================================
// INJECTED SERVICES
// ============================================================================
renderService = inject(VenueMapRenderService);
miniMapService = inject(VenueMapMiniMapService);
// ============================================================================
// SIGNAL-BASED STATE MANAGEMENT (Angular 20)
// ============================================================================
// Canvas state signals
canvasReady$ = new BehaviorSubject(false);
// Public signal state
zoomLevel = signal(1);
isZooming = signal(false);
canvasReady = signal(false);
// Computed signals for getters
disableZoomIn = computed(() => this.zoomLevel() >= this.mapConfig?.zoom.max);
disableZoomOut = computed(() => this.zoomLevel() <= this.mapConfig?.zoom.min);
showMiniMap = computed(() => this.zoomLevel() >= this.mapConfig?.zoom.showMiniMapAtLevel);
// ============================================================================
// PRIVATE PROPERTIES
// ============================================================================
// Touch and interaction state
touchState = {
isTouching: false,
isDragging: false,
isPinching: false,
isZooming: false,
isMoving: false,
pausePanning: false,
};
touchTimeout;
animationFrameId = null;
currentPosition = { x: 0, y: 0 };
lastPosition = { x: 0, y: 0 };
lastTouchEventTimestamp = 0;
zoomStartScale = 0;
lastScale = 0;
mapWidth = 0;
// Event handlers
debouncedClickHandler;
debounceClick;
workerRenderValue;
// ============================================================================
// PUBLIC PROPERTIES
// ============================================================================
clientPopupPosition;
miniMapCanvas;
viewDimensions;
canvas;
mapConfig;
theme;
constructor() {
// Effect to sync zoom level with canvas zoom
effect(() => {
if (this.canvas) {
const currentZoom = this.canvas.getZoom();
if (currentZoom !== this.zoomLevel()) {
this.zoomLevel.set(currentZoom);
}
}
});
// Effect to sync canvas ready state
effect(() => {
this.canvasReady$.next(this.canvasReady());
});
}
// ============================================================================
// PRIVATE HELPER METHODS
// ============================================================================
/**
* Updates the zoom level signal and triggers reactive updates
*/
updateZoomLevel(zoom) {
this.zoomLevel.set(zoom);
}
// ============================================================================
// PUBLIC METHODS - INITIALIZATION
// ============================================================================
getCanvasReadyObservable() {
return this.canvasReady$.asObservable();
}
initialize(viewDimensions, config, theme) {
this.viewDimensions = viewDimensions;
this.mapConfig = config;
this.theme = theme;
}
setupCanvas(canvasElement) {
this.canvas = new fabric.Canvas(canvasElement, {
selection: false,
backgroundColor: this.theme.canvasBackgroundColor,
renderOnAddRemove: false,
stateful: false,
});
this.hiddenMap();
this.setupEventListeners();
}
hiddenMap() {
if (!this.canvas)
return;
this.canvas.getElement().style.visibility = 'hidden';
this.canvas.renderOnAddRemove = false;
}
// ============================================================================
// PRIVATE METHODS - EVENT SETUP
// ============================================================================
setupEventListeners() {
this.canvas.on('mouse:wheel', throttle(e => this.handleZoom(e), THROTTLE_DELAY));
this.canvas.on('touch:gesture', throttle(e => this.handleGesture(e), THROTTLE_DELAY));
this.canvas.on('selection:created', () => (this.touchState.pausePanning = true));
this.canvas.on('selection:cleared', () => (this.touchState.pausePanning = false));
this.canvas.on('touch:drag', e => this.handleDrag(e));
this.canvas.on('mouse:down', options => this.handleMouseDown(options));
this.canvas.on('mouse:move', throttle(options => this.handleMouseMove(options), 50));
this.canvas.on('mouse:up', options => this.handleMouseUp(options));
const wrapperEl = this.canvas.wrapperEl;
if (wrapperEl) {
wrapperEl.addEventListener('touchstart', this.handleTouchStart.bind(this), {
passive: false,
});
wrapperEl.addEventListener('touchmove', this.handleTouchMove.bind(this), {
passive: false,
});
wrapperEl.addEventListener('touchend', this.handleTouchEnd.bind(this), {
passive: false,
});
}
}
// ============================================================================
// PRIVATE METHODS - TOUCH EVENT HANDLERS
// ============================================================================
handleTouchStart(e) {
e.preventDefault();
this.cancelDebounceClick?.();
this.touchState.isTouching = true;
if (e.touches.length >= 2) {
this.touchState.pausePanning = true;
this.touchState.isZooming = true;
this.touchState.isPinching = true;
this.isZooming.set(true);
this.touchState.isDragging = false;
this.zoomStartScale = this.getDistance(e.touches[0], e.touches[1]);
return;
}
this.lastPosition = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
this.touchTimeout = setTimeout(() => {
if (this.touchState.isTouching && !this.touchState.isPinching) {
this.touchState.isDragging = true;
this.cancelDebounceClick?.();
}
}, LONG_TOUCH_DURATION);
}
handleTouchMove(e) {
e.preventDefault();
if (e.touches.length >= 2) {
this.handlePinchZoom(e);
return;
}
this.handleSingleTouchMove(e);
}
handlePinchZoom(e) {
const zoomFactor = 0.5;
const smoothingFactor = 0.5;
this.cancelDebounceClick?.();
if (!this.touchState.isPinching) {
this.touchState.isPinching = true;
this.zoomStartScale = this.getDistance(e.touches[0], e.touches[1]);
return;
}
this.touchState.isDragging = false;
const now = Date.now();
if (now - this.lastTouchEventTimestamp < 50)
return;
this.lastTouchEventTimestamp = now;
const distance = this.getDistance(e.touches[0], e.touches[1]);
let scale = distance / (this.zoomStartScale || distance);
scale = 1 + (scale - 1) * zoomFactor;
const zoomConfig = this.mapConfig.zoom;
scale = Math.max(zoomConfig.min, Math.min(zoomConfig.max, scale));
if (this.lastScale) {
scale = this.lastScale + (scale - this.lastScale) * smoothingFactor;
}
this.lastScale = scale;
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const rect = this.canvas.wrapperEl.getBoundingClientRect();
const point = new fabric.Point(centerX - rect.left, centerY - rect.top);
let zoom = this.canvas.getZoom() * scale;
zoom = Math.max(zoomConfig.min, Math.min(zoomConfig.max, zoom));
this.canvas.zoomToPoint(point, zoom);
this.updateZoomLevel(zoom);
this.showDisplayViewportMap();
if (zoom > zoomConfig.min && zoom < zoomConfig.max) {
this.zoomStartScale = distance;
}
}
handleSingleTouchMove(e) {
if (!this.touchState.pausePanning && !this.touchState.isPinching) {
this.cancelDebounceClick?.();
if (this.touchTimeout)
clearTimeout(this.touchTimeout);
const touch = e.touches[0];
const xChange = touch.clientX - this.lastPosition.x;
const yChange = touch.clientY - this.lastPosition.y;
if (Math.abs(xChange) > 5 || Math.abs(yChange) > 5) {
this.touchState.isDragging = true;
const delta = new fabric.Point(xChange, yChange);
this.canvas.relativePan(delta);
this.lastPosition = { x: touch.clientX, y: touch.clientY };
this.showDisplayViewportMap();
}
}
}
handleTouchEnd(e) {
e.preventDefault();
this.cancelDebounceClick?.();
if (this.touchTimeout)
clearTimeout(this.touchTimeout);
this.touchState.isTouching = false;
if (this.touchState.isPinching && e.touches.length < 2) {
const touch = e.touches[0];
this.lastPosition = { x: touch.clientX, y: touch.clientY };
}
if (!this.touchState.isDragging &&
!this.touchState.isPinching &&
!this.touchState.isZooming &&
e.changedTouches.length === 1) {
const touch = e.changedTouches[0];
const target = document.elementFromPoint(touch.clientX, touch.clientY);
if (target instanceof HTMLElement) {
const fabricObject = this.canvas.findTarget(e, false);
this.debounceClick?.(fabricObject, touch);
}
}
this.resetActionEvent();
}
resetActionEvent() {
this.zoomStartScale = 0;
this.touchState.isMoving = false;
this.touchState.isDragging = false;
if (this.touchState.isPinching ||
this.isZooming() ||
this.touchState.pausePanning ||
this.touchState.isDragging) {
setTimeout(() => {
this.touchState.pausePanning = false;
this.isZooming.set(false);
this.touchState.isPinching = false;
}, 150);
}
}
// ============================================================================
// PRIVATE METHODS - MOUSE EVENT HANDLERS
// ============================================================================
handleMouseDown(options) {
this.cancelDebounceClick?.();
this.touchState.isTouching = true;
const mouseEvent = options.e;
this.lastPosition = { x: mouseEvent.clientX, y: mouseEvent.clientY };
this.touchTimeout = setTimeout(() => {
if (this.touchState.isTouching) {
this.touchState.isMoving = true;
this.cancelDebounceClick?.();
}
}, LONG_TOUCH_DURATION);
}
handleMouseUp(options) {
const mouseEvent = options.e;
this.touchState.isTouching = false;
if (Math.abs(mouseEvent.movementX) <= CLICK_THRESHOLD &&
Math.abs(mouseEvent.movementY) <= CLICK_THRESHOLD &&
!this.touchState.isMoving &&
!this.touchState.isPinching &&
!this.touchState.isDragging) {
const selectedObject = options.target;
this.cancelDebounceClick?.();
this.debounceClick?.(selectedObject, options.e);
}
this.resetActionEvent();
}
handleMouseMove(options) {
if (!this.touchState.isMoving ||
this.touchState.isPinching ||
this.isZooming())
return;
const client = this.getClientPosition(options);
const xChange = client.x - this.lastPosition.x;
const yChange = client.y - this.lastPosition.y;
if (Math.abs(xChange) > 5 || Math.abs(yChange) > 5) {
const delta = new fabric.Point(xChange, yChange);
this.canvas.relativePan(delta);
this.lastPosition = client;
this.showDisplayViewportMap();
}
}
handleZoom(event) {
this.cancelDebounceClick?.();
if (this.isZooming())
return;
const e = event.e;
let zoom = this.canvas.getZoom();
zoom *= 0.999 ** e.deltaY;
const zoomConfig = this.mapConfig.zoom;
zoom = Math.max(zoomConfig.min, Math.min(zoomConfig.max, zoom));
this.canvas.zoomToPoint(new fabric.Point(e.offsetX, e.offsetY), zoom);
this.updateZoomLevel(zoom);
this.showDisplayViewportMap();
e.preventDefault();
e.stopPropagation();
}
handleGesture(event) {
const e = event.e;
const zoomFactor = 0.5;
if (!e.touches || e.touches.length !== 2)
return;
this.touchState.pausePanning = true;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2));
if (!this.zoomStartScale) {
this.zoomStartScale = distance;
}
else {
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
const point = new fabric.Point(centerX, centerY);
let scale = distance / this.zoomStartScale;
scale = 1 + (scale - 1) * zoomFactor;
this.zoomStartScale = distance;
let zoom = this.canvas.getZoom() * scale;
const zoomConfig = this.mapConfig.zoom;
zoom = Math.max(zoomConfig.min, Math.min(zoomConfig.max, zoom));
this.canvas.zoomToPoint(point, zoom);
}
e.preventDefault();
e.stopPropagation();
}
handleDrag(event) {
const e = event.e;
if (this.touchState.pausePanning ||
!e.touches ||
e.touches.length === 0 ||
this.isZooming())
return;
e.preventDefault();
const touch = e.touches[0];
this.currentPosition = { x: touch.clientX, y: touch.clientY };
const xChange = this.currentPosition.x - this.lastPosition.x;
const yChange = this.currentPosition.y - this.lastPosition.y;
if (Math.abs(xChange) <= 50 && Math.abs(yChange) <= 50) {
const delta = new fabric.Point(xChange, yChange);
this.canvas.relativePan(delta);
}
this.lastPosition = this.currentPosition;
}
// ============================================================================
// PUBLIC METHODS - IMAGE MANAGEMENT
// ============================================================================
loadBackgroundImage(pathImg, callback) {
if (!this.canvas)
return;
this.renderService.loadBackgroundImage(this.canvas, this.mapConfig, this.viewDimensions, pathImg, (mapWidth) => {
this.mapWidth = mapWidth;
this.renderFrame();
this.canvasReady.set(true);
callback?.();
});
}
// ============================================================================
// PUBLIC METHODS - RENDERING
// ============================================================================
renderFrame() {
this.workerRenderValue?.() ?? this.canvas?.renderAll();
this.animationFrameId = requestAnimationFrame(() => this.renderFrame());
}
stopRenderFrame() {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
showMap() {
this.canvas.getElement().style.visibility = 'visible';
this.canvas.renderOnAddRemove = true;
}
// ============================================================================
// PUBLIC METHODS - MINIMAP
// ============================================================================
initializeMiniMap() {
if (!this.canvas)
return;
this.miniMapCanvas = this.miniMapService.initializeMiniMap(this.mapWidth, {
width: this.viewDimensions.width,
height: this.viewDimensions.height,
scale: this.viewDimensions.scale || 1
}, this.mapConfig);
this.miniMapService.renderMiniMap(this.canvas);
this.showDisplayViewportMap();
}
showDisplayViewportMap() {
if (!this.canvas || !this.miniMapCanvas)
return;
this.miniMapService.showViewportRect(this.canvas.viewportTransform, this.canvas.getWidth(), this.canvas.getHeight(), this.canvas.getZoom(), {
width: this.viewDimensions.width,
height: this.viewDimensions.height,
scale: this.viewDimensions.scale || 1
}, this.mapConfig, this.theme);
}
// ============================================================================
// PUBLIC METHODS - ZOOM & NAVIGATION
// ============================================================================
zoomAnimated(point, zoomLevel, duration = ZOOM_ANIMATION_DURATION, callback) {
if (this.isZooming() || !this.canvas)
return;
this.isZooming.set(true);
const canvas = this.canvas;
const startZoom = canvas.getZoom();
const endZoom = zoomLevel;
const startPoint = canvas.getVpCenter();
const endPoint = new fabric.Point(point.x, point.y);
const vpw = canvas.getWidth();
const vph = canvas.getHeight();
const throttledUpdate = throttle((value) => {
const zoom = startZoom + (endZoom - startZoom) * value;
canvas.setZoom(zoom);
this.updateZoomLevel(zoom);
const newX = startPoint.x + (endPoint.x - startPoint.x) * value;
const newY = startPoint.y + (endPoint.y - startPoint.y) * value;
const currentZoom = zoom;
const viewportWidth = vpw / currentZoom;
const viewportHeight = vph / currentZoom;
const x = newX - viewportWidth / 2;
const y = newY - viewportHeight / 2;
canvas.setViewportTransform([zoom, 0, 0, zoom, -x * zoom, -y * zoom]);
this.showDisplayViewportMap();
}, 16);
fabric.util.animate({
startValue: 0,
endValue: 1,
duration: duration,
onChange: value => {
throttledUpdate(value);
},
onComplete: () => {
this.isZooming.set(false);
callback?.();
},
});
}
adjustViewToCenter(callback, widthPercentage = 0.9) {
const { width, height } = this.viewDimensions;
const centerPoint = this.centerPointCanvas();
const zoomLevel = width < height
? width / this.mapWidth
: height / this.scaleValue(this.mapConfig.height ?? 0);
this.zoomAnimated(centerPoint, zoomLevel * widthPercentage);
setTimeout(() => {
callback?.();
}, 550);
}
resetZoomCanvas() {
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.canvas.setZoom(1);
this.updateZoomLevel(1);
this.showDisplayViewportMap();
}
handleZoomBtn(type) {
if (!this.canvas)
return;
const currentZoom = this.canvas.getZoom();
const viewportTransform = this.canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
// Calculate the center of the current viewport
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
const viewportCenterX = -viewportTransform[4] / currentZoom + canvasWidth / (2 * currentZoom);
const viewportCenterY = -viewportTransform[5] / currentZoom + canvasHeight / (2 * currentZoom);
const centerPoint = new fabric.Point(viewportCenterX, viewportCenterY);
const zoomStepValue = this.mapConfig.zoom.step;
const zoomStep = zoomStepValue * currentZoom;
const targetZoom = type === 'zoomIn' ? currentZoom + zoomStep : currentZoom - zoomStep;
this.zoomAnimated(centerPoint, targetZoom);
}
zoomAndPanToObject(object, zoom = ZOOM_THRESHOLD, callback) {
const targetObject = object;
const objectCenter = targetObject.getCenterPoint();
this.zoomAnimated(objectCenter, zoom, ZOOM_ANIMATION_DURATION, callback);
}
zoomToObjectBufferHeight(object, zoom = ZOOM_THRESHOLD, heightPrice = 0, callback) {
const targetObject = object;
const objectCenter = targetObject.getCenterPoint();
const heightBuffer = this.viewDimensions.height * 0.3;
objectCenter.setY(objectCenter.y + (heightBuffer + heightPrice) / 2);
this.zoomAnimated(objectCenter, zoom, ZOOM_ANIMATION_DURATION, callback);
}
// ============================================================================
// PUBLIC METHODS - CANVAS MANAGEMENT
// ============================================================================
resetHeightCanvas() {
this.canvas.setHeight(this.viewDimensions.height);
}
setHeightCanvas(heightBuffer = 0) {
this.canvas.setHeight(this.viewDimensions.height + heightBuffer);
}
centerPointCanvas() {
const { width, height } = this.mapConfig;
return new fabric.Point(this.scaleValue(width / 2), this.scaleValue(height / 2));
}
removeGroupCanvas() {
const objects = this.canvas.getObjects();
const miniMapObjects = this.miniMapCanvas?.getObjects() ?? [];
for (let i = objects.length - 1; i >= 0; i--) {
this.canvas.remove(objects[i]);
}
for (let i = miniMapObjects.length - 1; i >= 0; i--) {
this.miniMapCanvas.remove(miniMapObjects[i]);
}
}
createObjectFromPoint(point) {
const selectedObject = new fabric.Object({
left: point.x,
top: point.y,
width: 1,
height: 1,
});
return selectedObject;
}
findObjectByGrade(id, shapeIndex) {
if (!id || typeof shapeIndex !== 'number')
return;
const selectedObject = this.canvas
.getObjects()
.find((obj) => obj.id === id && obj.shapeIndex === shapeIndex);
return selectedObject;
}
removeObjectCaching() {
this.canvas?.getObjects().forEach(obj => {
obj.set('objectCaching', false);
});
}
// ============================================================================
// PUBLIC METHODS - EVENT HANDLERS
// ============================================================================
setDebounceClickHandler(handler) {
this.debouncedClickHandler = debounce(handler, DEBOUNCE_DELAY$1);
this.debounceClick = this.debouncedClickHandler;
}
setWorkerRenderValue(handler) {
this.workerRenderValue = handler;
}
cancelDebounceClick() {
this.debouncedClickHandler?.cancel();
}
// ============================================================================
// PUBLIC METHODS - UTILITY
// ============================================================================
scaleValue(value) {
return scaleValue(value, this.viewDimensions.scale);
}
scalePoint(point) {
return scalePoint(point, this.viewDimensions.scale);
}
handleScalePoint(points) {
return scalePoints(points, this.viewDimensions.scale);
}
scalePointGrade(point, mapConfigGrade) {
const scaleMap = this.mapConfig.width / mapConfigGrade.width;
return {
x: point.x * scaleMap * this.viewDimensions.scale,
y: point.y * scaleMap * this.viewDimensions.scale,
};
}
calculateFramesDimensions(points) {
if (!points || points.length < 3) {
return {
width: 1,
height: 1,
left: 1,
top: 1,
right: 1,
bottom: 1,
centerX: 1,
centerY: 1,
};
}
const minX = Math.min(...points.map(point => point.x));
const maxX = Math.max(...points.map(point => point.x));
const minY = Math.min(...points.map(point => point.y));
const maxY = Math.max(...points.map(point => point.y));
const width = maxX - minX;
const height = maxY - minY;
const centerX = minX + width / 2;
const centerY = minY + height / 2;
return {
width,
height,
left: minX,
top: minY,
right: maxX,
bottom: maxY,
centerX,
centerY,
};
}
getVisibleBounds(buffer = 0) {
if (!this.canvas) {
return { left: 0, top: 0, right: 0, bottom: 0, zoom: 1 };
}
const zoom = this.canvas.getZoom();
const vpt = this.canvas.viewportTransform;
return {
left: -(vpt?.[4] ?? 0) / zoom - buffer,
top: -(vpt?.[5] ?? 0) / zoom - buffer,
right: (this.canvas.width ?? 0) / zoom - (vpt?.[4] ?? 0) / zoom + buffer,
bottom: (this.canvas.height ?? 0) / zoom - (vpt?.[5] ?? 0) / zoom + buffer,
zoom,
};
}
isInBounds(seatPoint, bounds) {
return (seatPoint.left >= bounds.left &&
seatPoint.left <= bounds.right &&
seatPoint.top >= bounds.top &&
seatPoint.top <= bounds.bottom);
}
getClientPosition(options) {
let clientX, clientY;
if (options.e.type.startsWith('touch')) {
const touch = options.e.touches[0] ||
options.e.changedTouches[0];
clientX = touch.clientX;
clientY = touch.clientY;
}
else {
const mouseEvent = options.e;
clientX = mouseEvent.clientX;
clientY = mouseEvent.clientY;
}
return { x: clientX, y: clientY };
}
getDistance(touch1, touch2) {
return Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2));
}
// ============================================================================
// CLEANUP METHODS
// ============================================================================
removeEventListeners() {
if (!this.canvas)
return;
this.canvas.off('mouse:wheel');
this.canvas.off('touch:gesture');
this.canvas.off('selection:created');
this.canvas.off('selection:cleared');
this.canvas.off('touch:drag');
this.canvas.off('mouse:down');
this.canvas.off('mouse:move');
this.canvas.off('mouse:up');
if (this.canvas.wrapperEl) {
this.canvas.wrapperEl.removeEventListener('touchstart', this.handleTouchStart);
this.canvas.wrapperEl.removeEventListener('touchmove', this.handleTouchMove);
this.canvas.wrapperEl.removeEventListener('touchend', this.handleTouchEnd);
}
}
removeCanvasMap() {
if (this.canvas) {
this.canvas.clear();
this.canvas.dispose();
this.canvas = undefined;
}
if (this.miniMapCanvas) {
this.miniMapCanvas.clear();
this.miniMapCanvas.dispose();
this.miniMapCanvas = undefined;
}
this.stopRenderFrame();
}
clearMapService() {
this.removeEventListeners();
this.removeCanvasMap();
this.stopRenderFrame();
this.miniMapService.cleanup();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapCanvasService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapCanvasService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapCanvasService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
/**
* Service responsible for creating and managing price displays on venue maps.
* Handles the creation of sticky price elements and price-related rendering logic.
*/
class VenueMapPriceService {
/**
* Finds the circle element within a grade shape.
*/
findCircleInGradeShape(gradeShape) {
return gradeShape
.getObjects()
.find(obj => obj.type === 'group' && obj.subType === 'circle');
}
/**
* Calculates the height offset for circle elements.
*/
calculateCircleHeight(subId, gradeShape, mapType, circle) {
return subId === gradeShape?.subId && mapType !== VenueMapType.GRADE
? (circle?.getScaledHeight() ?? 0) / 2
: 0;
}
/**
* Finds the text bubble group within a shape.
*/
findTextBubbleGroup(shape) {
return shape.getObjects().find(obj => obj.type === 'group');
}
/**
* Calculates the position for sticky price elements.
*/
calculateStickyPosition(circle, circleHeight = 0, textBubbleGroup) {
return {
left: circle?.left,
top: (circle?.top ?? 0) -
circleHeight -
(textBubbleGroup?.getScaledHeight() ?? 0),
};
}
/**
* Updates the content of text bubble elements.
*/
updateTextBubbleContent(grade, shape) {
shape.getObjects().forEach(obj => {
const objGroup = obj;
if (objGroup.subType === 'textBubbleGroup') {
this.updateTextContent(objGroup, grade);
}
});
}
/**
* Updates the text content within a text bubble group.
*/
updateTextContent(textBubbleGroup, grade) {
textBubbleGroup.getObjects().forEach(textBubble => {
if (textBubble.type === 'text') {
textBubble.set('text', Number(grade.price).toLocaleString());
}
});
}
/**
* Creates a sticky price element for grade display.
*/
createStickyPrice(grade, gradeSetting, mapInfo, scale, canvas, theme) {
const { fontSize, bubble, corner, arrow } = mapInfo;
const { circle } = gradeSetting;
const leftCircle = scaleValue(circle.x, scale);
const topCircle = scaleValue(circle.y, scale);
const price = Number(grade.price);
const priceText = new fabric.Text(price.toLocaleString(), {
fontSize: scaleValue(fontSize, scale),
fontFamily: 'Arial',
fontWeight: 'bold',
fill: theme.priceTextColor,
textAlign: 'center',
originX: 'center',
originY: 'center',
});
priceText.set('type', 'text');
canvas.add(priceText);
priceText.setCoords();
const bubbleRect = new fabric.Rect({
width: priceText.getScaledWidth() +
scaleValue(bubble.width, scale),
height: priceText.getScaledHeight() +
scaleValue(bubble.height, scale),
fill: grade.disabled ? undefined : theme.priceBackgroundColor,
stroke: theme.priceBubbleStrokeColor,
rx: scaleValue(corner.width, scale),
ry: scaleValue(corner.width, scale),
originX: 'center',
originY: 'center',
});
canvas.remove(priceText);
const arrowRect = new fabric.Triangle({
width: scaleValue(arrow.width, scale),
height: scaleValue(arrow.height, scale),
fill: grade.disabled ? undefined : theme.priceBackgroundColor,
stroke: theme.priceBubbleStrokeColor,
originX: 'center',
originY: 'top',
left: bubbleRect.left,
top: (bubbleRect.top ?? 0) +
(bubbleRect.height ?? 0) / 2 +
scaleValue(arrow.padding, scale),
angle: 180,
});
const textBubbleGroup = new fabric.Group([bubbleRect, priceText], {
originX: 'center',
originY: 'center',
});
textBubbleGroup.set('subType', 'textBubbleGroup');
const positionStick = {
left: leftCircle,
top: topCircle - textBubbleGroup.getScaledHeight(),
};
const stickyPrice = new fabric.Group([textBubbleGroup, arrowRect], {
...positionStick,
originX: 'center',
originY: 'top',
opacity: 0,
});
const shadowOptions = {
color: theme.shadowColor,
blur: 2,
offsetX: 0.5,
offsetY: 0.5,
};
stickyPrice.set('shadow', new fabric.Shadow(shadowOptions));
stickyPrice.set('subType', 'stickyPrice');
return stickyPrice;
}
/**
* Updates the position of sticky price elements.
*/
updateStickyPricePosition(grade, shape, gradeShape, subId, mapType) {
const circle = this.findCircleInGradeShape(gradeShape);
const circleHeight = this.calculateCircleHeight(subId, gradeShape, mapType, circle);
const textBubbleGroup = this.findTextBubbleGroup(shape);
const position = this.calculateStickyPosition(circle, circleHeight, textBubbleGroup);
this.updateTextBubbleContent(grade, shape);
shape.set('top', position.top);
}
/**
* Renders sticky price elements with proper visibility control.
*/
renderStickyPrice(grade, shape, isVisible, gradeShape, subId, mapType) {
if (shape.subType === 'stickyPrice') {
// Only render (make visible) when show price mode is on
shape.set('opacity', isVisible && mapType === VenueMapType.GRADE_PRICE ? 1 : 0);
if (isVisible && mapType === VenueMapType.GRADE_PRICE) {
this.updateStickyPricePosition(grade, shape, gradeShape, subId, mapType);
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapPriceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapPriceService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapPriceService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Service responsible for creating and managing venue grade visualizations.
* Handles the creation of polygons, circles, and price sticky elements for grades.
*/
class VenueMapGradeService {
gradeIconMultiplier = MULTIPLIER_GRADE_ICON;
canvasService = inject(VenueMapCanvasService);
priceService = inject(VenueMapPriceService);
/**
* Creates a complete grade group containing polygon, circle, and sticky price elements.
*/
createGrade(polygon, circle, stickyPrice, gradeInfo) {
const gradeGroup = new fabric.Group([polygon, circle, stickyPrice], {
originX: 'center',
originY: 'center',
hoverCursor: 'pointer',
selectable: false,
hasBorders: false,
perPixelTargetFind: true,
evented: !gradeInfo['disabled'],
});
gradeGroup.set({
id: gradeInfo['id'].toString(),
subId: v4(),
shapeIndex: Number(gradeInfo['shapeIndex']),
});
return gradeGroup;
}
/**
* Creates and configures sticky elements for grade display based on map type.
*/
createGradeSticky(grade, gradeShape, subId, mapType) {
if (!grade)
return;
gradeShape.getObjects().forEach(child => {
if (child.type !== 'group')
return;
const targetChild = child;
this.renderStickyType(grade, targetChild, gradeShape, subId, mapType);
if (subId === gradeShape.subId && mapType !== VenueMapType.GRADE) {
this.renderShape(grade, targetChild, 'circle', true, gradeShape, subId, mapType);
this.renderShape(grade, targetChild, 'stickyPrice', true, gradeShape, subId, mapType);
}
});
}
/**
* Renders sticky elements based on map type.
*/
renderStickyType(grade, shape, gradeShape, subId, mapType) {
if (mapType === VenueMapType.GRADE) {
this.renderShape(grade, shape, 'circle', true, gradeShape, subId, mapType);
this.renderShape(grade, shape, 'stickyPrice', false, gradeShape, subId, mapType);
return;
}
this.renderShape(grade, shape, 'circle', false, gradeShape, subId, mapType);
this.renderShape(grade, shape, 'stickyPrice', true, gradeShape, subId, mapType);
}
/**
* Renders individual shape elements with visibility control.
*/
renderShape(grade, shape, type, isVisible, gradeShape, subId, mapType) {
if (type === shape.subType) {
if (shape.subType === 'stickyPrice') {
// Delegate price rendering to price service
this.priceService.renderStickyPrice(grade, shape, isVisible, gradeShape, subId, mapType);
}
else {
// Handle other shape types (like circles)
shape.set('opacity', isVisible ? 1 : 0);
}
}
}
/**
* Creates a sticky price element for grade display.
*/
createStickyPrice(grade, gradeSetting, mapInfo, theme) {
return this.priceService.createStickyPrice(grade, gradeSetting, mapInfo, this.canvasService.viewDimensions.scale, this.canvasService.canvas, theme);
}
/**
* Creates a polygon shape for the grade area.
*/
createPolygon(gradeSetting, disabled = false) {
const { points, color } = gradeSetting;
const scaledPoints = scalePoints(points ?? [], this.canvasService.viewDimensions.scale);
this.renderGrade(points, color, disabled);
return new fabric.Polygon(scaledPoints, {
fill: color,
stroke: undefined,
strokeWidth: 0,
opacity: 0.01,
selectable: false,
evented: !disabled,
hoverCursor: 'pointer',
originX: 'center',
originY: 'center',
});
}
/**
* Renders a polygon with image color instead of rendering the actual image.
*/
renderGrade(points, color, disabled = false) {
if (!points?.length)
return;
const imagePoints = scalePoints(points, this.canvasService.viewDimensions.scale);
const polygon = new fabric.Polygon(imagePoints, {
fill: color,
stroke: undefined,
strokeWidth: 0,
opacity: disabled ? 0.5 : 1,
selectable: false,
evented: false,
originX: 'center',
originY: 'center',
globalCompositeOperation: disabled ? 'soft-light' : 'hard-light',
});
this.canvasService.canvas.add(polygon);
this.canvasService.canvas.sendToBack(polygon);
}
/**
* Creates a circle element for grade display.
*/
createCircle(grade, gradeSetting, mapInfo, theme) {
const resolvedTheme = theme;
const { color, circle } = gradeSetting;
const { circle: mapCircle, fontSize } = mapInfo;
const circleShape = new fabric.Rect({
fill: grade.disabled ? undefined : color,
stroke: resolvedTheme.gradeStrokeColor,
strokeWidth: this.canvasService.scaleValue(mapCircle.stroke),
originX: 'center',
originY: 'center',
height: this.canvasService.scaleValue(mapCircle.height * this.gradeIconMultiplier),
width: this.canvasService.scaleValue(mapCircle.width * this.gradeIconMultiplier),
rx: this.canvasService.scaleValue(mapCircle.height),
ry: this.canvasService.scaleValue(mapCircle.height),
});
const text = new fabric.Text(grade.abbreviation.toString(), {
fontSize: this.canvasService.scaleValue(fontSize) * this.gradeIconMultiplier,
fontFamily: 'Roboto',
fontWeight: 700,
top: 0,
fill: circle.textColor,
textAlign: 'center',
originX: 'center',
originY: 'center',
});
const leftCircle = this.canvasService.scaleValue(circle.x);
const topCircle = this.canvasService.scaleValue(circle.y);
const groupCircle = new fabric.Group(text ? [circleShape, text] : [circleShape], {
left: leftCircle,
top: topCircle,
originX: 'center',
originY: 'center',
selectable: false,
hoverCursor: 'pointer',
});
groupCircle.set('subType', 'circle');
return groupCircle;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapGradeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapGradeService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapGradeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
const DEBOUNCE_DELAY = 250;
const MAP_FOCUS_DELAY = 1500;
class VenueMapComponent {
grades = input([]);
reloadMap = input(true);
focusedGradePart = input();
mapConfiguration = input();
theme = input();
// Signal Outputs (Angular 20 feature)
selectGrade = output();
// Template References
canvasRef;
mapContainer;
// Public Properties
VenueMapType = VenueMapType;
// Signal-based State Management
gradeService = inject(VenueMapGradeService);
canvasService = inject(VenueMapCanvasService);
// Computed Signals
resolvedMapConfiguration = computed(() => {
const config = this.mapConfiguration();
const defaultMapConfiguration = cloneDeep(DEFAULT_MAP_CONFIG);
return merge(defaultMapConfiguration, config ?? {});
});
resolvedTheme = computed(() => {
const themeConfig = this.theme();
return merge(cloneDeep(DEFAULT_VENUE_MAP_THEME), themeConfig ?? {});
});
viewDimensions = computed(() => {
const mapHeight = window.innerHeight;
const config = this.resolvedMapConfiguration();
return {
width: this.mapWidth(),
height: mapHeight,
scale: mapHeight / config.height,
};
});
// Signal State
venueMapType = signal(VenueMapType.GRADE);
isLoading = signal(false);
mapWidth = signal(0);
gradeSelectCanvas = signal(undefined);
gradeSelect = signal(undefined);
// Private Properties
pendingMapWidth = 0;
isRendering = false;
// Debounced Methods
debounceGradeMapShow = debounce(() => this.renderGradeSticky(), DEBOUNCE_DELAY);
debounceUpdateMapWidth = debounce(() => this.updateMapWidth(), DEBOUNCE_DELAY);
constructor() {
// Effect for grades changes
effect(() => {
const gradesValue = this.grades();
const mapWidthValue = this.mapWidth();
if (gradesValue.length > 0 && mapWidthValue > 0) {
this.debounceUpdateMapWidth();
}
});
// Effect for map configuration changes
effect(() => {
this.mapConfiguration(); // Track changes
this.debounceUpdateMapWidth();
});
// Effect for theme changes
effect(() => {
this.theme(); // Track changes
this.debounceUpdateMapWidth();
});
}
// ============================================================================
// LIFECYCLE METHODS
// ============================================================================
ngOnInit() {
this.initializeCanvasHandlers();
}
ngOnDestroy() {
this.cleanupResources();
}
ngAfterViewInit() {
this.debounceUpdateMapWidth();
}
// ============================================================================
// PRIVATE INITIALIZATION METHODS
// ============================================================================
/**
* Initializes canvas event handlers and observables.
*/
initializeCanvasHandlers() {
this.canvasService.getCanvasReadyObservable().subscribe();
this.canvasService.setDebounceClickHandler(this.onGradeClick.bind(this));
}
/**
* Cleans up resources when component is destroyed.
*/
cleanupResources() {
this.canvasService.clearMapService();
this.debounceGradeMapShow.cancel();
this.debounceUpdateMapWidth.cancel();
}
// ============================================================================
// MAP RENDERING AND MANAGEMENT METHODS
// ============================================================================
/**
* Updates the map width and triggers re-rendering if needed.
*/
updateMapWidth() {
if (!this.mapContainer?.nativeElement)
return;
const newMapWidth = this.mapContainer.nativeElement.offsetWidth;
if (!newMapWidth)
return;
if (this.isRendering) {
this.pendingMapWidth = newMapWidth;
return;
}
this.startRenderProcess(newMapWidth);
}
/**
* Starts the rendering process with the new map width.
*/
startRenderProcess(mapWidth) {
this.isRendering = true;
this.mapWidth.set(mapWidth);
this.processRenderQueue();
}
/**
* Processes the render queue and handles pending width changes.
*/
async processRenderQueue() {
await this.handleMapSettingsChange();
if (this.pendingMapWidth) {
const nextMapWidth = this.pendingMapWidth;
this.pendingMapWidth = 0;
this.mapWidth.set(nextMapWidth);
return this.processRenderQueue();
}
this.isRendering = false;
}
/**
* Handles changes to map settings and triggers re-initialization.
*/
async handleMapSettingsChange() {
if (this.reloadMap()) {
this.canvasService.removeCanvasMap();
}
this.initializeMapData();
this.canvasService.initialize(this.viewDimensions(), this.resolvedMapConfiguration(), this.resolvedTheme());
this.setupCanvas();
await this.resetAndRenderGrades();
}
/**
* Initializes map data and view dimensions.
*/
initializeMapData() {
// Data is now handled by computed signals
// resolvedMapConfiguration and resolvedTheme are computed automatically
}
/**
* Sets up the canvas and loads background image.
*/
setupCanvas() {
if (this.reloadMap() || !this.canvasService.canvas) {
this.canvasService.setupCanvas(this.canvasRef.nativeElement);
}
const config = this.resolvedMapConfiguration();
this.canvasService.loadBackgroundImage(config.path);
}
/**
* Resets canvas state and renders all grades.
*/
async resetAndRenderGrades() {
this.isLoading.set(true);
this.resetCanvasState();
this.renderGrades();
this.renderGradeSticky();
await this.showCenterFocusMap();
this.canvasService.showMap();
this.isLoading.set(false);
}
/**
* Resets the canvas state and clears selections.
*/
resetCanvasState() {
this.canvasService.resetZoomCanvas();
this.canvasService.resetHeightCanvas();
this.canvasService.removeGroupCanvas();
this.clearGradeSelection();
}
/**
* Clears the current grade selection.
*/
clearGradeSelection() {
this.gradeSelectCanvas.set(undefined);
this.gradeSelect.set(undefined);
}
/**
* Renders all grades on the canvas.
*/
renderGrades() {
const grades = this.grades();
grades.forEach(grade => grade.settings.forEach((shape, index) => {
const gradeGroup = this.createGradeGroup(grade, shape, index);
this.canvasService.canvas.add(gradeGroup);
}));
}
/**
* Creates a grade group with all its components.
*/
createGradeGroup(grade, shape, index) {
const gradeInfo = this.buildGradeInfo(grade, index);
return this.gradeService.createGrade(this.gradeService.createPolygon(shape, grade.disabled), this.gradeService.createCircle(grade, shape, this.resolvedMapConfiguration(), this.resolvedTheme()), this.gradeService.createStickyPrice(grade, shape, this.resolvedMapConfiguration(), this.resolvedTheme()), gradeInfo);
}
/**
* Builds grade information object for grade creation.
*/
buildGradeInfo(grade, index) {
return {
id: grade.id,
subId: v4(),
shapeIndex: index.toString(),
disabled: !!grade.disabled,
};
}
/**
* Renders sticky elements for all grades based on current map type.
*/
renderGradeSticky() {
this.canvasService.canvas.getObjects().forEach(gradeShape => {
const gradeShapeTyped = gradeShape;
const gradeId = gradeShapeTyped.id;
if (gradeId) {
const grade = this.findGradeById(gradeId);
if (grade) {
this.gradeService.createGradeSticky(grade, gradeShapeTyped, this.gradeSelectCanvas()?.subId ?? '', this.venueMapType());
}
}
});
}
/**
* Finds a grade by its ID.
*/
findGradeById(gradeId) {
const grades = this.grades();
return grades.find(grade => grade.id === gradeId);
}
// ============================================================================
// MAP FOCUS AND INTERACTION METHODS
// ============================================================================
/**
* Shows the center focus map with animation delay.
*/
async showCenterFocusMap() {
return new Promise(resolve => {
setTimeout(() => {
this.renderMapFocus(resolve);
}, MAP_FOCUS_DELAY);
});
}
/**
* Renders the map focus and initializes mini map.
*/
renderMapFocus(resolve) {
this.canvasService.initializeMiniMap();
this.canvasService.adjustViewToCenter(() => {
resolve();
});
}
/**
* Handles grade click events.
*/
onGradeClick(selectedObject) {
if (selectedObject?.type === 'group') {
this.handleGradeSelection(selectedObject);
}
}
/**
* Handles the selection of a grade.
*/
handleGradeSelection(selectedGrade) {
if (this.isGradeAlreadySelected(selectedGrade))
return;
this.selectGradeInternal(selectedGrade);
this.updateGradeDisplay();
}
/**
* Updates the grade display after selection.
*/
updateGradeDisplay() {
this.renderGradeSticky();
this.canvasService.showDisplayViewportMap();
}
/**
* Checks if the grade is already selected.
*/
isGradeAlreadySelected(selectedGrade) {
const currentSelection = this.gradeSelectCanvas();
return selectedGrade.subId === currentSelection?.subId;
}
/**
* Selects a grade and updates the UI state.
*/
selectGradeInternal(selectedGrade) {
this.canvasService.zoomToObjectBufferHeight(selectedGrade);
const grade = this.findGradeById(selectedGrade.id ?? '');
if (grade) {
this.gradeSelect.set(cloneDeep(grade));
this.gradeSelectCanvas.set(selectedGrade);
}
}
// ============================================================================
// PUBLIC METHODS
// ============================================================================
/**
* Handles changes to grade map display.
*/
onChangeGradeMapShow() {
this.cancelDebounceGradeMapShow();
this.triggerDebounceGradeMapShow();
}
/**
* Handles grade selection from external components.
*/
onGradeSelected(selectedObject) {
const grade = this.findGradeById(selectedObject.id ?? '');
if (!grade)
return;
const shapeIndex = selectedObject.shapeIndex;
this.selectGrade.emit({ grade, shapeIndex });
}
/**
* Handles seat grade selection.
*/
onSelectSeatGrade() {
const currentGrade = this.gradeSelect();
const currentCanvas = this.gradeSelectCanvas();
if (!currentGrade || !currentCanvas)
return;
const shapeIndex = currentCanvas.shapeIndex;
this.selectGrade.emit({ grade: currentGrade, shapeIndex });
}
/**
* Handles changes to grade map type (grade view vs price view).
*/
onChangeGradeMapType(isPrice) {
this.venueMapType.set(isPrice
? VenueMapType.GRADE_PRICE
: VenueMapType.GRADE);
this.onChangeGradeMapShow();
}
// ============================================================================
// PRIVATE HELPER METHODS
// ============================================================================
/**
* Cancels the debounced grade map show operation.
*/
cancelDebounceGradeMapShow() {
this.debounceGradeMapShow.cancel();
}
/**
* Triggers the debounced grade map show operation.
*/
triggerDebounceGradeMapShow() {
this.debounceGradeMapShow();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.5", type: VenueMapComponent, isStandalone: true, selector: "ngx-venue-map", inputs: { grades: { classPropertyName: "grades", publicName: "grades", isSignal: true, isRequired: false, transformFunction: null }, reloadMap: { classPropertyName: "reloadMap", publicName: "reloadMap", isSignal: true, isRequired: false, transformFunction: null }, focusedGradePart: { classPropertyName: "focusedGradePart", publicName: "focusedGradePart", isSignal: true, isRequired: false, transformFunction: null }, mapConfiguration: { classPropertyName: "mapConfiguration", publicName: "mapConfiguration", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectGrade: "selectGrade" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["htmlCanvas"], descendants: true, static: true }, { propertyName: "mapContainer", first: true, predicate: ["mapContainer"], descendants: true }], ngImport: i0, template: "<div #mapContainer class=\"venue__container\">\n <div class=\"venue__grades\">\n <canvas #htmlCanvas id=\"c\"></canvas>\n </div>\n <canvas\n #miniMap\n class=\"venue__minimap\"\n id=\"miniMap\"\n [class.hidden]=\"!canvasService.showMiniMap()\"\n ></canvas>\n <div\n class=\"venue__switch-price\"\n *ngIf=\"\n !isLoading() && canvasService.canvas\n \"\n >\n <span class=\"venue__switch-label\">Show Price</span>\n <app-switch\n [checked]=\"venueMapType() === VenueMapType.GRADE_PRICE\"\n [handleOnChange]=\"onChangeGradeMapType.bind(this)\"\n className=\"bg-black\"\n ></app-switch>\n </div>\n <div\n class=\"venue__zoom-controls\"\n >\n <button\n [disabled]=\"canvasService.disableZoomIn()\"\n (click)=\"canvasService.handleZoomBtn('zoomIn')\"\n >\n <svg\n [class.venue__zoom-disabled]=\"canvasService.disableZoomIn()\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M9.6 0H6.4V6.4H0V9.6H6.4V16H9.6V9.6H16V6.4H9.6V0Z\"\n fill=\"#707086\"\n />\n </svg>\n </button>\n <hr class=\"venue__zoom-divider\" />\n <button\n [disabled]=\"canvasService.disableZoomOut()\"\n (click)=\"canvasService.handleZoomBtn('zoomOut')\"\n >\n <svg\n [class.venue__zoom-disabled]=\"canvasService.disableZoomOut()\"\n width=\"16\"\n height=\"3\"\n viewBox=\"0 0 16 3\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M0 0V3H16V0H0Z\" fill=\"#707086\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".venue__container{width:100%}.venue__grades{position:relative}.venue__switch-price{position:absolute;bottom:1.25rem;left:1.25rem;display:flex;align-items:center;background-color:#fff;padding:.5rem;border-radius:1.5rem;box-shadow:0 .125rem .25rem #0000001a}.venue__switch-label{color:#707086;font-size:.875rem;margin-right:.5rem}.venue__minimap{width:6rem;height:5rem;position:absolute;top:.5rem;right:.5rem;padding:.5rem;background-color:#f8f8fa;border-radius:.5rem}.venue__minimap.hidden{display:none}.venue__zoom-controls{display:flex;flex-direction:column;height:82px;width:2.25rem;background-color:#fff;border-radius:9999px;position:absolute;bottom:1.25rem;right:1.25rem;box-shadow:0 .125rem .25rem #0000001a}.venue__zoom-controls button{display:flex;justify-content:center;align-items:center;height:2.5625rem;padding:0;margin:0;padding-block:0;cursor:pointer;border-color:transparent;background-color:transparent;background-image:none}.venue__zoom-controls .venue__zoom-divider{width:2.25rem;border:1px solid #f8f8fa}.venue__zoom-controls .venue__zoom-disabled{opacity:.25;cursor:none}.venue__switch-item{width:3.25rem;border-color:#707086;border:2px solid #707086}\n"], dependencies: [{ kind: "component", type: SwitchComponent, selector: "app-switch", inputs: ["handleOnChange", "className", "checked"], outputs: ["checkedChange"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }], encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: VenueMapComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-venue-map', standalone: true, imports: [SwitchComponent, CommonModule, FormsModule], encapsulation: ViewEncapsulation.None, template: "<div #mapContainer class=\"venue__container\">\n <div class=\"venue__grades\">\n <canvas #htmlCanvas id=\"c\"></canvas>\n </div>\n <canvas\n #miniMap\n class=\"venue__minimap\"\n id=\"miniMap\"\n [class.hidden]=\"!canvasService.showMiniMap()\"\n ></canvas>\n <div\n class=\"venue__switch-price\"\n *ngIf=\"\n !isLoading() && canvasService.canvas\n \"\n >\n <span class=\"venue__switch-label\">Show Price</span>\n <app-switch\n [checked]=\"venueMapType() === VenueMapType.GRADE_PRICE\"\n [handleOnChange]=\"onChangeGradeMapType.bind(this)\"\n className=\"bg-black\"\n ></app-switch>\n </div>\n <div\n class=\"venue__zoom-controls\"\n >\n <button\n [disabled]=\"canvasService.disableZoomIn()\"\n (click)=\"canvasService.handleZoomBtn('zoomIn')\"\n >\n <svg\n [class.venue__zoom-disabled]=\"canvasService.disableZoomIn()\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M9.6 0H6.4V6.4H0V9.6H6.4V16H9.6V9.6H16V6.4H9.6V0Z\"\n fill=\"#707086\"\n />\n </svg>\n </button>\n <hr class=\"venue__zoom-divider\" />\n <button\n [disabled]=\"canvasService.disableZoomOut()\"\n (click)=\"canvasService.handleZoomBtn('zoomOut')\"\n >\n <svg\n [class.venue__zoom-disabled]=\"canvasService.disableZoomOut()\"\n width=\"16\"\n height=\"3\"\n viewBox=\"0 0 16 3\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M0 0V3H16V0H0Z\" fill=\"#707086\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".venue__container{width:100%}.venue__grades{position:relative}.venue__switch-price{position:absolute;bottom:1.25rem;left:1.25rem;display:flex;align-items:center;background-color:#fff;padding:.5rem;border-radius:1.5rem;box-shadow:0 .125rem .25rem #0000001a}.venue__switch-label{color:#707086;font-size:.875rem;margin-right:.5rem}.venue__minimap{width:6rem;height:5rem;position:absolute;top:.5rem;right:.5rem;padding:.5rem;background-color:#f8f8fa;border-radius:.5rem}.venue__minimap.hidden{display:none}.venue__zoom-controls{display:flex;flex-direction:column;height:82px;width:2.25rem;background-color:#fff;border-radius:9999px;position:absolute;bottom:1.25rem;right:1.25rem;box-shadow:0 .125rem .25rem #0000001a}.venue__zoom-controls button{display:flex;justify-content:center;align-items:center;height:2.5625rem;padding:0;margin:0;padding-block:0;cursor:pointer;border-color:transparent;background-color:transparent;background-image:none}.venue__zoom-controls .venue__zoom-divider{width:2.25rem;border:1px solid #f8f8fa}.venue__zoom-controls .venue__zoom-disabled{opacity:.25;cursor:none}.venue__switch-item{width:3.25rem;border-color:#707086;border:2px solid #707086}\n"] }]
}], ctorParameters: () => [], propDecorators: { canvasRef: [{
type: ViewChild,
args: ['htmlCanvas', { static: true }]
}], mapContainer: [{
type: ViewChild,
args: ['mapContainer']
}] } });
/*
* Public API Surface of ngx-venue-map
*/
/**
* Generated bundle index. Do not edit.
*/
export { DEFAULT_MAP_CONFIG, DEFAULT_VENUE_MAP_THEME, VenueMapComponent, VenueMapPriceService, VenueMapType, scalePoint, scalePoints, scaleValue };
//# sourceMappingURL=ngx-venue-map.mjs.map