Compare commits

..

No commits in common. "1c2d245f7cafa795e3bda276f3faa4165d3842fb" and "ad57086bcc035ced419874d05eaecb294ee82065" have entirely different histories.

13 changed files with 131 additions and 501 deletions

View File

@ -6,12 +6,22 @@ exports.up = function(knex) {
return knex.schema return knex.schema
.createTable('users', function (table) { .createTable('users', function (table) {
table.uuid('id').primary().defaultTo(knex.raw(`gen_random_uuid()`)); table.uuid('id')
table.string('first_name', 255).notNullable(); .primary()
table.string('last_name', 255).notNullable(); .defaultTo(knex.raw(`gen_random_uuid()`));
table.string('email', 255).unique().notNullable(); table.string('first_name', 255)
table.timestamps(true, true); .notNullable();
table.string('last_name', 255)
.notNullable();
table.string('email', 255)
.unique()
.notNullable();
}) })
// .createTable('products', function (table) {
// table.increments('id');
// table.decimal('price').notNullable();
// table.string('name', 1000).notNullable();
// });
}; };
/** /**
@ -19,5 +29,7 @@ exports.up = function(knex) {
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.down = function(knex) { exports.down = function(knex) {
return knex.schema.dropTable('users'); return knex.schema
// .dropTable('products')
.dropTable('users');
}; };

View File

@ -1,23 +0,0 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema
.createTable('places', function (table) {
table.uuid('id').primary().defaultTo(knex.raw(`gen_random_uuid()`));
table.string('name', 255).notNullable();
table.specificType('coordinates', 'GEOGRAPHY(Point, 4326)').notNullable();
table.string('externalId', 255);
table.timestamps(true, true);
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable('places');
};

View File

@ -1,30 +0,0 @@
const { fakerLV : faker } = require('@faker-js/faker');
const PLACE_COUNT = 1000;
const BBOX_RIGA = [23.755875,56.815914,24.466553,57.067617];
const [ BBOX_MIN_LAT, BBOX_MIN_LON, BBOX_MAX_LAT, BBOX_MAX_LON ] = BBOX_RIGA;
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.seed = async function(knex) {
const createPlace = () => {
const coords = [
faker.location.latitude({ min: BBOX_MIN_LAT, max: BBOX_MAX_LAT }),
faker.location.longitude({ min: BBOX_MIN_LON, max: BBOX_MAX_LON })
];
return {
name: faker.company.name(),
coordinates: knex.raw('ST_SetSRID(ST_MakePoint(?, ?), 4326)', coords),
}
};
// Deletes ALL existing entries
await knex('places').del()
await knex('places').insert([
...new Array(PLACE_COUNT).fill(() => createPlace()).map((fn, i) => fn(i)),
]);
};

View File

@ -2,25 +2,25 @@ const { fakerLV : faker } = require('@faker-js/faker');
const USER_COUNT = 500; const USER_COUNT = 500;
const createUser = ({
sex = faker.person.sexType(),
first_name = `${faker.person.firstName(sex)}`,
last_name = faker.person.lastName(sex),
email = faker.internet.email({ lastName: last_name, firstName: first_name }).toLocaleLowerCase(),
} = {}) => {
return {
first_name,
last_name,
email,
}
};
/** /**
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.seed = async function(knex) { exports.seed = async function(knex) {
// Deletes ALL existing entries
const createUser = ({
sex = faker.person.sexType(),
first_name = `${faker.person.firstName(sex)}`,
last_name = faker.person.lastName(sex),
email = faker.internet.email({ lastName: last_name, firstName: first_name }).toLocaleLowerCase(),
} = {}) => {
return {
first_name,
last_name,
email,
}
};
await knex('users').del() await knex('users').del()
await knex('users').insert([ await knex('users').insert([
createUser({ createUser({

View File

@ -1,9 +1,8 @@
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
// import { AMENITIES, OverpassService } from '../services/overpass'; import { OverpassService } from '../services/overpass';
import { OverpassQuerySchema, OverpassQueryType } from '../schemas/overpass'; import { OverpassQuerySchema, OverpassQueryType } from '../schemas/overpass';
import db from '../../db';
// const overpassService = new OverpassService(); const overpassService = new OverpassService();
export default async function (fastify: FastifyInstance) { export default async function (fastify: FastifyInstance) {
@ -16,76 +15,23 @@ export default async function (fastify: FastifyInstance) {
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
id: { type: 'string' },
title: { type: 'string' }, title: { type: 'string' },
amenity: { type: 'string' }, amenity: { type: 'string' },
coords: { tags: { type: 'object', additionalProperties: true }
type: 'object',
properties: {
lat: { type: 'number' },
lon: { type: 'number' }
}
},
affinity: { type: 'number' },
// tags: { type: 'object', additionalProperties: true }
} }
} }
} }
} }
}, },
config: { config: {
// serverCache: { ttl: 1 * 60 * 1000 } serverCache: { ttl: 10000 }
} }
}, async function (request, reply) { }, async function (request, reply) {
const {lat, lon} = request.query; const {lat, lon} = request.query;
const elements = await overpassService.getAmenitiesAroundLocation({
const knownPlaces = await db.select( latlon: { lat, lon },
'id', amenities: ['restaurant', 'cafe', 'bar', 'pub', 'biergarten', 'fast_food', 'food_court', 'ice_cream']
'name', });
'externalId', reply.send(elements);
db.raw('ST_Y(coordinates::geometry) AS lat'),
db.raw('ST_X(coordinates::geometry) AS lon')
).whereRaw(
'ST_DWithin(coordinates, ST_SetSRID(ST_MakePoint(?, ?), 4326), ?)',
[lon, lat, 2000]
).from('places');
// const externalIds = await db.select('externalId').from('places').where('externalId', 'like', 'osm:%');
// console.log(externalIds);
// overpassService.getAmenitiesAroundLocation({
// radius: 1000,
// coords: { lat, lon },
// amenities: AMENITIES.Sustanance
// }).then(result => {
// const insertItems = result
// .filter(node => {
// if (!node.title || !node.coords) return false;
// return !externalIds.some(({ externalId }) => externalId === `osm:${node.id}`);
// })
// .map(node => ({
// name: node.title,
// externalId: `osm:${node.id}`,
// coordinates: db.raw('ST_SetSRID(ST_MakePoint(?, ?), 4326)', [node.coords.lon, node.coords.lat])
// }));
// if (insertItems.length) db('places').insert(insertItems).then((success) => console.log({success}), (error) => console.log({error}));
// });
const blakus = knownPlaces.map(place => ({
id: place.id,
title: place.name,
amenity: 'pub',
coords: {
lat: place.lat,
lon: place.lon,
},
affinity: Math.floor(Math.random() * 101) // TODO: this is just a placeholder for "affinity" calculation
}));
reply.send(blakus);
}); });
} }

View File

@ -1,50 +0,0 @@
import { FastifyInstance } from 'fastify';
import db from '../../db';
export default async function (fastify: FastifyInstance) {
fastify.get('/places', {
schema: {
response: {
200: {
type: 'object',
properties: {
places: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
coords: {
type: 'object',
properties: {
lat: { type: 'number' },
lon: { type: 'number' }
}
}
}
}
}
}
}
}
},
config: {
// serverCache: { ttl: 10000 }
}
}, async function (request, reply) {
const placesResults = await db.select(
'name',
db.raw('ST_Y(coordinates::geometry) AS lat'),
db.raw('ST_X(coordinates::geometry) AS lon')
).from('places').limit(500);
const places = placesResults.map(({ name, lat, lon}) => ({
name,
coords: { lat, lon }
}));
reply.send({ places });
});
}

View File

@ -1,12 +1,10 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
type OverpassResultElement = NodeElement | WayElement | RelationElement; type OverpassResult = {
type OverpassResult<T = OverpassResultElement> = {
version: number; version: number;
generator: string; generator: string;
osm3s: Record<string, string>; osm3s: Record<string, string>;
elements: T[]; elements: (NodeElement | WayElement | RelationElement)[];
} }
type OverpassLocation = { type OverpassLocation = {
@ -40,39 +38,17 @@ type RelationElement = OverpassElement & {
}[]; }[];
} }
type OverpassQuerySettings = { function opq(strings, ...expressions) {
out?: 'json' | 'xml' // | 'csv' | 'custom' | 'popup'; // TODO: other options would require additional implementation let query = '[out:json];\n';
timeout?: number;
maxsize?: number;
bbox?: [number, number, number, number];
date?: Date;
}
export const AMENITIES = { for (let i = 0; i < strings.length; i++) {
get Sustanance() { query += strings[i];
return ['restaurant', 'cafe', 'bar', 'pub', 'biergarten', 'fast_food', 'food_court', 'ice_cream'] if (i < expressions.length) query += expressions[i];
} }
};
function overpassQueryTemplate ({ out = 'json', timeout, maxsize, bbox = [34.016242,-24.433594,71.663663,48.164063], date}: OverpassQuerySettings, statements: string[] = ['out geom']) { query += '\nout body;\n>;\nout skel qt;';
return function (strings, ...expressions) {
let query = [
`[out:${out}]`,
timeout ? `[timeout:${timeout}]` : '',
maxsize ? `[maxsize:${maxsize}]` : '',
bbox && bbox.length === 4 ? `[bbox:${bbox.join(',')}]` : '',
date ? `[date:${date.toISOString()}]` : ''
].join('') + ';';
for (let i = 0; i < strings.length; i++) { return query;
query += strings[i];
if (i < expressions.length) query += expressions[i];
}
query += statements.join(';') + ';';
return query;
}
} }
export class OverpassService { export class OverpassService {
@ -82,36 +58,22 @@ export class OverpassService {
this.axios = axios.create({ baseURL }); this.axios = axios.create({ baseURL });
} }
async getAmenitiesAroundLocation({radius = 500, amenities = [], coords}: {radius?: number, coords: OverpassLocation, amenities: string[]}) { async getAmenitiesAroundLocation({radius = 500, amenities = [], latlon}: {radius?: number, latlon: OverpassLocation, amenities: string[]}) {
if (!coords || !coords.lat || !coords.lon) throw new Error('Invalid location'); if (!latlon || !latlon.lat || !latlon.lon) throw new Error('Invalid location');
const { lat, lon } = coords; const { lat, lon } = latlon;
const opq = overpassQueryTemplate({ out: 'json', timeout: 60 });
const query = opq`node(around:${radius}, ${lat}, ${lon})["amenity"~"${amenities.join('|')}"];`; const query = opq`node(around:${radius}, ${lat}, ${lon})["amenity"~"${amenities.join('|')}"];`;
const result = await this.overpassQuery<NodeElement>(query); const result = await this.overpassQuery(query);
const output = result.elements return result.elements
.map(item => { .map(({ tags }) => ({ title: tags.name, amenity: tags.amenity, tags}))
const { tags } = item;
return {
id: item.id,
title: tags.name,
amenity: tags.amenity,
tags,
coords: item.lat && item.lon ? { lat: item.lat, lon: item.lon } : {}
}
})
.filter(i => amenities.includes(i.amenity)); // query returns some random results. need to look into this .filter(i => amenities.includes(i.amenity)); // query returns some random results. need to look into this
return output;
} }
async overpassQuery<T = OverpassResultElement>(query: string): Promise<OverpassResult<T>> { async overpassQuery(query: string): Promise<OverpassResult> {
return this.getData('interpreter', { data: query }); return this.getData('interpreter', { data: query });
} }

View File

@ -12,5 +12,4 @@ project.ext {
// useKotlin = true // useKotlin = true
// kotlinVersion = "1.6.0" // kotlinVersion = "1.6.0"
cartoMobileVersion = '4.4.7'
} }

View File

@ -4,7 +4,6 @@
"license": "SEE LICENSE IN <your-license-filename>", "license": "SEE LICENSE IN <your-license-filename>",
"repository": "<fill-your-repository-here>", "repository": "<fill-your-repository-here>",
"dependencies": { "dependencies": {
"@nativescript-community/ui-carto": "^1.8.18",
"@nativescript/core": "*", "@nativescript/core": "*",
"@nativescript/geolocation": "^9.0.0", "@nativescript/geolocation": "^9.0.0",
"@nativescript/localize": "^5.2.0" "@nativescript/localize": "^5.2.0"

View File

@ -1,14 +1,7 @@
import { EventData, Page } from '@nativescript/core'; import { EventData, Page } from '@nativescript/core';
import { HelloWorldModel } from './main-view-model'; import { HelloWorldModel } from './main-view-model';
import { CartoMap } from '@nativescript-community/ui-carto/ui';
import { MapModel } from './map-view-model';
export function navigatingTo(args: EventData) { export function navigatingTo(args: EventData) {
const page = <Page>args.object; const page = <Page>args.object;
page.bindingContext = new HelloWorldModel();
// There are issues with binding the events in the xml file due to naming
// conflicts with the map events (e.g. mapReady) so we use a reference to the map instead
const map = page.getViewById('cartoMap');
map.bindingContext = new MapModel(map as CartoMap);
page.bindingContext = new HelloWorldModel(map.bindingContext);
} }

View File

@ -1,22 +1,22 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" xmlns:map="@nativescript-community/ui-carto/ui" navigatingTo="navigatingTo" class="page"> <Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar> <Page.actionBar>
<ActionBar title="Blakus" icon="" class="action-bar"> <ActionBar title="Blakus" icon="" class="action-bar">
</ActionBar> </ActionBar>
</Page.actionBar> </Page.actionBar>
<StackLayout> <StackLayout class="p-20">
<Label text="Tap the button"/> <Label text="Tap the button" class="h1 text-center"/>
<Button text="TAP" tap="{{ onTap }}" /> <Button text="TAP" tap="{{ onTap }}" />
<Button text="Location" tap="{{ onLocationTap }}" /> <Button text="Location" tap="{{ onLocationTap }}" />
<Label text="{{ message }}" textWrap="true"/> <Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
<map:CartoMap id="cartoMap" row="0" zoom="15" width="100%" height="100%"></map:CartoMap> <ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<!-- <ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<ListView.itemTemplate> <ListView.itemTemplate>
<!-- The item template can only have a single root element -->
<GridLayout padding="16" columns="20, *, *"> <GridLayout padding="16" columns="20, *, *">
<ContentView width="20" height="20" borderRadius="20" backgroundColor="#65adf1" /> <ContentView width="20" height="20" borderRadius="20" backgroundColor="#65adf1" />
<Label text="{{ title }}" col="1" textWrap="true" marginLeft="8" /> <Label text="{{ title }}" col="1" textWrap="true" marginLeft="8" />
<Label text="{{ L(`amenity.${amenity}`) }}" col="2" /> <Label text="{{ L(`amenity.${amenity}`) }}" col="2" />
</GridLayout> </GridLayout>
</ListView.itemTemplate> </ListView.itemTemplate>
</ListView> --> </ListView>
</StackLayout> </StackLayout>
</Page> </Page>

View File

@ -2,7 +2,6 @@ import { Observable, Http, type ItemEventData, ListView } from '@nativescript/co
import { DeviceLocation } from './location'; import { DeviceLocation } from './location';
import type { Location } from '@nativescript/geolocation'; import type { Location } from '@nativescript/geolocation';
import { localize } from '@nativescript/localize'; import { localize } from '@nativescript/localize';
import { MapModel } from './map-view-model';
type ReverseGeocodeResult = { type ReverseGeocodeResult = {
valsts: string; valsts: string;
@ -23,15 +22,20 @@ type ReverseGeocodeResult = {
} }
type BlakusItem = { type BlakusItem = {
id: string;
title: string; title: string;
amenity: string; amenity: string;
tags: Record<string, string>; tags: Record<string, string>;
affinity: number; }
coords: {
lat: number; type OverpassResult = {
lon: number; version: number;
} generator: string;
osm3s: Record<string, string>;
elements: {
type: string,
id: number,
tags: Record<string, string>;
}[]
} }
async function aWait(timeout: number) { async function aWait(timeout: number) {
@ -39,26 +43,38 @@ async function aWait(timeout: number) {
} }
export class HelloWorldModel extends Observable { export class HelloWorldModel extends Observable {
#items: BlakusItem[]; private _counter: number;
#locationServiceBaseURL = 'https://api.kartes.lv/v3/KVDM_mwwKi/' private _message: string;
#blakusBaseURL = 'https://192.168.0.155:3000'; private _items: BlakusItem[];
#map: MapModel; private _locationServiceBaseURL = 'https://api.kartes.lv/v3/KVDM_mwwKi/'
private _blakusBaseURL = 'https://192.168.0.155:3000';
constructor(map: MapModel) { constructor() {
super(); super();
this.#map = map;
this.#map.on('mapMoveEnded', () => { // Initialize default values.
this.getNearbyAmenities(this.#map.focusPosition); this._counter = 42;
}); this.updateMessage();
}
get message(): string {
return this._message;
}
set message(value: string) {
if (this._message !== value) {
this._message = value;
this.notifyPropertyChange('message', value);
}
} }
get items(): BlakusItem[] { get items(): BlakusItem[] {
return this.#items || []; return this._items || [];
} }
set items(value: BlakusItem[]) { set items(value: BlakusItem[]) {
if (this.#items !== value) { if (this._items !== value) {
this.#items = value; this._items = value;
this.notifyPropertyChange('items', value); this.notifyPropertyChange('items', value);
} }
} }
@ -68,7 +84,9 @@ export class HelloWorldModel extends Observable {
params.set('lat', latitude.toString()); params.set('lat', latitude.toString());
params.set('lon', longitude.toString()); params.set('lon', longitude.toString());
const url = new URL(`reverse_geocoding?${params}`, this.#locationServiceBaseURL).toString(); this.message = [latitude, longitude].join(', ');
const url = new URL(`reverse_geocoding?${params}`, this._locationServiceBaseURL).toString();
const result = await Http.getJSON<ReverseGeocodeResult>(url); const result = await Http.getJSON<ReverseGeocodeResult>(url);
console.log(result); console.log(result);
@ -76,33 +94,20 @@ export class HelloWorldModel extends Observable {
} }
getBlakus(params = new URLSearchParams()) { getBlakus(params = new URLSearchParams()) {
const url = new URL(`blakus?${params.toString()}`, this.#blakusBaseURL).toString(); const url = new URL(`blakus?${params.toString()}`, this._blakusBaseURL).toString();
return Http.getJSON<BlakusItem[]>(url) console.log('blakus query', url)
return Http.getJSON<OverpassResult["elements"]>(url)
} }
async getNearbyAmenities({latitude, longitude}: Pick<Location, 'latitude' | 'longitude'>) { async getOverpassAmenities({latitude, longitude}: Location) {
const precision = 4; // limit uncached hits to API
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('lat', latitude.toFixed(precision).toString()); params.set('lat', latitude.toString());
params.set('lon', longitude.toFixed(precision).toString()); params.set('lon', longitude.toString());
this.items = await this.getBlakus(params); const result = await this.getBlakus(params);
console.log(result);
this.items.forEach(({ coords, title, amenity, affinity, id }) => { this.items = result.map(({ tags }) => ({ title: tags.name, amenity: tags.amenity, tags}))
this.#map.addMarker({ // .filter(i => amenities.includes(i.amenity));
id,
position: {
latitude: coords.lat,
longitude: coords.lon
},
detail: {
title,
description: localize(`amenity.${amenity}`),
affinity
}
})
});
this.#map.redrawMarkers();
} }
async postalPolygons(country = 'LV', code: string, mode: 'isolate' | 'collect' | 'union' = 'union') { async postalPolygons(country = 'LV', code: string, mode: 'isolate' | 'collect' | 'union' = 'union') {
@ -113,7 +118,7 @@ export class HelloWorldModel extends Observable {
params.set('union_mode', mode); params.set('union_mode', mode);
params.set('wgs84', ''); params.set('wgs84', '');
// params.set('wkt', ''); // params.set('wkt', '');
const url = new URL(`postal_codes?${params}`, this.#locationServiceBaseURL).toString(); const url = new URL(`postal_codes?${params}`, this._locationServiceBaseURL).toString();
const result = await Http.getJSON(url); const result = await Http.getJSON(url);
console.log(url); console.log(url);
@ -123,19 +128,25 @@ export class HelloWorldModel extends Observable {
async onLocationTap() { async onLocationTap() {
const location = await DeviceLocation.getDeviceLocation(); const location = await DeviceLocation.getDeviceLocation();
const { latitude, longitude } = location; await this.getOverpassAmenities(location);
if (this.#map) { }
this.#map.setPosition({ longitude, latitude }, 500);
onTap() {
this._counter--;
if (this._counter <= 0) {
this.updateMessage('Hoorraaay! You unlocked the NativeScript clicker achievement!');
} else {
this.updateMessage(`${this._counter} taps left`);
} }
await this.getNearbyAmenities(location);
} }
async onTap() { onItemTap(args: ItemEventData) {
await this.getNearbyAmenities(this.#map.focusPosition); const listView = args.object as ListView
console.log('Tapped item', listView.items[args.index])
} }
// onItemTap(args: ItemEventData) { private updateMessage(message = "") {
// const listView = args.object as ListView this.message = message;
// console.log('Tapped item', listView.items[args.index]) }
// }
} }

View File

@ -1,189 +0,0 @@
import { CartoMap, MapEventData, MapMovedEvent, MapReadyEvent } from "@nativescript-community/ui-carto/ui";
import { Dialogs, Observable } from "@nativescript/core";
import { HTTPTileDataSource } from '@nativescript-community/ui-carto/datasources/http';
import { LocalVectorDataSource } from '@nativescript-community/ui-carto/datasources/vector';
import { RasterTileLayer } from '@nativescript-community/ui-carto/layers/raster';
import { VectorElementEventData, VectorLayer } from '@nativescript-community/ui-carto/layers/vector';
import { Marker, MarkerOptions, MarkerStyleBuilder } from '@nativescript-community/ui-carto/vectorelements/marker';
import { GenericMapPos, DefaultLatLonKeys } from "@nativescript-community/ui-carto/core";
function debounce<T extends (...args: any[]) => void>(func: T, delay: number): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return function(...args: Parameters<T>) {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
}
export class MapModel extends Observable {
#map: CartoMap;
#localDataSource: LocalVectorDataSource;
#vectorLayer: VectorLayer;
#markers: Marker<DefaultLatLonKeys>[] = [];
constructor(map: CartoMap) {
super();
this.#map = map;
this.#map.on(MapReadyEvent, this.onMapReady.bind(this));
this.#map.on(MapMovedEvent, this.onMapMoved.bind(this));
}
get options() {
return this.#map.getOptions();
}
get focusPosition() {
return this.#map.getFocusPos();
}
#positionInBounds(position: GenericMapPos<DefaultLatLonKeys>): boolean {
return this.#map.getMapBounds().contains(position);
}
#updateMarkerMetaData(marker: Marker<DefaultLatLonKeys>, data: Record<string, string>) {
marker.setProperty('metaData', { ...marker.metaData, ...data });
// console.log(marker.metaData);
}
#handleElementClick(data: VectorElementEventData<DefaultLatLonKeys>) {
// TODO: actually filter for marker
const marker = data.element as Marker<DefaultLatLonKeys>;
this.#updateMarkerMetaData(marker, { status: 'active' })
Dialogs.alert({
title: marker.metaData.title,
message: marker.metaData.description,
okButtonText: 'OK'
}).then(() => {
this.#updateMarkerMetaData(marker, { status: 'inactive' })
});
this.setPosition(data.elementPos, 200).then(() => {
if (this.#map.getZoom() < 17) {
this.#map.setZoom(17, data.elementPos, 500);
}
this.#map.setBearing(0, 500);
});
}
redrawMarkers() {
const visibleMarkers = this.#markers
.filter(marker => this.#positionInBounds(marker.position))
.sort((a, b) => parseInt(a.metaData.affinity) - parseInt(b.metaData.affinity))
.slice(0, 20);
// Hide markers that are not in bounds
this.#markers
.filter(marker => marker.metaData.status !== 'active' && !visibleMarkers.includes(marker))
.forEach(marker => {
marker.setProperty('visible', false);
this.#localDataSource.remove(marker);
this.#markers = this.#markers.filter(m => m !== marker);
});
// Add markers that are visible and not already added
visibleMarkers.forEach(marker => {
if (!marker.metaData.created_at) {
console.log('adding', marker.metaData);
this.#localDataSource.add(marker);
this.#updateMarkerMetaData(marker, { created_at: Date.now().toString() });
}
this.#updateMarkerMetaData(marker, { last_seen_at: Date.now().toString() });
marker.setProperty('visible', true);
});
}
addMarker({ id, position, detail }: { id: string, position: GenericMapPos, detail: Record<string, any> }) {
if (this.#markers.find(({ options : { metaData } }) => id === metaData.id)) return;
const markerStyleBuilder = new MarkerStyleBuilder({
size: 20,
color: '#0000ff',
clickSize: 50,
// anchor: [0.5, 1],
// image: 'res://marker',
});
const marker = new Marker({
projection: this.#map.projection,
position,
styleBuilder: markerStyleBuilder,
visible: false,
metaData: {
id,
title: detail.title,
description: detail.description,
affinity: detail.affinity,
status: 'inactive'
},
});
this.#markers.push(marker);
}
async setPosition(position: GenericMapPos, duration = 0) {
return new Promise((resolve, reject) => {
try {
this.#map.setFocusPos(position, duration);
setTimeout(() => {
resolve(this.#map.getFocusPos());
}, duration);
} catch (e) {
reject(e);
}
})
}
onMapMoved = debounce(({data}: MapEventData) => {
this.redrawMarkers();
this.notify({ eventName: 'mapMoveEnded', object: this });
}, 500)
onMapReady() {
this.setPosition({ latitude: 56.94964870, longitude: 24.10518640 });
const dataSource = new HTTPTileDataSource({
minZoom: 0,
maxZoom: 22,
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
httpHeaders: { 'User-Agent': 'Blakus' }
});
const rasterLayer = new RasterTileLayer({
dataSource,
zoomLevelBias: 1
});
this.#map.addLayer(rasterLayer);
// source for our stuff
this.#localDataSource = new LocalVectorDataSource({
projection: this.#map.projection
});
this.#vectorLayer = new VectorLayer({
dataSource: this.#localDataSource,
visibleZoomRange: [0, 24]
})
this.#map.addLayer(this.#vectorLayer);
this.#vectorLayer.setVectorElementEventListener({
onVectorElementClicked: this.#handleElementClick.bind(this)
}, this.#map.projection);
this.options.setRotatable(false);
}
}