|
| 1 | +import { |
| 2 | + Component, |
| 3 | + OnInit, |
| 4 | + Input, |
| 5 | + ViewChild, |
| 6 | + ElementRef, |
| 7 | + EventEmitter, |
| 8 | + Output, |
| 9 | + SimpleChanges, |
| 10 | + OnChanges |
| 11 | +} from '@angular/core'; |
| 12 | + |
| 13 | +import { RenderableMap } from '../../classes/renderable-map'; |
| 14 | +import { Point, MapEvent } from '../../helpers/type.helpers'; |
| 15 | +import { eventToPoint, staggerChange } from '../../helpers/map.helpers'; |
| 16 | +import { MapRenderFeature } from '../../classes/map-render-feature'; |
| 17 | + |
| 18 | +@Component({ |
| 19 | + selector: 'a-map-outlet', |
| 20 | + templateUrl: './map-outlet.component.html', |
| 21 | + styleUrls: ['./map-outlet.component.scss'] |
| 22 | +}) |
| 23 | +export class MapOutletComponent implements OnInit, OnChanges { |
| 24 | + /** Details of the map */ |
| 25 | + @Input() map: RenderableMap; |
| 26 | + /** Zoom level of the map as a whole number. 1 = 100% zoom */ |
| 27 | + @Input() public zoom: number; |
| 28 | + /** |
| 29 | + * Position of the center point of the component on the map displayed |
| 30 | + * |
| 31 | + * For example: |
| 32 | + * |
| 33 | + * { x: 0, y: 0 } |
| 34 | + * Places the map top left corner in the middle of the component |
| 35 | + * |
| 36 | + * { x: 0.5, y: 0.5 } |
| 37 | + * Places the center of the map in the middle of the component |
| 38 | + * |
| 39 | + * { x: 1, y: 1 } |
| 40 | + * Places the bottom right corner of the map in the middle of the component |
| 41 | + */ |
| 42 | + @Input() public center: Point; |
| 43 | + /** List of features to render over the map */ |
| 44 | + @Input() public features: MapRenderFeature[] = []; |
| 45 | + /** List of text to render over the map */ |
| 46 | + @Input() public text: MapRenderFeature[] = []; |
| 47 | + /** Emitter for changes to the zoom value */ |
| 48 | + @Output() public zoomChange = new EventEmitter<number>(); |
| 49 | + /** Emitter for changes to the center value */ |
| 50 | + @Output() public centerChange = new EventEmitter<Point>(); |
| 51 | + /** Emitter for changes to the zoom value */ |
| 52 | + @Output() public events = new EventEmitter<MapEvent>(); |
| 53 | + /** Element reference to the map display element */ |
| 54 | + @ViewChild('element', { static: true }) public map_element: ElementRef<HTMLDivElement>; |
| 55 | + /** Element reference to the map container element */ |
| 56 | + @ViewChild('container', { static: true }) private _container: ElementRef<HTMLDivElement>; |
| 57 | + /** Bounding box for the map */ |
| 58 | + private _box: ClientRect; |
| 59 | + /** Local zoom value used to rendered the map */ |
| 60 | + public local_zoom: number = 1; |
| 61 | + /** Local zoom value used to rendered the map */ |
| 62 | + public local_center: Point = { x: .5, y: .5 }; |
| 63 | + /** Promise for handling changes to zoom values */ |
| 64 | + private zoom_promise: Promise<void>; |
| 65 | + /** Promise for handling changes to center position values */ |
| 66 | + private center_promise: Promise<void>; |
| 67 | + /** Store of latest difference change between zoom values */ |
| 68 | + private _zoom_diff: number; |
| 69 | + |
| 70 | + /** Width of the map outlet container */ |
| 71 | + public get width(): string { |
| 72 | + if (!this.map) { |
| 73 | + return '0'; |
| 74 | + } |
| 75 | + return `${(this.local_zoom * this.size_dimension).toFixed(2)}px`; |
| 76 | + } |
| 77 | + /** Height of the map outlet container */ |
| 78 | + public get height(): string { |
| 79 | + if (!this.map) { |
| 80 | + return '0'; |
| 81 | + } |
| 82 | + const height = |
| 83 | + this.map.dimensions.y > this.map.dimensions.x |
| 84 | + ? this.local_zoom * this.size_dimension |
| 85 | + : this.local_zoom * this.size_dimension * this.map.dimensions.y; |
| 86 | + return `${height.toFixed(2)}px`; |
| 87 | + } |
| 88 | + /** Position of the map outlet container */ |
| 89 | + public get transformX(): number { |
| 90 | + return -(this.local_center ? this.local_center.x : 0.5) * 100; |
| 91 | + } |
| 92 | + /** Position of the map outlet container */ |
| 93 | + public get transformY(): number { |
| 94 | + return -(this.local_center ? this.local_center.y : 0.5) * 100; |
| 95 | + } |
| 96 | + |
| 97 | + /** */ |
| 98 | + public get size_dimension(): number { |
| 99 | + return this._box ? this._box.width : 100; |
| 100 | + } |
| 101 | + |
| 102 | + public ngOnInit(): void { |
| 103 | + this.updateContainerBox(); |
| 104 | + } |
| 105 | + |
| 106 | + public ngOnChanges(changes: SimpleChanges): void { |
| 107 | + if (changes.zoom) { |
| 108 | + this.staggerZoom(); |
| 109 | + } |
| 110 | + if (changes.center) { |
| 111 | + this.staggerCenter(); |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Emitted the position of the mouse click relative to the map |
| 117 | + * @param event Mouse or touch event |
| 118 | + */ |
| 119 | + public emitPointerPostion(event: MouseEvent | TouchEvent) { |
| 120 | + const point = eventToPoint(event); |
| 121 | + const box = this.map_element.nativeElement.getBoundingClientRect(); |
| 122 | + const position = { |
| 123 | + x: +((point.x - box.left) / box.width).toFixed(4), |
| 124 | + y: +((point.y - box.top) / box.height).toFixed(4) |
| 125 | + }; |
| 126 | + this.events.emit({ type: 'click', metadata: position } as MapEvent); |
| 127 | + } |
| 128 | + |
| 129 | + /** Update the bound box of the container bounding box */ |
| 130 | + public updateContainerBox() { |
| 131 | + if (this._container && this._container.nativeElement) { |
| 132 | + this._box = this._container.nativeElement.getBoundingClientRect(); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + public updateZoom(new_zoom: number) { |
| 137 | + this.zoomChange.emit(new_zoom); |
| 138 | + this.staggerZoom(); |
| 139 | + } |
| 140 | + |
| 141 | + private staggerZoom() { |
| 142 | + this._zoom_diff = Math.abs(this.zoom - this.local_zoom); |
| 143 | + if (!this.zoom_promise) { |
| 144 | + this.zoom_promise = staggerChange(this.zoom - this.local_zoom, () => { |
| 145 | + let change = this.zoom - this.local_zoom; |
| 146 | + const direction = change < 0 ? -1 : 1; |
| 147 | + const change_value = Math.max(0.02, Math.min(0.75, Math.abs(this._zoom_diff) / 10)); |
| 148 | + this.local_zoom += |
| 149 | + this._zoom_diff > change_value ? (direction < 0 ? -change_value : change_value) : change; |
| 150 | + this.local_zoom = Math.max(1, Math.min(10, this.local_zoom)); |
| 151 | + const not_done = Math.abs(change) < change_value ? 0 : change; |
| 152 | + if (!not_done) { |
| 153 | + this.local_zoom = this.zoom; |
| 154 | + } |
| 155 | + return not_done; |
| 156 | + }); |
| 157 | + this.zoom_promise.then(() => this.zoom_promise = null); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + public updateCenter(new_center: Point) { |
| 162 | + this.centerChange.emit(new_center); |
| 163 | + this.staggerCenter(); |
| 164 | + } |
| 165 | + |
| 166 | + private staggerCenter() { |
| 167 | + if (!this.center_promise) { |
| 168 | + this.center_promise = staggerChange(1, () => { |
| 169 | + const change = { |
| 170 | + x: this.center.x - this.local_center.x, |
| 171 | + y: this.center.y - this.local_center.y |
| 172 | + }; |
| 173 | + const direction = { |
| 174 | + x: change.x < 0 ? -1 : 1, |
| 175 | + y: change.y < 0 ? -1 : 1 |
| 176 | + }; |
| 177 | + const change_value = { |
| 178 | + x: Math.max(0.01, Math.min(0.05, Math.abs(change.x) / 5)), |
| 179 | + y: Math.max(0.01, Math.min(0.05, Math.abs(change.y) / 5)) |
| 180 | + }; |
| 181 | + this.local_center = { |
| 182 | + x: this.local_center.x + (Math.abs(change.x) > change_value.x ? (direction.x < 0 ? -1 : 1) * change_value.x : change.x), |
| 183 | + y: this.local_center.y + (Math.abs(change.y) > change_value.y ? (direction.y < 0 ? -1 : 1) * change_value.y : change.y), |
| 184 | + }; |
| 185 | + return Math.abs(change.x) < change_value.x && Math.abs(change.y) < change_value.y ? 0 : 1; |
| 186 | + }); |
| 187 | + this.center_promise.then(() => this.center_promise = null); |
| 188 | + } |
| 189 | + } |
| 190 | +} |
0 commit comments