Skip to content

Commit 5e465e2

Browse files
committed
chore: add outlet for rendering the map
1 parent 29a3d24 commit 5e465e2

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<div
2+
class="map-container"
3+
#container
4+
(window:resize)="updateContainerBox()"
5+
map-input
6+
[(zoom)]="zoom"
7+
(zoomChange)="updateZoom($event)"
8+
[(center)]="center"
9+
(centerChange)="updateCenter($event)"
10+
[map]="map"
11+
[element]="map_element"
12+
(click)="emitPointerPostion($event)"
13+
>
14+
<div
15+
class="map-outlet"
16+
#element
17+
[class.zooming]="zoom !== local_zoom"
18+
[style.width]="width"
19+
[style.height]="height"
20+
[style.transform]="'translate(' + transformX + '%, ' + transformY + '%)'"
21+
>
22+
<div class="map" *ngIf="map" [innerHTML]="map.raw_data | safe"></div>
23+
<map-overlay-outlet [zoom]="local_zoom" [center]="local_center" [items]="features"></map-overlay-outlet>
24+
<map-text-outlet [items]="text"></map-text-outlet>
25+
</div>
26+
</div>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
:host,
3+
.map-container {
4+
position: relative;
5+
width: 100%;
6+
height: 100%;
7+
box-sizing: border-box;
8+
}
9+
10+
.map-container {
11+
width: calc(100% - 2em);
12+
height: calc(100% - 2em);
13+
margin: 1em;
14+
}
15+
16+
.map-outlet {
17+
position: relative;
18+
will-change: height, width, transform;
19+
top: 50%;
20+
left: 50%;
21+
transform-origin: left top;
22+
23+
* {
24+
user-select: none;
25+
}
26+
}
27+
28+
.map {
29+
z-index: 1;
30+
}
31+
32+
map-text-outlet {
33+
z-index: 2;
34+
}
35+
36+
map-overlay-outlet {
37+
z-index: 3;
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { MapOutletComponent } from './map-outlet.component';
4+
5+
describe('MapOutletComponent', () => {
6+
let component: MapOutletComponent;
7+
let fixture: ComponentFixture<MapOutletComponent>;
8+
9+
beforeEach(async(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [ MapOutletComponent ]
12+
})
13+
.compileComponents();
14+
}));
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(MapOutletComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)