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

Release 2025-01-08 #161

Merged
merged 16 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ DATABASE_PASSWORD=mysecretpassword
DATABASE_PORT=5432

# Application flags
OSM_DOWNLOAD_URL=http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
OSM_DOWNLOAD_URL=https://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
SKIP_DOWNLOAD=1 # skips the download if a file already exists
SKIP_UNCHANGED=1 # skips processing of unchanged code
SKIP_TAG_FILTER=0 # skips tag filtering
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions app/src/analysis/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from 'chalk'
import { aggregateLengths } from './aggregateLengths'

// This function gets called by the `private/post-processing-hook` endpoint
// To run it locally, use http://127.0.0.1:5173/api/private/post-processing-hook?apiKey=<KEY_FROM_ENV>
export async function analysis() {
try {
const startTime = Date.now()
Expand Down
36 changes: 36 additions & 0 deletions app/src/app/api/processing-dates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isProd } from '@/src/app/_components/utils/isEnv'
import { geoDataClient } from '@/src/prisma-client'
import { NextResponse } from 'next/server'
import { z } from 'zod'

const DatesSchema = z.object({
processed_at: z.date(),
osm_data_from: z.date(),
})

export async function GET() {
try {
const result = await geoDataClient.$queryRaw`
SELECT processed_at, osm_data_from
FROM public.meta
ORDER BY id DESC
LIMIT 1
`

const parsed = DatesSchema.parse(
// @ts-expect-error
result[0],
)

return NextResponse.json(parsed)
} catch (error) {
console.error(error) // Log files
return Response.json(
{
error: 'Internal Server Error',
info: isProd ? undefined : error,
},
{ status: 500 },
)
}
}
45 changes: 42 additions & 3 deletions app/src/app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import { isProd } from '@/src/app/_components/utils/isEnv'
import { geoDataClient } from '@/src/prisma-client'
import { feature, featureCollection, simplify, truncate } from '@turf/turf'
import { NextResponse } from 'next/server'
import { z } from 'zod'

const position = z.tuple([z.number(), z.number()])
const linearRing = z.array(position)
const polygon = z.array(linearRing)
const multiPolygon = z.object({
type: z.literal('MultiPolygon'),
coordinates: z.array(polygon),
})
const DbStatSchema = z.object({
id: z.string(),
name: z.string(),
level: z.enum(['4', '6']),
road_length: z.record(z.string(), z.number()),
bikelane_length: z.record(z.string(), z.number()).nullable(),
geojson: multiPolygon,
})
const DbStatsSchema = z.array(DbStatSchema)

export async function GET() {
try {
const stats = await geoDataClient.$queryRaw`
SELECT name, level, road_length, bikelane_length from public.aggregated_lengths;`
return NextResponse.json(stats)
const raw = await geoDataClient.$queryRaw`
SELECT
id,
name,
level,
road_length,
bikelane_length,
ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb AS geojson
FROM public.aggregated_lengths;`

const parsed = DbStatsSchema.parse(raw)

const features = parsed.map(({ geojson, ...properties }) => {
// GEOMETRY: Cleanup
// Docs https://turfjs.org/docs/api/simplify
const geomSimplified = simplify(geojson, { tolerance: 0.05 })
// Docs https://turfjs.org/docs/api/truncate
const geomTruncated = truncate(geomSimplified, { precision: 6, coordinates: 2 })

return feature(geomTruncated, properties, { id: properties.id })
})

return NextResponse.json(featureCollection(features))
} catch (error) {
console.error(error) // Log files
return Response.json(
Expand Down
8 changes: 4 additions & 4 deletions app/src/app/regionen/(index)/_data/regions.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,12 +743,12 @@ export const staticRegion: StaticRegion[] = [
},
{
slug: 'radinfra',
name: 'Radinfra.de',
fullName: 'Radinfrastruktur Detuschland (radinfra.de)',
name: 'radinfra.de',
fullName: 'Radinfrastrukturdaten für Deutschland (radinfra.de)',
osmRelationIds: [],
map: { lat: 51.07, lng: 13.35, zoom: 5 },
map: { lat: 51.07, lng: 13.35, zoom: 6 },
bbox: null,
logoPath: null,
externalLogoPath: 'https://radinfra.de/radinfra-de-logo.png',
logoWhiteBackgroundRequired: false,
categories: [
// The order here specifies the order in the UI
Expand Down
11 changes: 6 additions & 5 deletions processing/steps/download.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { basename, join } from 'path'
import { OSM_DOWNLOAD_DIR } from '../constants/directories.const'
import { writePersistent } from '../utils/persistentData'
import { readPersistent, writePersistent } from '../utils/persistentData'
import { synologyLogError } from '../utils/synology'

/**
Expand Down Expand Up @@ -75,10 +75,10 @@ export async function downloadFile(fileURL: URL, skipIfExists: boolean) {
throw new Error('No ETag found')
}

// if (eTag === (await readPersistent(fileName))) {
// console.log('⏩ Skipped download because the file has not changed.')
// return { fileName, fileChanged: false }
// }
if (eTag === (await readPersistent(fileName))) {
console.log('⏩ Skipped download because the file has not changed.')
return { fileName, fileChanged: false }
}

// download file and write to disc
console.log(`Downloading file "${fileName}"...`)
Expand All @@ -96,6 +96,7 @@ export async function downloadFile(fileURL: URL, skipIfExists: boolean) {
if (done) break
await writer.write(value)
}
writer.end()

// save etag
writePersistent(fileName, eTag)
Expand Down
4 changes: 2 additions & 2 deletions processing/topics/helper/ContainsSubstring.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---@param str string
---@param subStr string
---@param str string | nil
---@param subStr string | nil
---@return boolean
function ContainsSubstring(str, subStr)
if (str == nil) then return false end
Expand Down
57 changes: 31 additions & 26 deletions processing/topics/roads_bikelanes/bikelanes/BikelaneCategories.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package.path = package.path .. ";/processing/topics/roads_bikelanes/bikelanes/categories/?.lua"
package.path = package.path .. ";/processing/topics/helper/?.lua"
require("Set")
require("ContainsSubstring")
require("IsSidepath")
require("CreateSubcategoriesAdjoiningOrIsolated")
Expand Down Expand Up @@ -187,6 +188,7 @@ local footAndCyclewaySegregated = BikelaneCategory.new({
if osm2pgsql.has_prefix(trafficSign, "DE:241") then
return true
end

-- Edge case: https://www.openstreetmap.org/way/1319011143#map=18/52.512226/13.288552
-- No traffic_sign but mapper decided to map foot- and bike lane as separate geometry
-- We check for traffic_mode:right=foot
Expand Down Expand Up @@ -324,6 +326,11 @@ local cyclewayOnHighway_advisoryOrExclusive = BikelaneCategory.new({
condition = function(tags)
if tags.highway == 'cycleway' then
if tags._side ~= 'self' then
-- "Angstweichen" are a special case where the cycleway is part of the road which is tagged using one of their `*:lanes` schema.
-- Those get usually dual tagged as `cycleway:right=lane` to make the "Angstweiche" "visible" to routing.
-- For this category, we skip the dual tagging but still want to capture cases where there is an actual `lane` ("Schutzstreifen") as well as a "Angstweiche".
-- The actual double infra is present when the lanes have both "|lane|" (the "Angstweiche") as well a a suffix "|lane" (the "Schutzstreifen").
-- Note: `tags.lanes` is `cycleway:lanes` but unnested whereas `bicycle:lanes` does not get unnested.
if ContainsSubstring(tags.lanes,'|lane|') then
if not osm2pgsql.has_suffix(tags.lanes, '|lane') then
return false
Expand Down Expand Up @@ -420,43 +427,38 @@ local protectedCyclewayOnHighway = BikelaneCategory.new({
infrastructureExists = true,
implicitOneWay = true, -- "lane"-like
condition = function(tags)
local function isPhysicalSeperation(separation)
local physicalSeparations = {
'bollard',
'parking_lane',
'bump',
'separation_kerb',
'vertical_panel',
'fence',
'flex_post',
'jersey_barrier',
'kerb'
}
for _, value in pairs(physicalSeparations) do
if ContainsSubstring(separation, value) then
return true
end
end
end
-- we go from specific to general tags (:side > :both > '')
local separationFallback = tags['separation:both'] or tags['separation']
-- only include center line tagged cycleways
-- Only include center line tagged cycleways
if tags._prefix == nil then
return false
end

local separation_left = tags['separation:left'] or separationFallback
if not isPhysicalSeperation(separation_left) then
-- We go from specific to general tags (:side > :both > '')
local separation_left = tags['separation:left'] or tags['separation:both'] or tags['separation']
local separation_right = tags['separation:right'] or tags['separation:both'] or tags['separation']
local physicalSeparations = Set({
'bollard',
'parking_lane',
'bump',
'separation_kerb',
'vertical_panel',
'fence',
'flex_post',
'jersey_barrier',
'kerb'
})

if not physicalSeparations[separation_left] then
return false
end

-- Check also the left separation for the rare case that there is motorized traffic on the right hand side
local traffic_mode_right = tags['traffic_mode:right'] or tags['traffic_mode:both'] or tags['traffic_mode']
if traffic_mode_right == 'motorized' then
local separation_right = tags['separation:right'] or separationFallback
if not isPhysicalSeperation(separation_right) then
if not physicalSeparations[separation_right] then
return false
end
end

return true
end
})
Expand Down Expand Up @@ -529,13 +531,16 @@ local needsClarification = BikelaneCategory.new({
return false
end
end

if tags.cycleway == "shared" then
return true
end

if tags.highway == "cycleway"
or (tags.highway == "path" and tags.bicycle == "designated") then
return true
end

if tags.highway == 'footway' and tags.bicycle == 'designated' then
return true
end
Expand Down Expand Up @@ -571,7 +576,7 @@ local categoryDefinitions = {
cyclewayOnHighway_advisory,
cyclewayOnHighway_exclusive,
cyclewayOnHighway_advisoryOrExclusive,
footwayBicycleYes_adjoining, -- after `cyclewaySeparatedCases`
footwayBicycleYes_adjoining, -- after `cyclewaySeparated_*`
footwayBicycleYes_isolated,
footwayBicycleYes_adjoiningOrIsolated,
-- Needs to be last
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ BikelaneTodos = {
missing_acccess_tag_bicycle_road,
missing_traffic_sign_but_bicycle_designated,
missing_traffic_sign_but_bicycle_yes,
missing_traffic_sign,
missing_access_tag_240,
missing_segregated,
missing_sidepath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe("Bikelanes", function()

describe("explicit category tests", function()

it('Categories for "angstweiche"', function()
it('Categories for "Angstweiche" and "Schutzstreifen" using cycleway:lanes', function()
local input_object = {
tags = {
highway = 'tertiary',
Expand All @@ -140,9 +140,62 @@ describe("Bikelanes", function()
assert.are.equal(v.category, "cyclewayOnHighway_exclusive")
end
end
-- same test but with `bicycle:lanes`
input_object.tags['cycleway:lanes'] = nil
input_object.tags['bicycle:lanaes'] = 'no|no|no|designated|no|designated'
end)

it('Categories for "Angstweiche" and "Schutzstreifen" using bicycle:lanes', function()
local input_object = {
tags = {
highway = 'tertiary',
['cycleway:right'] = 'lane',
['cycleway:right:lane'] = 'exclusive',
['bicycle:lanes'] = 'no|no|no|designated|no|designated',
['cycleway:right:traffic_sign'] = 'DE:237'
},
id = 1,
type = 'way'
}
local result = Bikelanes(input_object)
for _, v in pairs(result) do
if v._side == 'self' then
assert.are.equal(v.category, "cyclewayOnHighwayBetweenLanes")
end
if v._side == 'right' and v.prefix == 'cycleway' then
assert.are.equal(v.category, "cyclewayOnHighway_exclusive")
end
end
end)

it('Categories for "Angstweiche" (only) using cycleway:lanes', function()
local input_object = {
tags = {
highway = 'tertiary',
['cycleway:right'] = 'lane',
['cycleway:lanes'] = 'no|no|no|lane|no',
},
id = 1,
type = 'way'
}
local result = Bikelanes(input_object)
for _, v in pairs(result) do
if v._side == 'self' then
assert.are.equal(v.category, "cyclewayOnHighwayBetweenLanes")
end
if v._side == 'right' and v.prefix == 'cycleway' then
assert.are.equal(v.category, "cyclewayOnHighway_exclusive")
end
end
end)

it('Categories for "Angstweiche" (only) using bicycle:lanes', function()
local input_object = {
tags = {
highway = 'tertiary',
['cycleway:right'] = 'lane',
['bicycle:lanes'] = 'no|no|no|designated|no',
},
id = 1,
type = 'way'
}
local result = Bikelanes(input_object)
for _, v in pairs(result) do
if v._side == 'self' then
Expand Down Expand Up @@ -196,5 +249,28 @@ describe("Bikelanes", function()
end
end
end)

pending('Bug https://www.openstreetmap.org/way/565095160/history/5', function()
local input_object = {
highway = 'primary',
tags = {
['cycleway:left'] = 'lane',
['cycleway:left:bicyle'] = 'yes',
['cycleway:separation:left'] = 'yes'
},
id = 1,
type = 'way'
}
local result = Bikelanes(input_object)
for _, v in pairs(result) do
if v._side == 'right' and v.prefix == 'cycleway' then
assert.are.equal("a", v)
assert.are.equal("a", v.category)
end
if v._side == 'left' and v.prefix == 'cycleway' then
assert.are.equal("cyclewayOnHighway_advisoryOrExclusive", v.category)
end
end
end)
end)
end)
Loading
Loading