Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

climbingTiles: custom geojson tiles from DB 🎉 #906

Merged
merged 47 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a380d7e
xata init
zbycz Nov 10, 2024
fd54abe
xata schema upload + xata pull main
zbycz Nov 10, 2024
0a1b1a3
fetchAll by single call (8 minutes= 3000 records)
zbycz Nov 10, 2024
9d6224e
rework to transaction
zbycz Nov 11, 2024
b1454af
column change to geojson + tweak size
zbycz Nov 11, 2024
0d9d367
smaller geojson
zbycz Nov 11, 2024
8eba362
add `name` because of skeleton
zbycz Nov 11, 2024
dbfe836
resolve route of type relation (via ferrata eg relation/10621005 )
zbycz Nov 11, 2024
438da56
comment
zbycz Nov 11, 2024
5c030ee
fix test
zbycz Nov 11, 2024
7eef9f3
fetch from overpass
zbycz Nov 12, 2024
f082603
use fetchJson
zbycz Nov 12, 2024
fa83a01
xata-postgre + more cases in refresh
zbycz Nov 14, 2024
0b9bb99
change to pg lib + other db
zbycz Nov 15, 2024
a2c36c3
add vercel.json
zbycz Nov 26, 2024
a9e3b66
use pg-format to format query
zbycz Nov 26, 2024
b24e332
geohashing + pg
zbycz Nov 26, 2024
82ada7c
from overpass
zbycz Nov 26, 2024
6982f65
fix sortKey for groups without `name`
zbycz Dec 5, 2024
9a3aeb5
MVT
zbycz Dec 7, 2024
b40399c
MVT works http://localhost:3000/api/climbing-tiles/tile?z=10&x=553&y=346
zbycz Jan 10, 2025
dace8b3
MVT clientside + pg pool
zbycz Jan 11, 2025
6dd014f
geojson tiles works (from chatgpt)
zbycz Jan 22, 2025
45abd4e
tiles_cache
zbycz Jan 22, 2025
8607b57
getTile refactor
zbycz Jan 24, 2025
5d3a15e
optimize tile to grid 10x10
zbycz Jan 24, 2025
47292c0
client side tiles cache
zbycz Jan 28, 2025
2059f7f
get rid of MVT - it simplifies too much
zbycz Jan 28, 2025
6dc9117
remove unneeded files
zbycz Jan 29, 2025
975eda8
use 4 zoom levels: 0, 6, 9, 12(with routes)
zbycz Jan 29, 2025
955fd94
refactoring vol 1
zbycz Jan 31, 2025
75c1e2f
refactoring vol 2
zbycz Jan 31, 2025
d845d6f
refactoring vol 3
zbycz Jan 31, 2025
ee605be
refactoring vol 4
zbycz Jan 31, 2025
56bc1e7
refactoring vol 5
zbycz Feb 1, 2025
b9ce709
refactoring vol 6
zbycz Feb 2, 2025
f79f3af
refactoring + rename tables
zbycz Feb 2, 2025
79bb3e6
refactoring - extract tileToBBOX + optimizeGeojsonToGrid
zbycz Feb 2, 2025
eb9a996
refactoring - getClimbingTile
zbycz Feb 2, 2025
32157d8
refactoring - db api_keys + fresh db
zbycz Feb 2, 2025
ad5c654
refactoring - db api_keys + fresh db
zbycz Feb 2, 2025
10d06a8
fallback to overpass
zbycz Feb 2, 2025
363418d
add cron, final touches
zbycz Feb 3, 2025
1d79fd3
add cors
zbycz Feb 3, 2025
b739d15
enable by default
zbycz Feb 3, 2025
ecdb650
paralell requests
zbycz Feb 3, 2025
791f931
prod url
zbycz Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ NEXT_PUBLIC_API_KEY_GRAPHHOPPER=f189b841-6529-46c6-8a91-51f17477dcda
# Umami anylytics is used to track server load only. We don't want to track clicks in browser.
# optional, fill blank to disable
UMAMI_WEBSITE_ID=5e4d4917-9031-42f1-a26a-e71d7ab8e3fe

# Use climbingTiles loaded from openclimbing.org (instead of Overpass version)
#NEXT_PUBLIC_ENABLE_CLIMBING_TILES=true
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"open-location-code": "^1.0.3",
"opening_hours": "^3.8.0",
"osm-auth": "^2.5.0",
"pg": "^8.13.1",
"pg-format": "^1.0.4",
"react": "^18.3.1",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^18.3.1",
Expand All @@ -69,6 +71,7 @@
"@types/jest": "^29.5.13",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.10",
"@types/pg-format": "^1.0.5",
"@types/react-custom-scrollbars": "^4.0.13",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/typescript-estree": "^8.8.1",
Expand Down
17 changes: 17 additions & 0 deletions pages/api/climbing-tiles/refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { refreshClimbingTiles } from '../../../src/server/climbing-tiles/refreshClimbingTiles';

export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
if (!process.env.XATA_PASSWORD) {
throw new Error('XATA_PASSWORD must be set');
}

const log = await refreshClimbingTiles();

res.status(200).send(log);
} catch (err) {
console.error(err); // eslint-disable-line no-console
res.status(err.code ?? 400).send(String(err));
}
};
41 changes: 41 additions & 0 deletions pages/api/climbing-tiles/tile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getClimbingTile } from '../../../src/server/climbing-tiles/getClimbingTile';
import { Tile } from '../../../src/types';

const CORS_ORIGINS = [
'http://localhost:3000',
'http://127.0.0.1:3000',
'https://osmapp.org',
];

const addCorsHeaders = (req: NextApiRequest, res: NextApiResponse) => {
const origin = req.headers.origin;
if (CORS_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
};

export default async (req: NextApiRequest, res: NextApiResponse) => {
addCorsHeaders(req, res);
try {
if (!process.env.XATA_PASSWORD) {
throw new Error('XATA_PASSWORD must be set');
}

const tileNumber: Tile = {
z: Number(req.query.z),
x: Number(req.query.x),
y: Number(req.query.y),
};

const geojson = await getClimbingTile(tileNumber);

res.status(200).send(geojson);
} catch (err) {
console.error(err); // eslint-disable-line no-console
res.status(err.code ?? 400).send(String(err));
}
};
6 changes: 0 additions & 6 deletions src/components/Map/TopMenu/HamburgerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,6 @@ export const HamburgerMenu = () => {
<InstallLink closeMenu={close} />
<AboutLink closeMenu={close} />
<GithubLink closeMenu={close} />
{isOpenClimbing && (
<>
<ClimbingAreasLink closeMenu={close} />
<ClimbingGradesTableLink closeMenu={close} />
</>
)}
<StyledDivider />
<LangSwitcher />
</Menu>
Expand Down
9 changes: 8 additions & 1 deletion src/components/Map/behaviour/useUpdateStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { setUpHover } from './featureHover';
import { layersWithOsmId } from '../helpers';
import { Theme } from '../../../helpers/theme';
import { addIndoorEqual, removeIndoorEqual } from './indoor';
import { addClimbingTilesSource } from '../climbingTiles/climbingTilesSource';

const ofrBasicStyle = {
...basicStyle,
Expand Down Expand Up @@ -85,12 +86,18 @@ const addOverlaysToStyle = (
overlays: string[],
currentTheme: Theme,
) => {
// removeClimbingTilesSource(); // TODO call when climbing removed

overlays
.filter((key: string) => osmappLayers[key]?.type === 'overlay')
.forEach((key: string) => {
switch (key) {
case 'climbing':
addClimbingOverlay(style, map);
if (process.env.NEXT_PUBLIC_ENABLE_CLIMBING_TILES) {
addClimbingTilesSource(style);
} else {
addClimbingOverlay(style, map); // TODO remove this when climbingTiles are tested
}
break;

case 'indoor':
Expand Down
223 changes: 223 additions & 0 deletions src/components/Map/climbingTiles/climbingLayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import {
ExpressionSpecification,
LayerSpecification,
SymbolLayerSpecification,
} from '@maplibre/maplibre-gl-style-spec';
import type { DataDrivenPropertyValueSpecification } from 'maplibre-gl';
import { AREA, CRAG } from '../MapFooter/ClimbingLegend';

export const CLIMBING_TILES_SOURCE = 'climbing-tiles';

export const CLIMBING_SPRITE = {
id: 'climbing',
url: `${window.location.protocol}//${window.location.host}/icons-climbing/sprites/climbing`,
};

const linear = (
from: number,
a: number | ExpressionSpecification,
mid: number,
b: number | ExpressionSpecification,
to?: number,
c?: number | ExpressionSpecification,
): ExpressionSpecification => [
'interpolate',
['linear'],
['zoom'],
from,
a,
mid,
b,
...(to && c ? [to, c] : []),
];

const linearByRouteCount = (
from: number,
a: number | ExpressionSpecification,
to: number,
b: number | ExpressionSpecification,
): ExpressionSpecification => [
'interpolate',
['linear'],
['coalesce', ['get', 'osmappRouteCount'], 0],
from,
a,
to,
b,
];

const ifHasImages = (
value: string,
elseValue: string,
): ExpressionSpecification => [
'case',
['get', 'osmappHasImages'],
value,
elseValue,
];

const byHasImages = (
spec: typeof AREA | typeof CRAG,
property: 'IMAGE' | 'COLOR',
): ExpressionSpecification =>
ifHasImages(spec.HAS_IMAGES[property], spec.NO_IMAGES[property]);

const ifCrag = (
value: ExpressionSpecification | number,
elseValue: ExpressionSpecification | number,
): ExpressionSpecification => [
'case',
['==', ['get', 'climbing'], 'crag'],
value,
elseValue,
];

const sortKey = [
'*',
-1,
[
'+',
['to-number', ['get', 'osmappRouteCount']],
['case', ['get', 'osmappHasImages'], 10000, 0], // preference for items with images
['case', ['to-boolean', ['get', 'name']], 2, 0], // prefer items with name
],
] as DataDrivenPropertyValueSpecification<number>;

const hover = (basic: number, hovered: number): ExpressionSpecification => [
'case',
['boolean', ['feature-state', 'hover'], false],
hovered,
basic,
];
const step = (
a: unknown,
from: number,
b: unknown,
): ExpressionSpecification => [
'step',
['zoom'],
['literal', a],
from,
['literal', b],
];

export const routes: LayerSpecification[] = [
{
id: 'climbing routes (line)',
type: 'line',
source: CLIMBING_TILES_SOURCE,
minzoom: 16,
filter: ['all', ['==', 'type', 'route']],
paint: {
'line-width': 2,
},
},
{
id: 'climbing routes (circle)',
type: 'circle',
source: CLIMBING_TILES_SOURCE,
minzoom: 16,
filter: ['all', ['==', 'type', 'route']],
paint: {
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#4150a0',
'#ea5540',
],
'circle-radius': linear(16, 1, 21, 6),
'circle-opacity': linear(16, 0.4, 21, 1),
},
} as LayerSpecification,
{
id: 'climbing routes (labels)',
type: 'symbol',
source: CLIMBING_TILES_SOURCE,
minzoom: 19,
filter: ['all', ['==', 'type', 'route']],
layout: {
'text-padding': 2,
'text-font': ['Noto Sans Medium'],
'text-anchor': 'left',
'text-field': '{name} {climbing:grade:uiaa}',
'text-offset': [1, 0],
'text-size': linear(20, 12, 26, 30),
'text-max-width': 9,
'text-allow-overlap': false,
'text-optional': true,
},
paint: {
'text-halo-blur': 0.5,
'text-color': '#666',
'text-halo-width': 1,
'text-halo-color': '#ffffff',
},
},
];

const COMMON_LAYOUT: SymbolLayerSpecification['layout'] = {
'icon-optional': false,
'icon-ignore-placement': false,
'icon-allow-overlap': ['step', ['zoom'], true, 4, false],
'text-field': ['step', ['zoom'], '', 4, ['get', 'osmappLabel']],
'text-padding': 2,
'text-font': ['Noto Sans Bold'],
'text-anchor': 'top',
'text-max-width': 9,
'text-ignore-placement': false,
'text-allow-overlap': false,
'text-optional': true,
'symbol-sort-key': sortKey,
};

const COMMON_PAINT: SymbolLayerSpecification['paint'] = {
'text-halo-color': '#ffffff',
'text-halo-width': 2,
};

const areaSize = linearByRouteCount(0, 0.4, 400, 1);
const cragSize = linearByRouteCount(0, 0.4, 50, 0.7);
const cragSizeBig = 1;

const mixed: LayerSpecification = {
id: 'climbing crags and areas',
type: 'symbol',
source: CLIMBING_TILES_SOURCE,
maxzoom: 20,
filter: ['all', ['==', 'type', 'group']],
layout: {
'icon-image': ifCrag(
byHasImages(CRAG, 'IMAGE'),
byHasImages(AREA, 'IMAGE'),
),
'icon-size': linear(
6,
0.4,
8,
ifCrag(cragSize, areaSize),
21,
ifCrag(cragSizeBig, areaSize),
),
'text-size': linear(
5,
ifCrag(12, 12),
15,
ifCrag(12, 14),
21,
ifCrag(20, 14),
),
'text-offset': [0, 0.6],
...COMMON_LAYOUT,
},
paint: {
'icon-opacity': hover(1, 0.6),
'text-opacity': hover(1, 0.6),
'text-color': ifCrag(
byHasImages(CRAG, 'COLOR'),
byHasImages(AREA, 'COLOR'),
),
...COMMON_PAINT,
},
};

export const climbingLayers: LayerSpecification[] = [...routes, mixed];
Loading