...
 
Commits (8)
......@@ -2197,6 +2197,11 @@
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.1.tgz",
"integrity": "sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg=="
},
"bowser": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz",
"integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ=="
},
"boxen": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-0.6.0.tgz",
......@@ -6268,6 +6273,9 @@
"assert-plus": "1.0.0"
}
},
"gl-matrix": {
"version": "git://github.com/toji/gl-matrix.git#7c8d5ddf78a403fed71504664f0c66b5d0c62112"
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
......@@ -6569,6 +6577,9 @@
"pify": "3.0.0"
}
},
"hammerjs": {
"version": "git://github.com/digisfera/hammer.js.git#52e11b34bfc6c6af6cd57cfa51ad7ba711d501f3"
},
"handle-thing": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
......@@ -9570,6 +9581,17 @@
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==",
"dev": true
},
"marzipano": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/marzipano/-/marzipano-0.7.2.tgz",
"integrity": "sha512-u6hGFe1UJsRIFHbwWUslKoiZealxJkPZHYa9ClCMKmZTrFV5NFg9nSNWWqu9NFQVkfo39DlvODB3gYBNTqoNbg==",
"requires": {
"bowser": "1.9.4",
"gl-matrix": "git://github.com/toji/gl-matrix.git#7c8d5ddf78a403fed71504664f0c66b5d0c62112",
"hammerjs": "git://github.com/digisfera/hammer.js.git#52e11b34bfc6c6af6cd57cfa51ad7ba711d501f3",
"minimal-event-emitter": "0.1.0"
}
},
"math-random": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
......@@ -9721,6 +9743,11 @@
"webpack-sources": "1.1.0"
}
},
"minimal-event-emitter": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/minimal-event-emitter/-/minimal-event-emitter-0.1.0.tgz",
"integrity": "sha1-AcTPdu7zMt8E1LjolL5SUzmvOKo="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
......
......@@ -55,6 +55,9 @@ import {TagInputComponent} from './components/add-edit/common/tag-input/tag-inpu
import {ContactInputComponent} from './components/add-edit/common/contact-input/contact-input.component';
import {SingleEnumInputComponent} from './components/add-edit/common/single-enum-input/single-enum-input.component';
import {JsonInputComponent} from './components/add-edit/common/json-input/json-input.component';
import {PanoramaOverlayComponent} from './components/panorama-overlay/panorama-overlay.component';
import {PanoramaOverlayService} from './components/panorama-overlay/panorama-overlay.service';
import {PanoramaComponent} from './components/panorama/panorama.component';
export function testModule(): NgModule {
return {
......@@ -172,6 +175,8 @@ export function testModule(): NgModule {
ContactInputComponent,
SingleEnumInputComponent,
JsonInputComponent,
PanoramaOverlayComponent,
PanoramaComponent,
],
imports: [
BrowserModule,
......@@ -191,6 +196,7 @@ export function testModule(): NgModule {
GenericDataService,
BackendService,
CommonAddEditService,
PanoramaOverlayService,
],
bootstrap: [AppComponent],
entryComponents: [
......@@ -203,6 +209,8 @@ export function testModule(): NgModule {
AddEditPersonComponent,
AddEditTagComponent,
AddEditUserComponent,
PanoramaOverlayComponent,
PanoramaComponent,
],
})
export class AppModule {
......
import { OverlayRef } from '@angular/cdk/overlay';
export class PanoramaOverlayRef {
constructor(private overlayRef: OverlayRef) {}
close(): void {
this.overlayRef.dispose();
}
}
import { Component, Input } from '@angular/core';
import { PanoramaOverlayRef } from './panorama-overlay-ref';
import { PanoramaModel } from '../../models/panorama.model';
@Component({
selector: 'app-panorama-overlay',
template: `
<button class="btn btn-dark float-right" (click)="closePanorama()">Close</button>
<app-panorama [details]="panorama"></app-panorama>`,
styles: [`
:host {
display: block;
background: white;
width: 100%;
height: 100%;
}
button {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 1025;
}
`]
})
export class PanoramaOverlayComponent {
constructor(public panorama: PanoramaModel, private overlayRef: PanoramaOverlayRef) {}
closePanorama() {
this.overlayRef.close();
}
}
import { Injectable, Injector } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { PanoramaOverlayComponent } from './panorama-overlay.component';
import { PanoramaOverlayRef } from './panorama-overlay-ref';
import { PanoramaModel } from '../../models/panorama.model';
@Injectable()
export class PanoramaOverlayService {
constructor(private overlay: Overlay, private injector: Injector) {}
open(panorama: PanoramaModel): PanoramaOverlayRef {
const overlayRef: OverlayRef = this.overlay.create();
const panoramaOverlayRef = new PanoramaOverlayRef(overlayRef);
const injector = this.createInjector(panorama, panoramaOverlayRef);
const panoramaPortal = new ComponentPortal(PanoramaOverlayComponent, null, injector);
overlayRef.attach(panoramaPortal);
return panoramaOverlayRef;
}
private createInjector(panorama: PanoramaModel, overlayRef: PanoramaOverlayRef): PortalInjector {
const tokens = new WeakMap();
tokens.set(PanoramaOverlayRef, overlayRef);
tokens.set(PanoramaModel, panorama);
return new PortalInjector(this.injector, tokens);
}
}
import { Component, Input, OnInit } from '@angular/core';
//import { PanoramaOverlayRef } from './panorama-overlay-ref';
import { PanoramaModel } from '../../models/panorama.model';
import * as Marzipano from 'marzipano';
@Component({
selector: 'app-panorama',
template: `
<div class="panorama"></div>`,
})
export class PanoramaComponent implements OnInit {
@Input() details: PanoramaModel;
constructor() {}
ngOnInit() {
const node = document.querySelector('.panorama');
const viewer = new Marzipano.Viewer(node);
const source = Marzipano.ImageUrlSource.fromString(this.details.file);
// Assume image to be 4000px wide with 2:1 ratio
const geometry = new Marzipano.EquirectGeometry([{ width: 4000 }]);
// Prevent zoom, limit vertical field of view so users can't move over poles
const limiter = Marzipano.RectilinearView.limit.traditional(1024, this.degreesToRadians(100));
// Start view at the center of the image
const view = new Marzipano.RectilinearView({ yaw: this.degreesToRadians(0) }, limiter);
const scene = viewer.createScene({
source,
geometry,
view,
pinFirstLevel: true,
});
scene.switchTo();
}
private degreesToRadians(degrees: number): number {
return degrees * Math.PI / 180;
}
}
export enum EndpointType {
CMS, IndoorModel,
CMS, IndoorModel, Indoor360,
}
import {ColumnType} from '../enums/ColumnType';
import {IModelDefinition} from '../interfaces/IModelDefinition';
import {PanoramaModel} from '../models/panorama.model';
import {EndpointType} from '../enums/EndpointType';
export const panoramaModelDefinition: IModelDefinition<PanoramaModel> = {
titlePlural: 'Panorama',
titleSingular: 'Panoramas',
needAdminPrivileges: false,
endpoint: EndpointType.Indoor360,
viewPagePath: 'panorama',
addEditPath: 'panoramas',
ressourcePathBackend: 'panoramas',
crudDisabled: true,
constructor: (obj) => new PanoramaModel(obj),
columnDefinitions: [
{
title: 'Name',
displayFunction: (object: PanoramaModel) => object.name,
},
],
};
import {DataReader} from '../classes/DataReader';
import {IModel} from '../interfaces/IModel';
import {LocationModel} from './sub-types/location.model';
import {PanoramaPortalModel} from './sub-types/portal.model';
import {PanoramaHotspotModel} from './sub-types/hotspot.model';
/**
* A model representing a 360° panorama.
*/
export class PanoramaModel implements IModel {
/**
* The id.
*/
id: string;
/**
* The panorama's name.
*/
name: string;
/**
* The location of the panorama.
*/
location: LocationModel;
/**
* The link to the panorama image.
*/
file: string;
/**
* The bearing of the panorama.
*/
bearing: number;
/**
* The outgoing hotspots of this panorama.
*/
portals: PanoramaPortalModel[];
/**
* The hotspots of this panorama.
*/
hotspots: PanoramaHotspotModel[];
/**
* Constructor.
* @param object The raw JSON object to parse.
*/
constructor(object: object) {
this.id = DataReader.readKeySafe(object, 'id', '');
this.name = DataReader.readKeySafe(object, 'name', '');
this.location = DataReader.readObjectSafe(object, 'location', (location) => new LocationModel(location));
this.file = DataReader.readKeySafe(object, 'file', '');
this.bearing = DataReader.readKeySafe(object, 'bearing', 0);
this.portals = DataReader.readArraySafe(object, 'portals', (portal) => new PanoramaPortalModel(portal));
this.hotspots = DataReader.readArraySafe(object, 'hotspots', (hotspot) => new PanoramaHotspotModel(hotspot));
}
/**
* See {@link IModel}.
* @returns {object}
*/
toPOSTObject(): object {
return {
id: this.id,
name: this.name,
location: this.location.toPOSTObject(),
bearing: this.bearing,
};
}
/**
* Returns the panorama's name.
* See {@link IModel}.
* @returns {string}
*/
toString(): string { return this.name; }
/**
* See {@link IModel}.
* @returns {string}
*/
getID(): string { return this.id; }
/**
* See {@link IModel}
* The Indoor 360 API does not use revisions, so this method returns null.
* @returns {number}
*/
getRevision(): number { return null; }
}
import {DataReader} from '../../classes/DataReader';
export class PanoramaHotspotModel {
poi: string;
bearing: number;
height: number;
/**
* Constructor.
* @param object The raw JSON object to parse.
*/
constructor(object: object) {
this.poi = DataReader.readKeySafe(object, 'poi', '');
this.bearing = DataReader.readKeySafe(object, 'bearing', 0);
this.height = DataReader.readKeySafe(object, 'height', 0);
}
}
import {DataReader} from '../../classes/DataReader';
export class LocationModel {
lat: number;
lng: number;
floor: number;
building: string | null;
room: string | null;
constructor(object: object) {
this.lat = DataReader.readKeySafe(object, 'lat', 0);
this.lng = DataReader.readKeySafe(object, 'lng', 0);
this.floor = DataReader.readKeySafe(object, 'floor', 0);
this.building = DataReader.readKeySafe(object, 'building', null);
this.room = DataReader.readKeySafe(object, 'room', null);
}
/**
* See {@link IModel}.
* @returns {object}
*/
toPOSTObject(): object {
return {
lat: this.lat,
lng: this.lng,
floor: this.floor,
building: this.building,
room: this.room,
};
}
}
import {DataReader} from '../../classes/DataReader';
export class PanoramaPortalModel {
/**
* The URL this panorama links to.
*/
panorama: string;
/**
* The bearing where this portal is placed.
*/
bearing: number;
/**
* The distance where this portal is placed.
*/
distance: number;
/**
* Constructor.
* @param object The raw JSON object to parse.
*/
constructor(object: object) {
this.panorama = DataReader.readKeySafe(object, 'panorama', '');
this.bearing = DataReader.readKeySafe(object, 'bearing', 0);
this.distance = DataReader.readKeySafe(object, 'distance', 0);
}
}
......@@ -74,6 +74,18 @@ export class BackendService {
testPath: '/building/all',
};
/**
* The indoor 360° backend, which is a different server project.
* @type {IEndpointDefinition}
*/
private indoor360EndpointMain: IEndpointDefinition = {
baseUrl: 'http://localhost:8080',
domainDisplay: 'localhost:8080',
title: 'Indoor 360 Backend',
apiUrl: 'http://localhost:8080/openapi.json',
testPath: '/panoramas',
};
/**
* Lists all the possible CMS backends, from which the user can choose.
* The first endpoint is the default endpoint.
......@@ -96,6 +108,8 @@ export class BackendService {
return this.possibleCMSEndpoints[this.selectedCMSEndpointIndex];
case EndpointType.IndoorModel:
return this.indoorModelEndpointMain;
case EndpointType.Indoor360:
return this.indoor360EndpointMain;
}
}
......
......@@ -165,13 +165,13 @@ export class GenericDataService {
}
const id = object.getID();
const revision = object.getRevision().toString();
//const revision = object.getRevision().toString();
const baseURL = this.backendService.getEndpointDefinitionForBackEndType(modelDefinition.endpoint).baseUrl;
const ressourceURL = modelDefinition.ressourcePathBackend;
let params = new HttpParams();
params = params.set('revision', revision);
//params = params.set('revision', revision);
await this.http.delete(`${baseURL}/${ressourceURL}/${id}`, {
headers,
......
......@@ -16,8 +16,10 @@ import {
} from '../../data/map-renderer.data';
import {buildingModelDefinition} from '../../model-definitions/BuildingModelDefinition';
import {poiModelDefinition} from '../../model-definitions/PoiModelDefinition';
import {panoramaModelDefinition} from '../../model-definitions/PanoramaModelDefinition';
import {BuildingModel} from '../../models/building.model';
import {PoiModel} from '../../models/poi.model';
import {PanoramaModel} from '../../models/panorama.model';
import {Level} from '../../models/sub-types/level.model';
import {Polygon} from '../../models/sub-types/polygon.model';
import {RoomModel} from '../../models/sub-types/room.model';
......@@ -28,6 +30,7 @@ import {AuthService} from '../auth/auth.service';
import {tagModelDefinition} from '../../model-definitions/TagModelDefinition';
import {TagModel} from '../../models/tag.model';
import {AddEditTagComponent} from '../../components/add-edit/add-edit-tag/add-edit-tag.component';
import {PanoramaOverlayService} from '../../components/panorama-overlay/panorama-overlay.service';
@Injectable()
export class MapRendererService {
......@@ -37,17 +40,20 @@ export class MapRendererService {
public selectedLevel: Level = null;
public poisShown = true;
public tagsShown = true;
public panoramasShown = true;
public error: string = '';
private polygons: Polygon[] = [];
private polygonLayers: L.Polygon[] = [];
private map: L.Map;
private poiMarkers: Array<[L.Marker, PoiModel]> = [];
private tagMarkers: Array<[L.Marker, TagModel]> = [];
private panoramaMarkers: Array<[L.Marker, PanoramaModel]> = [];
private menuPosition: L.LatLng;
constructor(private dataService: GenericDataService,
private modalService: NgbModal,
private authService: AuthService) {
private authService: AuthService,
private panoramaOverlayService: PanoramaOverlayService) {
}
render(): void {
......@@ -76,6 +82,7 @@ export class MapRendererService {
this.drawBuildings();
this.drawPois();
this.drawTags();
this.drawPanoramas();
}
setCenterView(): void {
......@@ -141,6 +148,24 @@ export class MapRendererService {
}
}
async drawPanoramas(): Promise<void> {
this.removePanoramas();
this.panoramasShown = true;
try {
const panoramas = await this.dataService.get(panoramaModelDefinition);
panoramas.forEach((panorama: PanoramaModel) => {
if (!this.selectedLevel && panorama.location.floor === 0) {
this.addPanoramaToMap(panorama);
} else if (this.selectedLevel && this.selectedLevel.level === panorama.location.floor) {
this.addPanoramaToMap(panorama);
}
});
} catch (err) {
this.error = err.message;
}
}
togglePois() {
if (this.poisShown) {
this.removePois();
......@@ -173,6 +198,14 @@ export class MapRendererService {
this.tagMarkers = [];
}
removePanoramas() {
this.panoramasShown = false;
this.panoramaMarkers.forEach((tuple) => {
tuple[0].removeFrom(this.map);
});
this.panoramaMarkers = [];
}
drawLevel() {
const level = this.selectedLevel;
if (!level || !level.rooms || !level.rooms.length) {
......@@ -189,6 +222,7 @@ export class MapRendererService {
this.applyPolygons();
this.drawPois();
this.drawTags();
this.drawPanoramas();
}
public leaveBuilding(): void {
......@@ -197,6 +231,7 @@ export class MapRendererService {
}
this.drawPois();
this.drawTags();
this.drawPanoramas();
}
public isBuildingClicked(): boolean {
......@@ -249,6 +284,20 @@ export class MapRendererService {
this.tagMarkers.push([marker, tag]);
}
private addPanoramaToMap(panorama: PanoramaModel) {
let marker: L.Marker;
marker = L.marker([panorama.location.lat, panorama.location.lng],
{
icon: redIcon(),
});
marker.on('click', () => {
this.panoramaOverlayService.open(panorama);
});
marker.addTo(this.map);
marker.bindTooltip(panorama.name, toolTipOptionsMarker());
this.panoramaMarkers.push([marker, panorama]);
}
private editPoi(id: string) {
const modalRef = this.modalService.open(AddEditPoiComponent, {size: 'lg'});
modalRef.componentInstance.id = id;
......
......@@ -32,3 +32,10 @@
object-fit: cover;
}
div.cdk-overlay-container {
z-index: 1025;
}
.cdk-overlay-container div {
height: 100%;
}
......@@ -3,3 +3,5 @@ declare var module: NodeModule;
interface NodeModule {
id: string;
}
declare module 'marzipano';