Map View in Fredy :D (#253)

* init map view

* switching off 3d buildings when sattelite view is on

* rename menu items

* upgrading dependencies, adding provider to popups

* adding screenshot for map view

* fixing readme

* next release version
This commit is contained in:
Christian Kellner
2026-01-12 15:00:36 +01:00
committed by GitHub
parent 7fd8be07a2
commit d43c5b3f97
168 changed files with 16264 additions and 1510 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
@@ -173,9 +173,9 @@ export const storeListings = (jobId, providerId, listings) => {
SqliteConnection.withTransaction((db) => {
const stmt = db.prepare(
`INSERT INTO listings (id, hash, provider, job_id, price, size, title, image_url, description, address,
link, created_at, is_active)
link, created_at, is_active, latitude, longitude)
VALUES (@id, @hash, @provider, @job_id, @price, @size, @title, @image_url, @description, @address, @link,
@created_at, 1)
@created_at, 1, @latitude, @longitude)
ON CONFLICT(job_id, hash) DO NOTHING`,
);
@@ -193,6 +193,8 @@ export const storeListings = (jobId, providerId, listings) => {
address: removeParentheses(item.address),
link: item.link,
created_at: Date.now(),
latitude: item.latitude || null,
longitude: item.longitude || null,
};
stmt.run(params);
}
@@ -392,6 +394,105 @@ export const deleteListingsById = (ids) => {
);
};
/**
* Return all listings that are active, have an address, and do not yet have geocoordinates.
*
* @returns {Object[]} Array of listing objects {id, address}.
*/
export const getListingsToGeocode = () => {
return SqliteConnection.query(
`SELECT id, address
FROM listings
WHERE is_active = 1
AND address IS NOT NULL
AND (latitude IS NULL OR longitude IS NULL)`,
);
};
/**
* Update the geocoordinates for a listing.
*
* @param {string} id - The listing ID.
* @param {number} latitude
* @param {number} longitude
* @returns {void}
*/
export const updateListingGeocoordinates = (id, latitude, longitude) => {
SqliteConnection.execute(
`UPDATE listings
SET latitude = @latitude,
longitude = @longitude
WHERE id = @id`,
{ id, latitude, longitude },
);
};
/**
* Return listings with geocoordinates for the map view, with optional filtering.
*
* @param {Object} params
* @param {string} [params.jobId]
* @param {boolean} [params.activeOnly=true]
* @param {number} [params.minPrice]
* @param {number} [params.maxPrice]
* @param {string} [params.userId]
* @param {boolean} [params.isAdmin=false]
* @returns {{listings: Object[], maxPrice: number}} Object containing listings and maxPrice.
*/
export const getListingsForMap = ({ jobId, minPrice, maxPrice, userId = null, isAdmin = false } = {}) => {
const baseWhereParts = [
'l.latitude IS NOT NULL',
'l.longitude IS NOT NULL',
'l.latitude != -1',
'l.longitude != -1',
'l.is_active = 1',
];
const params = { userId: userId || '__NO_USER__' };
if (!isAdmin) {
baseWhereParts.push(
`(j.user_id = @userId OR EXISTS (SELECT 1 FROM json_each(j.shared_with_user) AS sw WHERE sw.value = @userId))`,
);
}
if (jobId) {
params.jobId = jobId;
baseWhereParts.push('l.job_id = @jobId');
}
const wherePartsForListings = [...baseWhereParts];
if (minPrice !== undefined && minPrice !== null) {
params.minPrice = minPrice;
wherePartsForListings.push('l.price >= @minPrice');
}
if (maxPrice !== undefined && maxPrice !== null) {
params.maxPrice = maxPrice;
wherePartsForListings.push('l.price <= @maxPrice');
}
const listings = SqliteConnection.query(
`SELECT l.*, j.name AS job_name
FROM listings l
LEFT JOIN jobs j ON j.id = l.job_id
WHERE ${wherePartsForListings.join(' AND ')}`,
params,
);
const maxPriceRow = SqliteConnection.query(
`SELECT MAX(l.price) AS maxPrice
FROM listings l
LEFT JOIN jobs j ON j.id = l.job_id
WHERE ${baseWhereParts.join(' AND ')}`,
params,
)[0];
return {
listings,
maxPrice: maxPriceRow?.maxPrice || 0,
};
};
/**
* Return all listings with only the fields: title, address, and price.
* This is the single helper requested for simple consumers.
@@ -401,3 +502,24 @@ export const deleteListingsById = (ids) => {
export const getAllEntriesFromListings = () => {
return SqliteConnection.query(`SELECT title, address, price FROM listings`);
};
/**
* Return geocoordinates for a given address if it has been geocoded before.
*
* @param {string} address
* @returns {{lat: number, lng: number}|null}
*/
export const getGeocoordinatesByAddress = (address) => {
const row = SqliteConnection.query(
`SELECT latitude, longitude
FROM listings
WHERE address = @address
AND latitude IS NOT NULL
AND longitude IS NOT NULL
AND latitude != -1
AND longitude != -1
LIMIT 1`,
{ address },
)[0];
return row ? { lat: row.latitude, lng: row.longitude } : null;
};

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
// Migration: Add geocoordinates to listings for map display
export function up(db) {
db.exec(`
ALTER TABLE listings ADD COLUMN latitude REAL;
ALTER TABLE listings ADD COLUMN longitude REAL;
`);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/