import { round } from "./utils"; import { Coord } from "./class.coord"; import { ImageMap } from "./class.image-map"; import type P5 from "p5"; export type Shape = "empty" | "rect" | "circle" | "poly" | "default"; export abstract class Area { /** * @param {Shape} shape the type of area * @param {Coord[]} coords the list of coordinates * @param {string} href the link this area is going to point to * @param {number} id the unique id */ constructor( protected shape: Shape, protected coords: Coord[] = [], protected href: string = "", protected title: string = "", public id: number = 0) { } static fromObject(o: Object): Area { switch ((o).shape) { case 'rect': const r = o as AreaRect; return new AreaRect(r.coords.map(Coord.fromObject), r.href, r.title, r.id); case 'circle': const c = o as AreaCircle return new AreaCircle(c.coords.map(Coord.fromObject), c.radius, c.href, c.title, c.id); case 'poly': const p = o as AreaPoly return new AreaPoly(p.coords.map(Coord.fromObject), p.href, p.title, p.id, p.closed); case 'default': const d = o as AreaDefault return new AreaDefault(d.iMap, d.href, d.title); default: throw 'Not an area' } } getId(): number { return this.id; } setShape(shape: Shape): this { this.shape = shape; return this; } getShape(): Shape { return this.shape; } /** * Adds a coordinate to the coords array, and returns it's new length * @param {Coord} coord coordinate */ addCoord(coord: Coord): number { return this.coords.push(coord); } setCoords(coords: Coord[]): this { this.coords = coords; return this; } getCoords(): Coord[] { return this.coords; } /** * Returns a copy of this area's coordinates list */ drawingCoords(): Coord[] { return this.coords.slice(); } getPoints(): Coord[] { return this.coords; } isEmpty(): boolean { return this.coords.length == 0; } /** * @param {Area} area */ equals(area: Area): boolean { return this.id == area.id; } /** * Retuns a deep copy of this area's coordinates */ copyCoords(): Coord[] { let copy: Coord[] = []; this.coords.forEach((val, index) => { copy[index] = new Coord(val.x, val.y); }); return copy; } updateLastCoord(coord: Coord): this { this.coords[this.coords.length - 1] = coord; return this; } //------------------------ Start Interface Movable ------------------------------- move(coord: Coord): void { let fcoord = this.firstCoord(); if (coord != undefined) { fcoord.add(coord); } } getPosition(): Coord { return this.firstCoord(); } setPosition(coord: Coord): void { let move = this.coords[0].diff(coord); this.move(move); } //------------------------- End Interface Movable -------------------------------- isValidShape(): boolean { return this.isDrawable(); } /** * @param {Coord} coord * @param {number} tolerance * @returns {Coord|false} */ isOverPoint(coord: Coord, tolerance: number): Coord | false { let point = this.getPoints().find(c => { return Coord.dist(coord, c) < tolerance; }); return point ? point : false; } setHref(url: string): this { this.href = url; return this; } getHref(): string { return this.href; } getHrefVerbose(): string { return this.href === "" ? "undefined" : this.href; } setTitle(title: string): this { this.title = title; return this; } getTitle(): string { return this.title; } setId(id: number): this { this.id = id; return this; } firstCoord(): Coord { return this.coords[0]; } htmlCoords(dec: number, scale: number): string { return this.drawingCoords().map(c => { return c.toHtml(dec, scale); }).join(','); } toHtml(scale: number = 1): string { let htmlCoords = this.htmlCoords(0, scale); htmlCoords = htmlCoords ? `coords="${htmlCoords}"` : ""; const href = this.href ? `href="${this.href}"` : "nohref"; const title = this.title ? `title="${this.title}"` : ""; return `${this.href}`; } toSvg(scale = 1): string { return `${this.svgArea(scale)}`; } abstract isDrawable(): boolean; abstract svgArea(scale: number): string; abstract isOver(coord: Coord): boolean; abstract display(p: P5): void; } export class AreaEmpty extends Area { constructor() { super("empty"); } isDrawable(): boolean { return false } svgArea(scale: number): string { return ""; } isOver(coord: Coord): boolean { return false; } display(p: P5): void {} } export class AreaCircle extends Area { /** * @param {Coord[]} coords the list of coordinates * @param {number} radius radius of the circle * @param {string} href the link this area is going to point to * @param {number} id the unique id */ constructor( coords: Coord[] = [], public radius: number = 0, href: string = "", title: string = "", id: number = 0 ) { super("circle", coords, href, title, id); } getCenter(): Coord { return this.firstCoord(); } isValidShape(): boolean { return super.isValidShape() && this.radius > 0; } isDrawable(): boolean { return this.coords.length == 1; } isOver(coord: Coord): boolean { let center = this.getCenter(); return Coord.dist(coord, center) < this.radius; } updateLastCoord(coord: Coord): this { let center = this.getCenter(); this.radius = Coord.dist(center, coord); return this } /** * draw the area to the given p5 instance * @param {P5} p5 */ display(p5: P5): void { p5.ellipse(this.getCenter().x, this.getCenter().y, this.radius * 2); } htmlCoords(dec: number, scale: number): string { return this.getCenter().toHtml(dec, scale) + "," + round(this.radius, dec); } svgArea(scale: number): string { let x = this.coords[0].toStr(0, 'x', scale); let y = this.coords[0].toStr(0, 'y', scale); let r = round(this.radius, 0); return ``; } } export class AreaPoly extends Area { /** * @param {Coord[]} coords the list of coordinates * @param {string} href the link this area is going to point to * @param {int} id the unique id */ constructor( coords: Coord[] = [], href: string = "", title: string = "", id: number = 0, public closed = false ) { super("poly", coords, href, title, id); } isDrawable(): boolean { return this.coords.length >= 2; } isValidShape(): boolean { return super.isValidShape() && this.closed; } isOver(coord: Coord): boolean { let x = coord.x; let y = coord.y; let cornersX = this.coords.map(c => { return c.x }); let cornersY = this.coords.map(c => { return c.y }); let i, j = cornersX.length - 1; let oddNodes = 0; let polyX = cornersX; let polyY = cornersY; for (i = 0; i < cornersX.length; i++) { if ((polyY[i] < y && polyY[j] >= y || polyY[j] < y && polyY[i] >= y) && (polyX[i] <= x || polyX[j] <= x)) { oddNodes ^= (polyX[i] + (y - polyY[i]) / (polyY[j] - polyY[i]) * (polyX[j] - polyX[i]) < x) ? 1 : 0; } j = i; } return Boolean(oddNodes); } isClosable(coord: Coord, tolerance: number): boolean { let dist = Coord.dist(coord, this.firstCoord()); return this.coords.length >= 4 && dist < tolerance; } drawingCoords(): Coord[] { let coords = super.drawingCoords(); if (this.closed) { coords.push(this.firstCoord()); } return coords; } close(): this { this.closed = true; this.coords.pop(); return this; } move(coord: Coord): void { this.coords.map(c => c.add(coord)); } /** * draw the area to the given p5 instance * @param {P5} p5 */ display(p5: P5): void { p5.beginShape(); this.drawingCoords().forEach(c => p5.vertex(c.x, c.y)); p5.endShape(); } svgArea(scale: number): string { let points = this.drawingCoords().map(c => { return c.toStr(0, 'x', scale) + ',' + c.toStr(0, 'y', scale); }).join(' '); return ``; } } export class AreaRect extends AreaPoly { /** * @param {Coord[]} coords the list of coordinates * @param {string} href the link this area is going to point to * @param {number} id the unique id */ constructor( coords: Coord[] = [], href: string = "", title: string = "", id: number = 0 ) { super(coords, href, title, id, true); if (this.coords.length > 0 && this.coords.length < 4) { let coord = this.firstCoord(); this.coords = [ coord, coord.clone(), coord.clone(), coord.clone(), ]; } } isValidShape(): boolean { return super.isValidShape() && !this.isNullArea(); } private isNullArea(): boolean { return this.coords[0].x == this.coords[1].x || this.coords[0].y == this.coords[3].y; } updateLastCoord(coord: Coord): this { this.coords[1].x = coord.x this.coords[2] = coord; this.coords[3].y = coord.y; return this; } } export class AreaDefault extends Area { protected isDefault = true; /** * Constructor * @param {string} href the link this area is going to point to * @param {string} title the title on hover */ constructor( public iMap: ImageMap, href: string = "", title: string = "" ) { super("default", [], href, title); } isDrawable(): boolean { return true; } isOver(): boolean { return true; } /** * draw the area to the given p5 instance * @param {P5} p5 */ display(p5: P5): void { p5.rect(0, 0, this.iMap.width - 1, this.iMap.height - 1); } svgArea(scale: number): string { return ''; } }