Files
openstapps/src/app/modules/map/map.provider.ts
2022-02-10 16:21:34 +01:00

245 lines
6.2 KiB
TypeScript

/*
* Copyright (C) 2019-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {ElementRef, Injectable} from '@angular/core';
import {
SCBuilding,
SCSearchFilter,
SCSearchQuery,
SCSearchResponse,
SCThingType,
SCUuid,
} from '@openstapps/core';
import {Point, Polygon} from 'geojson';
import {divIcon, geoJSON, icon, LatLng, Map, marker, Marker} from 'leaflet';
import {DataProvider} from '../data/data.provider';
import {MapPosition, PositionService} from './position.service';
import {hasValidLocation} from '../data/types/place/place-types';
import {ConfigProvider} from '../config/config.provider';
/**
* Provides methods for presenting the map
*/
@Injectable({
providedIn: 'root',
})
export class MapProvider {
/**
* Area to show when the map is initialized (shown for the first time)
*/
defaultPolygon: Polygon;
/**
* Provide a point marker for a leaflet map
*
* @param point Point to get marker for
*/
static getPointMarker(point: Point) {
return marker(geoJSON(point).getBounds().getCenter(), {
icon: icon({
iconAnchor: [13, 41],
iconSize: [25, 41],
iconUrl: '../assets/marker-icon.png',
shadowUrl: '../assets/marker-shadow.png',
}),
});
}
/**
* Provide a position marker for a leaflet map
*
* @param position Current position
* @param className CSS class name
* @param iconSize Size of the position icon
*/
static getPositionMarker(
position: MapPosition,
className: string,
iconSize: number,
) {
return new Marker(new LatLng(position.latitude, position.longitude), {
icon: divIcon({
className: className,
html:
typeof position.heading !== 'undefined'
? `<ion-icon name="navigate-straight"
style="transform-origin: center; transform: rotate(${position.heading}deg);">
</ion-icon>`
: '<ion-icon name="locate"></ion-icon>',
iconSize: [iconSize, iconSize],
}),
zIndexOffset: 1000,
});
}
/**
* Fixes the issue of missing tiles when map renders before its container element
*
* @param map The initialized map
* @param element The element containing the map
* @param interval Interval to clear when map's appearance is corrected
*/
static invalidateWhenRendered = (
map: Map,
element: ElementRef,
interval: number,
) => {
if (element.nativeElement.offsetWidth === 0) {
return;
}
// map's container is ready
map.invalidateSize();
// stop repeating when it's rendered and invalidateSize done
clearInterval(interval);
};
constructor(
private dataProvider: DataProvider,
private positionService: PositionService,
private configProvider: ConfigProvider,
) {
this.defaultPolygon = this.configProvider.getValue(
'campusPolygon',
) as Polygon;
}
/**
* Provide the specific place by its UID
*
* @param uid UUID of the place to look for
*/
async searchPlace(uid: SCUuid): Promise<SCSearchResponse> {
const uidFilter: SCSearchFilter = {
arguments: {
field: 'uid',
value: uid,
},
type: 'value',
};
return this.dataProvider.search({filter: uidFilter});
}
/**
* Provide places (buildings and canteens) const result = await this.dataProvider.search(query);
*
* @param contextFilter Additional contextual filter (e.g. from the context menu)
* @param queryText Query (text) of the search query
*/
async searchPlaces(
contextFilter?: SCSearchFilter,
queryText?: string,
): Promise<SCSearchResponse> {
const buildingFilter: SCSearchFilter = {
arguments: {
field: 'type',
value: SCThingType.Building,
},
type: 'value',
};
const mensaFilter: SCSearchFilter = {
arguments: {
filters: [
{
arguments: {
field: 'categories',
value: 'canteen',
},
type: 'value',
},
{
arguments: {
field: 'categories',
value: 'student canteen',
},
type: 'value',
},
{
arguments: {
field: 'categories',
value: 'cafe',
},
type: 'value',
},
{
arguments: {
field: 'categories',
value: 'restaurant',
},
type: 'value',
},
],
operation: 'or',
},
type: 'boolean',
};
// initial filter for the places
const baseFilter: SCSearchFilter = {
arguments: {
operation: 'or',
filters: [buildingFilter, mensaFilter],
},
type: 'boolean',
};
let filter = baseFilter;
if (typeof contextFilter !== 'undefined') {
filter = {
arguments: {
operation: 'and',
filters: [baseFilter, contextFilter],
},
type: 'boolean',
};
}
const query: SCSearchQuery = {
filter,
};
if (queryText && queryText.length > 0) {
query.query = queryText;
}
if (this.positionService.position) {
query.sort = [
{
type: 'distance',
order: 'asc',
arguments: {
field: 'geo.point.coordinates',
position: [
this.positionService.position.longitude,
this.positionService.position.latitude,
],
},
},
];
}
const result = await this.dataProvider.search(query);
result.data = result.data.filter(place =>
hasValidLocation(place as SCBuilding),
);
return result;
}
}