Files
openstapps/frontend/app/src/app/modules/map/map.provider.ts
2023-08-02 17:59:20 +02:00

238 lines
6.8 KiB
TypeScript

/*
* Copyright (C) 2022 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, 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';
import {SCIcon} from '../../util/ion-icon/icon';
/**
* 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
* @param className CSS class name
* @param iconSize Size of the position icon
*/
static getPointMarker(point: Point, className: string, iconSize: number) {
return marker(geoJSON(point).getBounds().getCenter(), {
icon: divIcon({
className: className,
html: `<span
name="${SCIcon`location_on`}"
class="material-symbols-rounded map-location-pin"
style="font-size: ${iconSize}px;"
>${SCIcon`location_on`}</span>`,
iconSize: [iconSize, iconSize],
iconAnchor: [iconSize / 2, iconSize],
}),
});
}
/**
* 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:
position.heading === undefined
? `<span
name="${SCIcon`person_pin_circle`}"
class="material-symbols-rounded map-location-pin"
style="font-size: ${iconSize}px; color: var(--ion-color-primary);"
>${SCIcon`person_pin_circle`}</span>`
: `<span
class="material-symbols-rounded map-location-pin"
style="
transform-origin: center;
transform: rotate(${position.heading}deg);
font-size: ${iconSize}px;
color: var(--ion-color-primary);
"
>${SCIcon`navigation`}</span>`,
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 (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',
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;
}
}