2024-01-16 04:07:02 +01:00
import Script from "../Script"
import fs from "fs"
2024-04-05 17:49:31 +02:00
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
import { Utils } from "../../src/Utils"
2024-12-17 03:31:28 +01:00
import { Feature , FeatureCollection , Point } from "geojson"
2024-04-05 17:49:31 +02:00
import { BBox } from "../../src/Logic/BBox"
2024-01-16 04:07:02 +01:00
import { Overpass } from "../../src/Logic/Osm/Overpass"
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
import { ImmutableStore } from "../../src/Logic/UIEventSource"
2024-04-05 17:49:31 +02:00
import Constants from "../../src/Models/Constants"
2024-12-17 03:31:28 +01:00
import { MaprouletteStatus } from "../../src/Logic/Maproulette"
2024-01-17 18:08:14 +01:00
2024-01-16 04:07:02 +01:00
class VeloParkToGeojson extends Script {
constructor ( ) {
2024-01-17 18:08:14 +01:00
super (
2024-12-17 04:39:38 +01:00
"Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory"
2024-01-17 18:08:14 +01:00
)
2024-01-16 04:07:02 +01:00
}
2024-04-05 17:49:31 +02:00
private static exportGeojsonTo ( filename : string , features : Feature [ ] , extension = ".geojson" ) {
2024-04-13 02:40:21 +02:00
const file = filename + "_" + /*new Date().toISOString() + */ extension
fs . writeFileSync (
file ,
2024-02-15 01:07:50 +01:00
JSON . stringify (
2024-04-13 02:40:21 +02:00
extension === ".geojson"
? {
2024-12-17 04:39:38 +01:00
type : "FeatureCollection" ,
features ,
}
2024-04-13 02:40:21 +02:00
: features ,
2024-02-15 01:07:50 +01:00
null ,
2024-12-17 04:39:38 +01:00
" "
)
2024-02-15 01:07:50 +01:00
)
2024-04-13 01:36:37 +02:00
console . log ( "Written" , file , "(" + features . length , " features)" )
2024-02-15 01:07:50 +01:00
}
2024-06-16 16:06:26 +02:00
private static async downloadDataFor ( url : string ) : Promise < Feature [ ] > {
const cachePath =
"/home/pietervdvn/data/velopark_cache_refined/" + url . replace ( /[/:.]/g , "_" )
2024-06-14 01:01:41 +02:00
if ( fs . existsSync ( cachePath ) ) {
return JSON . parse ( fs . readFileSync ( cachePath , "utf8" ) )
2024-04-13 01:36:37 +02:00
}
2024-06-14 01:01:41 +02:00
console . log ( "Fetching data for" , url )
2024-04-13 01:36:37 +02:00
const linkedData = await LinkedDataLoader . fetchVeloparkEntry ( url )
const allVelopark : Feature [ ] = [ ]
2024-12-17 03:31:28 +01:00
if ( linkedData . length > 1 ) {
console . log ( "Detected multiple sections in:" , url )
}
2024-04-13 01:36:37 +02:00
for ( const sectionId in linkedData ) {
const sectionInfo = linkedData [ sectionId ]
if ( Object . keys ( sectionInfo ) . length === 0 ) {
console . warn ( "No result for" , url )
}
2024-12-17 03:31:28 +01:00
if ( ! sectionInfo . geometry ? . [ "coordinates" ] ) {
2024-06-14 01:01:41 +02:00
throw "Invalid properties!"
}
2024-04-13 01:36:37 +02:00
allVelopark . push ( sectionInfo )
}
2024-06-14 01:01:41 +02:00
fs . writeFileSync ( cachePath , JSON . stringify ( allVelopark ) , "utf8" )
2024-04-13 01:36:37 +02:00
return allVelopark
}
2024-04-05 17:49:31 +02:00
private static async downloadData() {
2024-01-16 04:07:02 +01:00
console . log ( "Downloading velopark data" )
// Download data for NIS-code 1000. 1000 means: all of belgium
const url = "https://www.velopark.be/api/parkings/1000"
2024-12-17 04:39:38 +01:00
const allVeloparkRaw = await Utils . downloadJson < { url : string } [ ] > ( url )
2024-12-17 03:31:28 +01:00
// Example multi-entry: https://data.velopark.be/data/Stad-Izegem_IZE_015
2024-04-05 17:49:31 +02:00
let failed = 0
console . log ( "Got" , allVeloparkRaw . length , "items" )
const allVelopark : Feature [ ] = [ ]
2024-06-14 01:01:41 +02:00
const batchSize = 50
for ( let i = 0 ; i < allVeloparkRaw . length ; i += batchSize ) {
2024-06-16 16:06:26 +02:00
await Promise . all (
Utils . TimesT ( batchSize , ( j ) = > j ) . map ( async ( j ) = > {
2024-06-14 01:01:41 +02:00
const f = allVeloparkRaw [ i + j ]
if ( ! f ) {
return
}
try {
const sections : Feature [ ] = await VeloParkToGeojson . downloadDataFor ( f . url )
allVelopark . push ( . . . sections )
} catch ( e ) {
console . error ( "Loading " , f . url , " failed due to" , e )
failed ++
}
2024-12-17 04:39:38 +01:00
} )
2024-06-16 16:06:26 +02:00
)
2024-12-17 03:31:28 +01:00
console . log ( "Batch complete:" , i )
2024-04-05 17:49:31 +02:00
}
2024-04-13 02:40:21 +02:00
console . log (
"Fetching data done, got " ,
allVelopark . length + "/" + allVeloparkRaw . length ,
"failed:" ,
2024-12-17 04:39:38 +01:00
failed
2024-04-13 02:40:21 +02:00
)
2024-04-13 01:36:37 +02:00
VeloParkToGeojson . exportGeojsonTo ( "velopark_all" , allVelopark )
2024-04-05 17:49:31 +02:00
return allVelopark
}
2024-04-13 02:40:21 +02:00
private static loadFromFile ( maxCacheAgeSeconds = 24 * 60 * 60 ) : Feature [ ] | null {
2024-04-13 01:36:37 +02:00
const path = "velopark_all.geojson"
2024-04-13 02:40:21 +02:00
if ( ! fs . existsSync ( path ) ) {
2024-04-13 01:36:37 +02:00
return null
}
// Millis since epoch
2024-04-13 02:40:21 +02:00
const mtime : number = fs . statSync ( path ) . mtime . getTime ( )
2024-04-13 01:36:37 +02:00
const stalenessSeconds = ( new Date ( ) . getTime ( ) - mtime ) / 1000
2024-04-13 02:40:21 +02:00
if ( stalenessSeconds > maxCacheAgeSeconds ) {
2024-04-13 01:36:37 +02:00
return null
}
return JSON . parse ( fs . readFileSync ( path , "utf-8" ) ) . features
2024-04-05 17:49:31 +02:00
}
private static exportExtraAmenities ( allVelopark : Feature [ ] ) {
const amenities : Record < string , Feature [ ] > = { }
for ( const bikeparking of allVelopark ) {
const props = bikeparking . properties
if ( ! props [ "fixme_nearby_amenity" ] ) {
continue
}
if ( props [ "fixme_nearby_amenity" ] ? . endsWith ( "CameraSurveillance" ) ) {
delete props [ "fixme_nearby_amenity" ]
continue
}
const amenity = props [ "fixme_nearby_amenity" ] . split ( "#" ) [ 1 ]
if ( ! amenities [ amenity ] ) {
amenities [ amenity ] = [ ]
}
amenities [ amenity ] . push ( bikeparking )
}
for ( const k in amenities ) {
this . exportGeojsonTo ( "velopark_amenity_" + k + ".geojson" , amenities [ k ] )
}
}
2024-12-17 03:31:28 +01:00
private static async fetchMapRouletteClosedItems() {
const challenges = [ "https://maproulette.org/api/v2/challenge/view/43282" ]
2024-12-17 04:39:38 +01:00
const solvedRefs : Set < string > = new Set < string > ( )
2024-12-17 03:31:28 +01:00
for ( const url of challenges ) {
2024-12-17 04:39:38 +01:00
const data = await Utils . downloadJson <
FeatureCollection <
Point ,
{
mr_taskId : string
"ref:velopark" : string
mr_taskStatus : MaprouletteStatus
mr_responses : string | undefined
}
>
> ( url )
2024-12-17 03:31:28 +01:00
for ( const challenge of data . features ) {
const status = challenge . properties . mr_taskStatus
2024-12-17 04:39:38 +01:00
const isClosed =
status === "Fixed" ||
status === "False_positive" ||
status === "Already fixed" ||
status === "Too_Hard" ||
status === "Deleted"
if ( isClosed ) {
2024-12-17 03:31:28 +01:00
const ref = challenge . properties [ "ref:velopark" ]
2024-12-17 04:39:38 +01:00
solvedRefs . add ( ref )
2024-12-17 03:31:28 +01:00
}
}
}
2024-12-17 04:39:38 +01:00
console . log ( "Detected" , solvedRefs , "as closed on mapRoulette" )
2024-12-17 03:31:28 +01:00
return solvedRefs
}
/ * *
* Creates an extra version where all bicycle parkings which are already linked are removed .
* Fetches the latest OSM - data from overpass
* @param allVelopark
* @private
* /
2024-04-05 17:49:31 +02:00
private static async createDiff ( allVelopark : Feature [ ] ) {
2024-12-17 03:31:28 +01:00
console . log ( "Creating diff..." )
2024-01-17 18:08:14 +01:00
const bboxBelgium = new BBox ( [
[ 2.51357303225 , 49.5294835476 ] ,
2024-06-16 16:06:26 +02:00
[ 6.15665815596 , 51.4750237087 ] ,
2024-01-17 18:08:14 +01:00
] )
2024-04-05 17:49:31 +02:00
2024-01-17 18:08:14 +01:00
const alreadyLinkedQuery = new Overpass (
new RegexTag ( "ref:velopark" , /.+/ ) ,
2024-01-16 04:07:02 +01:00
[ ] ,
Constants . defaultOverpassUrls [ 0 ] ,
2024-01-17 18:08:14 +01:00
new ImmutableStore ( 60 * 5 ) ,
2024-12-17 04:39:38 +01:00
false
2024-01-17 18:08:14 +01:00
)
2024-04-05 17:49:31 +02:00
const alreadyLinkedFeatures = ( await alreadyLinkedQuery . queryGeoJson ( bboxBelgium ) ) [ 0 ]
2024-01-17 18:08:14 +01:00
const seenIds = new Set < string > (
2024-12-17 04:39:38 +01:00
alreadyLinkedFeatures . features . map ( ( f ) = > f . properties ? . [ "ref:velopark" ] )
2024-01-17 18:08:14 +01:00
)
2024-04-13 01:36:37 +02:00
this . exportGeojsonTo ( "osm_with_velopark_link" , < Feature [ ] > alreadyLinkedFeatures . features )
2024-01-17 18:08:14 +01:00
console . log ( "OpenStreetMap contains" , seenIds . size , "bicycle parkings with a velopark ref" )
2024-02-15 01:07:50 +01:00
2024-04-13 02:40:21 +02:00
const features : Feature [ ] = allVelopark . filter (
2024-12-17 04:39:38 +01:00
( f ) = > ! seenIds . has ( f . properties [ "ref:velopark" ] )
2024-04-13 02:40:21 +02:00
)
2024-04-05 17:49:31 +02:00
VeloParkToGeojson . exportGeojsonTo ( "velopark_nonsynced" , features )
2024-01-16 04:07:02 +01:00
2024-12-17 04:39:38 +01:00
const synced = await this . fetchMapRouletteClosedItems ( )
2024-12-17 03:31:28 +01:00
const featuresMoreFiltered = features . filter (
( f ) = > ! synced . has ( f . properties [ "ref:velopark" ] )
)
VeloParkToGeojson . exportGeojsonTo ( "velopark_nonsynced_nonclosed" , featuresMoreFiltered )
2024-12-17 04:39:38 +01:00
const featuresMoreFilteredFailed = features . filter ( ( f ) = >
synced . has ( f . properties [ "ref:velopark" ] )
)
VeloParkToGeojson . exportGeojsonTo (
"velopark_nonsynced_human_import_failed" ,
featuresMoreFilteredFailed
2024-12-17 03:31:28 +01:00
)
2024-01-16 04:07:02 +01:00
const allProperties = new Set < string > ( )
2024-12-17 03:31:28 +01:00
for ( const feature of featuresMoreFiltered ) {
Object . keys ( feature . properties ) . forEach ( ( k ) = > allProperties . add ( k ) )
2024-01-16 04:07:02 +01:00
}
allProperties . delete ( "ref:velopark" )
2024-12-17 03:31:28 +01:00
for ( const feature of featuresMoreFiltered ) {
2024-01-17 18:08:14 +01:00
allProperties . forEach ( ( k ) = > {
2024-12-17 04:39:38 +01:00
if ( k === "ref:velopark" ) {
2024-12-17 03:31:28 +01:00
return
}
delete feature . properties [ k ]
2024-01-16 04:07:02 +01:00
} )
}
2024-12-17 03:31:28 +01:00
this . exportGeojsonTo ( "velopark_nonsynced_nonclosed_id_only" , featuresMoreFiltered )
}
public static async findMultiSection ( ) : Promise < string [ ] > {
const url = "https://www.velopark.be/api/parkings/1000"
2024-12-17 04:39:38 +01:00
const raw = await Utils . downloadJson < { "@graph" : { } [ ] ; url : string } [ ] > ( url )
2024-12-17 03:31:28 +01:00
const multiEntries : string [ ] = [ ]
for ( const entry of raw ) {
2024-12-17 04:39:38 +01:00
if ( entry [ "@graph" ] . length > 1 ) {
2024-12-17 03:31:28 +01:00
multiEntries . push ( entry . url )
}
}
return multiEntries
2024-04-05 17:49:31 +02:00
}
2024-04-13 01:36:37 +02:00
async main ( ) : Promise < void > {
2024-12-17 03:31:28 +01:00
// const multiEntries = new Set(await VeloParkToGeojson.findMultiSection())
2024-04-13 02:40:21 +02:00
const allVelopark =
VeloParkToGeojson . loadFromFile ( ) ? ? ( await VeloParkToGeojson . downloadData ( ) )
2024-04-05 17:49:31 +02:00
console . log ( "Got" , allVelopark . length , " items" )
2024-04-13 01:36:37 +02:00
VeloParkToGeojson . exportExtraAmenities ( allVelopark )
2024-04-05 17:49:31 +02:00
await VeloParkToGeojson . createDiff ( allVelopark )
2024-04-13 02:40:21 +02:00
console . log (
2024-12-17 16:18:32 +01:00
"Use \nvite-node scripts/velopark/compare.ts -- velopark_all_.geojson osm_with_velopark_link_.geojson\n to compare the results and generate a diff file"
2024-04-13 02:40:21 +02:00
)
2024-01-16 04:07:02 +01:00
}
}
new VeloParkToGeojson ( ) . run ( )