From 1c2d245f7cafa795e3bda276f3faa4165d3842fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Broks=20Randolfs=20Gail=C4=ABtis?= Date: Sat, 15 Mar 2025 12:52:17 +0200 Subject: [PATCH] Initial places and map implementation --- .../migrations/20250304134127_initial.js | 24 +-- .../migrations/20250310143023_places.js | 23 +++ apps/blakus-api/seeds/places.js | 30 +++ apps/blakus-api/seeds/users.js | 28 +-- apps/blakus-api/src/app/routes/blakus.ts | 65 +++++- apps/blakus-api/src/app/routes/places.ts | 50 +++++ apps/blakus-api/src/app/services/overpass.ts | 13 +- .../Android/before-plugins.gradle | 1 + apps/blakus-nativescript/package.json | 1 + apps/blakus-nativescript/src/main-page.ts | 9 +- apps/blakus-nativescript/src/main-page.xml | 14 +- .../src/main-view-model.ts | 119 +++++------ .../blakus-nativescript/src/map-view-model.ts | 189 ++++++++++++++++++ 13 files changed, 450 insertions(+), 116 deletions(-) create mode 100644 apps/blakus-api/migrations/20250310143023_places.js create mode 100644 apps/blakus-api/seeds/places.js create mode 100644 apps/blakus-api/src/app/routes/places.ts create mode 100644 apps/blakus-nativescript/src/map-view-model.ts diff --git a/apps/blakus-api/migrations/20250304134127_initial.js b/apps/blakus-api/migrations/20250304134127_initial.js index 45b31a1..edba833 100644 --- a/apps/blakus-api/migrations/20250304134127_initial.js +++ b/apps/blakus-api/migrations/20250304134127_initial.js @@ -6,22 +6,12 @@ exports.up = function(knex) { return knex.schema .createTable('users', function (table) { - table.uuid('id') - .primary() - .defaultTo(knex.raw(`gen_random_uuid()`)); - table.string('first_name', 255) - .notNullable(); - table.string('last_name', 255) - .notNullable(); - table.string('email', 255) - .unique() - .notNullable(); + table.uuid('id').primary().defaultTo(knex.raw(`gen_random_uuid()`)); + table.string('first_name', 255).notNullable(); + table.string('last_name', 255).notNullable(); + table.string('email', 255).unique().notNullable(); + table.timestamps(true, true); }) - // .createTable('products', function (table) { - // table.increments('id'); - // table.decimal('price').notNullable(); - // table.string('name', 1000).notNullable(); - // }); }; /** @@ -29,7 +19,5 @@ exports.up = function(knex) { * @returns { Promise } */ exports.down = function(knex) { - return knex.schema - // .dropTable('products') - .dropTable('users'); + return knex.schema.dropTable('users'); }; diff --git a/apps/blakus-api/migrations/20250310143023_places.js b/apps/blakus-api/migrations/20250310143023_places.js new file mode 100644 index 0000000..9f8f5e3 --- /dev/null +++ b/apps/blakus-api/migrations/20250310143023_places.js @@ -0,0 +1,23 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +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 } + */ +exports.down = function(knex) { + return knex.schema.dropTable('places'); +}; diff --git a/apps/blakus-api/seeds/places.js b/apps/blakus-api/seeds/places.js new file mode 100644 index 0000000..c2ce9a7 --- /dev/null +++ b/apps/blakus-api/seeds/places.js @@ -0,0 +1,30 @@ +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 } + */ +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)), + ]); +}; diff --git a/apps/blakus-api/seeds/users.js b/apps/blakus-api/seeds/users.js index e855fe9..bf31bc6 100644 --- a/apps/blakus-api/seeds/users.js +++ b/apps/blakus-api/seeds/users.js @@ -2,25 +2,25 @@ const { fakerLV : faker } = require('@faker-js/faker'); 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 * @returns { Promise } */ 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').insert([ createUser({ diff --git a/apps/blakus-api/src/app/routes/blakus.ts b/apps/blakus-api/src/app/routes/blakus.ts index 2a6bcf3..126e6ad 100644 --- a/apps/blakus-api/src/app/routes/blakus.ts +++ b/apps/blakus-api/src/app/routes/blakus.ts @@ -1,8 +1,9 @@ import { FastifyInstance } from 'fastify'; -import { OverpassService } from '../services/overpass'; +// import { AMENITIES, OverpassService } from '../services/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) { @@ -15,6 +16,7 @@ export default async function (fastify: FastifyInstance) { items: { type: 'object', properties: { + id: { type: 'string' }, title: { type: 'string' }, amenity: { type: 'string' }, coords: { @@ -24,21 +26,66 @@ export default async function (fastify: FastifyInstance) { lon: { type: 'number' } } }, - tags: { type: 'object', additionalProperties: true } + affinity: { type: 'number' }, + // tags: { type: 'object', additionalProperties: true } } } } } }, config: { - serverCache: { ttl: 10000 } + // serverCache: { ttl: 1 * 60 * 1000 } } }, async function (request, reply) { const {lat, lon} = request.query; - const elements = await overpassService.getAmenitiesAroundLocation({ - coords: { lat, lon }, - amenities: ['restaurant', 'cafe', 'bar', 'pub', 'biergarten', 'fast_food', 'food_court', 'ice_cream'] - }); - reply.send(elements); + + const knownPlaces = await db.select( + 'id', + 'name', + 'externalId', + 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); }); } diff --git a/apps/blakus-api/src/app/routes/places.ts b/apps/blakus-api/src/app/routes/places.ts new file mode 100644 index 0000000..a257e1e --- /dev/null +++ b/apps/blakus-api/src/app/routes/places.ts @@ -0,0 +1,50 @@ +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 }); + }); +} \ No newline at end of file diff --git a/apps/blakus-api/src/app/services/overpass.ts b/apps/blakus-api/src/app/services/overpass.ts index 6f49d3f..9e667df 100644 --- a/apps/blakus-api/src/app/services/overpass.ts +++ b/apps/blakus-api/src/app/services/overpass.ts @@ -48,6 +48,12 @@ type OverpassQuerySettings = { date?: Date; } +export const AMENITIES = { + get Sustanance() { + return ['restaurant', 'cafe', 'bar', 'pub', 'biergarten', 'fast_food', 'food_court', 'ice_cream'] + } +}; + function overpassQueryTemplate ({ out = 'json', timeout, maxsize, bbox = [34.016242,-24.433594,71.663663,48.164063], date}: OverpassQuerySettings, statements: string[] = ['out geom']) { return function (strings, ...expressions) { let query = [ @@ -82,17 +88,18 @@ export class OverpassService { const { lat, lon } = coords; - const opq = overpassQueryTemplate({ out: 'json', timeout: 25 }); + const opq = overpassQueryTemplate({ out: 'json', timeout: 60 }); const query = opq`node(around:${radius}, ${lat}, ${lon})["amenity"~"${amenities.join('|')}"];`; const result = await this.overpassQuery(query); - return result.elements + const output = result.elements .map(item => { const { tags } = item; return { + id: item.id, title: tags.name, amenity: tags.amenity, tags, @@ -100,6 +107,8 @@ export class OverpassService { } }) .filter(i => amenities.includes(i.amenity)); // query returns some random results. need to look into this + + return output; } async overpassQuery(query: string): Promise> { diff --git a/apps/blakus-nativescript/App_Resources/Android/before-plugins.gradle b/apps/blakus-nativescript/App_Resources/Android/before-plugins.gradle index 9faffb8..a954cec 100644 --- a/apps/blakus-nativescript/App_Resources/Android/before-plugins.gradle +++ b/apps/blakus-nativescript/App_Resources/Android/before-plugins.gradle @@ -12,4 +12,5 @@ project.ext { // useKotlin = true // kotlinVersion = "1.6.0" + cartoMobileVersion = '4.4.7' } diff --git a/apps/blakus-nativescript/package.json b/apps/blakus-nativescript/package.json index 62022dd..12bc886 100644 --- a/apps/blakus-nativescript/package.json +++ b/apps/blakus-nativescript/package.json @@ -4,6 +4,7 @@ "license": "SEE LICENSE IN ", "repository": "", "dependencies": { + "@nativescript-community/ui-carto": "^1.8.18", "@nativescript/core": "*", "@nativescript/geolocation": "^9.0.0", "@nativescript/localize": "^5.2.0" diff --git a/apps/blakus-nativescript/src/main-page.ts b/apps/blakus-nativescript/src/main-page.ts index dfd7622..e96dcc2 100644 --- a/apps/blakus-nativescript/src/main-page.ts +++ b/apps/blakus-nativescript/src/main-page.ts @@ -1,7 +1,14 @@ import { EventData, Page } from '@nativescript/core'; 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) { const 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); } diff --git a/apps/blakus-nativescript/src/main-page.xml b/apps/blakus-nativescript/src/main-page.xml index 4f0c4ef..c47c67f 100644 --- a/apps/blakus-nativescript/src/main-page.xml +++ b/apps/blakus-nativescript/src/main-page.xml @@ -1,22 +1,22 @@ - + - -