Created
January 9, 2025 19:58
-
-
Save sebilasse/ca76c60955e5414cff2c253f1cd89af4 to your computer and use it in GitHub Desktop.
OSM Place to ActivityPub (WIP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AsLocation, AsLinkObject, AsObjectNormalized } from "@AS"; | |
import { OsmRes, NominatimRes, NominatimLookup } from '../interfaces.d.ts'; | |
import { ASUnits } from "../context/index.ts"; | |
import { getSitelinkUrl } from "../wikidata.ts"; | |
export interface LocationFeatureSpecification { | |
/* amenity */ | |
propertyID?: string; | |
/* restaurant */ | |
value?: any; /* string | boolean | number | https://schema.org/StructuredValue; */ | |
/* osm:979019920 */ | |
valueReference?: any; | |
maxValue?: number; | |
minValue?: number; | |
measurementTechnique?: string; | |
unitCode?: string; | |
unitText?: string; | |
} | |
/* | |
TODO schema | |
Accommodation: CampingPitch Room Suite | |
Residence: ApartmentComplex GatedResidenceCommunity | |
CivicStructure: -> amenity | |
*/ | |
const icons: any = { | |
amenity: { | |
restaurant: 'b/bb/Restaurant-14', | |
food_court: 'b/bb/Restaurant-14', | |
cafe: 'd/da/Cafe-16', | |
fast_food: '1/1f/Fast-food-16', | |
bar: '9/94/Bar-16', | |
pub: '5/5d/Pub-16', | |
ice_cream: '0/0f/Ice-cream-14', | |
biergarten: 'e/e1/Biergarten-16', | |
community_centre: '0/0b/Community_centre-14', | |
library: 'c/c5/Library.14', | |
theatre: 'e/eb/Theatre-16', | |
cinema: '3/31/Cinema-16', | |
nightclub: 'e/ee/Nightclub-16', | |
arts_centre: 'b/bf/Arts_centre', | |
internet_cafe: '8/89/Internet_cafe-14', | |
casino: '8/83/Casino-14', | |
public_bookcase: 'b/b2/Public_bookcase-14', | |
public_bath: '0/01/Public_bath', | |
toilets: 'f/fa/Toilets-16', | |
recycling: '1/16/Recycling-16', | |
waste_basket: '6/6f/Waste-basket-12', | |
waste_disposal: 'e/e6/Waste_disposal-14', | |
bench: '0/0c/Bench-16', | |
shelter: 'f/f8/Shelter-14', | |
drinking_water: '0/08/Drinking-water-16', | |
fountain: 'a/a1/Fountain-14', | |
bbq: '5/50/Bbq-14', | |
shower: '5/5a/Shower-14', | |
bank: '3/3b/Bank-16', | |
atm: 'f/f9/Atm-14', | |
bureau_de_change: 'e/ed/Bureau_de_change-14', | |
pharmacy: '1/1e/Pharmacy-14', | |
hospital: '3/33/Hospital-14', | |
clinic: '7/71/Doctors-14', | |
doctors: '7/71/Doctors-14', | |
dentist: '8/86/Dentist-14', | |
veterinary: 'f/fc/Veterinary-14', | |
post_box: 'd/d4/Post_box-12', | |
post_office: 'e/e1/Post_office-14', | |
telephone: 'f/fa/Telephone.16', | |
parking: '7/7b/Parking-16', | |
fuel: '7/77/Fuel-16', | |
bicycle_parking: '7/7f/Parking-bicycle-16', | |
bus_station: '5/5a/Amenity_bus_station', | |
bicycle_rental: 'd/d5/Rental-bicycle-16', | |
taxi: '9/94/Taxi.16', | |
charging_station: 'a/af/Charging_station.16', | |
car_rental: '1/11/Rental-car-16', | |
ferry_terminal: '2/24/Ferry-icon', | |
motorcycle_parking: '3/31/Parking-motorcycle-16', | |
bicycle_repair_station: '0/01/Bicycle_repair_station-14', | |
boat_rental: 'b/b1/Boat_rental-14', | |
police: '5/59/Police-16', | |
townhall: 'a/a3/Town-hall-16', | |
fire_station: 'b/b7/Fire-station-16', | |
social_facility: '0/0e/Social_facility-14', | |
courthouse: 'd/db/Courthouse-16', | |
prison: 'd/d0/Prison-16', | |
marketplace: '1/1c/Marketplace-14', | |
car_wash: '6/65/Car_wash-14', | |
vehicle_inspection: '5/56/Vehicle_inspection-14', | |
driving_school: 'c/c4/Shop-other-16', | |
nursing_home: 'a/ad/Social_amenity_darken_80-16', | |
childcare: 'a/ad/Social_amenity_darken_80-16', | |
hunting_stand: 'a/a6/Hunting-stand-16' | |
}, | |
leisure: { | |
outdoor_seating: 'a/ac/Outdoor_seating-14', | |
amusement_arcade: '4/44/Amusement_arcade-14', | |
playground: '3/31/Playground-16', | |
fitness_centre: 'b/bd/Fitness', | |
fitness_station: 'b/bd/Fitness', | |
golf_course: 'd/d2/Golf-icon', | |
sauna: '3/3a/Sauna-14', | |
miniature_golf: '4/44/Miniature_golf', | |
beach_resort: 'c/cd/Beach_resort-14', | |
fishing: 'b/ba/Fishing-14', | |
bowling_alley: '0/05/Bowling_alley-14', | |
dog_park: 'd/da/Dog_park', | |
picnic_table: '7/7d/Table-16', | |
firepit: 'd/df/Firepit', | |
bird_hide: '9/92/Bird_hide-14', | |
slipway: '8/88/Transport_slipway' | |
}, | |
tourism: { | |
artwork: '1/12/Artwork-14', | |
museum: 'a/a9/Museum-16', | |
gallery: '7/7e/Gallery-14', | |
picnic_site: 'f/fc/Picnic_site', | |
camp_site: 'e/e4/Camping.16', | |
caravan_site: 'a/a1/Caravan-16', | |
viewpoint: 'c/c2/Viewpoint-16', | |
hotel: 'c/ca/Hotel-16', | |
guest_house: 'd/dc/Tourism_guest_house', | |
hostel: '4/4f/Hostel-16', | |
chalet: 'e/e9/Chalet', | |
motel: '1/10/Motel-16', | |
apartment: '0/0d/Apartment', | |
alpine_hut: 'f/f1/Alpinehut', | |
wilderness_hut: '8/8e/Wilderness_hut' | |
}, | |
historic: { | |
memorial: '6/6e/Memorial-16', | |
archaeological_site: '7/7d/Archaeological-site-16', | |
wayside_shrine: '1/17/Carto_shrine', | |
monument: '9/94/Monument-16', | |
castle: '5/51/Castle-14', | |
fort: '0/0d/Historic-fort', | |
city_gate: '4/47/City-gate-14', | |
wayside_cross: '2/26/Christian.9' | |
}, | |
man_made: { | |
obelisk: '8/82/Obelisk-14', | |
storage_tank: '1/15/Storage_tank-14', | |
silo: '1/15/Storage_tank-14', | |
tower: '0/0d/Tower_freestanding', | |
cross: '2/26/Christian.9', | |
water_tower: '1/13/Water-tower-16', | |
mast: '4/4b/Mast_general', | |
chimney: '8/8a/Chimney-14', | |
lighthouse: 'c/c2/Lighthouse-16', | |
crane: 'e/e0/Crane-14', | |
windmill: '0/0b/Windmill-16', | |
communications_tower: '0/0c/Communication_tower-14' | |
}, | |
shop: { | |
massage: '2/29/Massage-14', | |
convenience: '9/96/Convenience-14', | |
supermarket: '7/76/Supermarket-14', | |
clothes: 'd/de/Clothes-16', | |
fashion: 'd/de/Clothes-16', | |
hairdresser: '6/6b/Hairdresser-16', | |
bakery: 'f/fe/Bakery-16', | |
car_repair: '2/26/Car_repair-14', | |
doityourself: 'c/c3/Doityourself-16', | |
hardware: 'c/c3/Doityourself-16', | |
car: 'b/b2/Purple-car', | |
kiosk: 'b/bf/Newsagent-14', | |
newsagent: 'b/bf/Newsagent-14', | |
beauty: '0/06/Beauty-14', | |
butcher: 'b/b8/Butcher', | |
alcohol: 'e/eb/Alcohol-16', | |
wine: 'e/eb/Alcohol-16', | |
furniture: 'a/a0/Furniture-16', | |
florist: '6/69/Florist-16', | |
mobile_phone: '1/19/Mobile-phone-16', | |
electronics: '2/27/Electronics-16', | |
shoes: '3/3b/Shoes-16', | |
car_parts: '7/78/Car_parts-14', | |
greengrocer: 'd/d8/Greengrocer-14', | |
farm: 'd/d8/Greengrocer-14', | |
laundry: '3/34/Laundry-14', | |
dry_cleaning: '3/34/Laundry-14', | |
optician: '6/60/Optician-16', | |
jewelry: '8/8d/Jewellery-16', | |
jewellery: '8/8d/Jewellery-16', | |
books: '1/18/Books-16', | |
gift: '1/11/Gift-16', | |
department_store: '7/79/Department_store-16', | |
bicycle: '1/1b/Bicycle-16', | |
confectionery: 'c/cc/Confectionery-14', | |
chocolate: 'c/cc/Confectionery-14', | |
pastry: 'c/cc/Confectionery-14', | |
variety_store: '2/24/Variety_store-14', | |
travel_agency: 'b/b1/Travel_agency-14', | |
sports: 'd/df/Sports-14', | |
chemist: '3/36/Chemist-14', | |
computer: 'b/bb/Computer-14', | |
stationery: '5/58/Stationery-14', | |
pet: '5/5d/Pet-16', | |
beverages: '9/98/Beverages-14', | |
cosmetics: 'e/e9/Perfumery-14', | |
perfumery: 'e/e9/Perfumery-14', | |
tyres: '5/53/Tyres', | |
motorcycle: '5/5d/Shop_motorcycle', | |
garden_centre: '4/48/Garden_centre-14', | |
copyshop: '2/2c/Copyshop-14', | |
toys: '6/62/Toys-14', | |
deli: '3/3b/Deli-14', | |
tobacco: 'b/b0/Tobacco-14', | |
seafood: 'd/d9/Seafood-14', | |
interior_decoration: 'f/f4/Interior_decoration-14', | |
ticket: '7/79/Ticket-14', | |
photo: '4/4a/Photo-14', | |
trade: 'f/f2/Trade-14', | |
wholesale: 'f/f2/Trade-14', | |
outdoor: '7/76/Outdoor-14', | |
houseware: '8/84/Houseware-14', | |
art: 'f/fb/Art-14', | |
paint: '3/31/Paint-14', | |
fabric: '4/45/Fabric-14', | |
bookmaker: '8/81/Bookmaker-14', | |
second_hand: 'a/a3/Second_hand-14', | |
charity: '8/85/Charity-14', | |
bed: '9/91/Bed-14', | |
medical_supply: 'f/fa/Medical_supply', | |
hifi: '0/0c/Hifi-14', | |
music: '1/13/Shop_music', | |
coffee: 'd/d5/Coffee-14', | |
musical_instrument: 'd/d0/Musical_instrument-14', | |
tea: '3/34/Tea-14', | |
video: '2/2d/Video-14', | |
bag: '4/41/Bag-14', | |
carpet: '5/5f/Carpet-14', | |
video_games: 'a/ab/Video_games-14', | |
dairy: '0/0e/Dairy', | |
'*': 'c/c4/Shop-other-16' | |
}, | |
golf: { pin: 'c/ca/Leisure-golf-pin' }, | |
emergency: { phone: '1/1c/Emergency-phone.16' }, | |
highway: { | |
bus_stop: '5/52/Bus-stop-12', | |
elevator: '6/6d/Elevator-12', | |
traffic_signals: '8/84/Traffic_light-16', | |
mini_roundabout: '9/9b/Highway_mini_roundabout' | |
}, | |
railway: { | |
station: '1/11/Tram-16', | |
halt: '1/11/Tram-16', | |
tram_stop: '1/11/Tram-16', | |
subway_entrance: '3/3c/Subway-entrance-12', | |
level_crossing: 'f/f7/Level_crossing', | |
crossing: 'f/f7/Level_crossing' | |
}, | |
aeroway: { helipad: 'e/ed/Helipad.16', aerodrome: 'b/bc/Aerodrome' }, | |
oneway: { yes: 'e/e6/Oneway' }, | |
barrier: { | |
gate: '9/97/Barrier_gate', | |
bollard: '8/8f/Barrier', | |
block: '8/8f/Barrier', | |
turnstile: '8/8f/Barrier', | |
log: '8/8f/Barrier', | |
lift_gate: '8/8f/Liftgate-7', | |
swing_gate: '8/8f/Liftgate-7', | |
cycle_barrier: '0/09/Cycle_barrier-14', | |
stile: '7/7c/Barrier_stile-14', | |
toll_booth: 'd/d7/Toll_booth', | |
cattle_grid: '4/4c/Barrier_cattle_grid-14', | |
kissing_gate: '2/2f/Kissing_gate-14', | |
'full-height_turnstile': 'b/bb/Full-height_turnstile-14', | |
motorcycle_barrier: 'b/b5/Motorcycle_barrier-14' | |
}, | |
ford: { yes: '5/50/Ford.16', stepping_stones: '5/50/Ford.16' }, | |
vending: { | |
'public_transport_tickets': [ | |
'2/28/Public_transport_tickets-14', | |
'A machine vending bus, tram, train... tickets' | |
], | |
'parking_tickets': [ | |
'b/be/Parking_tickets-14', 'A machine selling tickets for parking' | |
], | |
'excrement_bags': [ '0/08/Excrement_bags-14', 'Excrement bag dispenser' ] | |
}, | |
waterway: { | |
dam: 'c/cb/Dam_node', | |
weir: '0/00/Weir_node', | |
lock_gate: '9/97/Lock_gate_node', | |
waterfall: '7/72/Waterfall-14' | |
}, | |
'Node with highway': { | |
'turning_circle at way with highwaytrack': '2/2f/Turning_circle_on_highway_track-16' | |
}, | |
natural: { | |
tree: '6/65/Tree-16', | |
peak: '6/67/Peak-8', | |
spring: '0/0e/Spring-14', | |
cave_entrance: 'b/b1/Cave.14', | |
saddle: 'a/a3/Saddle-8', | |
volcano: 'e/e3/Volcano-8' | |
}, | |
military: { bunker: '3/36/Bunker-osmcarto' }, | |
advertising: { column: '2/20/Column-14' }, | |
power: { tower: 'e/e3/Power_tower', pole: 'd/d8/Power_pole' }, | |
entrance: { | |
yes: '9/92/Rect', | |
main: '0/00/Entrance_main', | |
service: '2/27/Entrance' | |
}, | |
wheelchair: { | |
yes: '5/5f/Wheelchair_sign_yes', | |
limited: '0/0c/Wheelchair_sign_limited', | |
no: ' 4/46/Wheelchair_sign_no', | |
designated: '8/8a/Wheelchair_sign_only' | |
}, | |
place: { city: '9/97/Place-6' }, | |
capital: '0/0d/Place-capital-8', | |
office: '9/92/Office-16' | |
}; | |
interface WheelchairAndAmenities { wheelchair: [string,string][]; amenity: [string,string][]; } | |
export function osmWheelchairAndAmenities( | |
locationFeatures: LocationFeatureSpecification[] | LocationFeatureSpecification, | |
hasLocalIcon = true, | |
isAdministrativeForCountry: false | string = false | |
): WheelchairAndAmenities { | |
if (typeof locationFeatures !== 'object') { return {wheelchair: [], amenity: []} } | |
const _a = (!Array.isArray(locationFeatures) ? [locationFeatures] : locationFeatures); | |
const sl = (s: string) => s.trim().toLowerCase(); | |
const o = _a.reduce((_o: any, l) => { | |
if ( | |
l.hasOwnProperty('propertyID') && l.hasOwnProperty('value') && | |
typeof l.propertyID === 'string' && typeof l.value === 'string' | |
) { | |
const k = sl(l.propertyID.replace('osm:','')); | |
const v = sl(l.value); | |
if (!_o.hasOwnProperty(k)) { _o[k] = {}; } | |
_o[k][v] = 1; | |
} | |
return _o | |
}, {}); | |
// console.log(o) | |
const a: [string, string][] = []; | |
if (isAdministrativeForCountry || o?.boundary === 'administrative') { | |
a.push(['AdministrativeArea', '0/00/AdministrativeArea']); | |
} else if (!!o.amenity) { | |
if (typeof o.amenity === 'string') { o.amenity = {[o.amenity]: o.amenity} } | |
if (!!o.amenity.parking && !!o.parking && (!!o.parking.lane || !!o.parking.street_side)) { | |
a.push(['parkingSubtle', '6/64/Parking-subtle']); | |
} | |
if (!!o.amenity.parking_entrance && !!o.parking) { | |
if (!!o.parking.underground) { | |
a.push(['parkingU', 'b/b1/Parking_entrance-14']); | |
} | |
if (!!o.parking['multi-storey']) { | |
a.push(['parkingMulti', 'b/b1/Parking_entrance-14']); | |
} | |
} | |
if (!!o.amenity.place_of_worship) { | |
const religions: [string, string][] = [ | |
['christian', '3/39/Christian-16'], ['jewish', 'c/cb/Jewish-16'], | |
['muslim', '5/5d/Muslim-16'], ['taoist', '7/7e/Taoist-16'], | |
['hindu', '2/2f/Hinduist-16'], ['buddhist', 'a/ae/Buddhist-16'], | |
['shinto', '8/8a/Shintoist-16'], ['sikh', '7/75/Sikhist-16'], | |
['religion_und', '0/04/Place-of-worship-16'] | |
]; | |
if (!!o.religion) { | |
religions.forEach((rA) => { | |
if (!!o.religion[rA[0]]) { a.push(rA); } | |
}); | |
} | |
} | |
/* | |
for (let aKey in icons.amenity) { | |
if (o.amenity.) | |
}*/ | |
} | |
if (!!o.artwork_type) { | |
if (!!o.artwork_type.statue) { a.push(['statue', '6/68/Statue-14']) } | |
if (!!o.artwork_type.bust) { a.push(['bust', '9/9f/Bust-14']) } | |
} | |
if (!!o.tourism && !!o.tourism.information && !!o.information) { | |
if (!!o.information.guidepost) { a.push(['guidepost', 'd/dc/Guidepost-14']) } | |
if (!!o.information.board) { a.push(['infoBoard', '7/77/Board-14']) } | |
if (!!o.information.map) { a.push(['infoMap', 'c/ca/Map-14']) } | |
if (!!o.information.tactile_map) { a.push(['infoMap3d', 'c/ca/Map-14']) } | |
if (!!o.information.office) { a.push(['infoOffice', '7/78/Office-14']) } | |
if (!!o.information.terminal) { a.push(['infoTerminal', '9/9c/Terminal-14']) } | |
if (!!o.information.audioguide) { a.push(['audioguide', '6/6a/Audioguide-14']) } | |
} | |
if (!!o.office && !!o.office.diplomatic && !!o.diplomatic) { | |
if (!!o.diplomatic.embassy) { a.push(['embassy', 'f/f5/Diplomatic']) } | |
if (!!o.diplomatic.consulate) { a.push(['consulate', '4/4f/Office-diplomatic-consulate']) } | |
} | |
if (!!o.historic && !!o.historic.memorial && !!o.memorial) { | |
if (!!o.memorial.plaque || !!o.memorial.blue_plaque) { a.push(['plaque', 'b/b2/Plaque']) } | |
if (!!o.memorial.statue) { a.push(['statue', '6/68/Statue-14']) } | |
if (!!o.memorial.stone) { a.push(['stone', '8/87/Stone-14']) } | |
if (!!o.memorial.bust) { a.push(['bust', '9/9f/Bust-14']) } | |
} | |
if (!!o.historic && !!o.historic.castle) { | |
if (!!o.castle_type && !!o.castle_type.palace) { a.push(['palace', '3/33/Palace-14']) } | |
if (!!o.castle_type && !!o.castle_type.stately) { a.push(['stately', '3/33/Palace-14']) } | |
if (!!o.castle_type && !!o.castle_type.manor) { a.push(['manor', '4/41/Manor-14']) } | |
if (!!o.castle_type && !o.castle_type.palace && !o.castle_type.stately) { | |
a.push([Object.keys(o.castle_type)[0]||'castle', '3/31/Fortress-14']) | |
} | |
if (!o.castle_type) { | |
a.push(['castle', '3/31/Fortress-14']) | |
} | |
} | |
if (!!o.leisure) { | |
if (!!o.leisure.swimming_area || (!!o.leisure.sports_centre && !!o.sport && !!o.sport.swimming)) { | |
a.push(['swimmingArea', 'c/cb/Swimming-16']) | |
} | |
if (!!o.leisure.water_park) { | |
a.push(['waterPark', 'c/cb/Swimming-16']) | |
} | |
} | |
if (!!o.entrance && !!o.access && !!o.access.no) { | |
a.push(['noEntrance', '0/0d/Rectdiag']) | |
} | |
const withLocalIcon = (_a: [string, string]) => { | |
if (Array.isArray(_a) && _a.length > 1 && typeof _a[1] === 'string') { | |
_a[1] = `/static/theme/svg/osm/${_a[1].split('/').reverse()[0]}.svg`; | |
} else { | |
console.log(o,_a) | |
} | |
return _a | |
} | |
for (let k in o) { | |
if (k === 'wheelchair' || k === 'hasDriveThroughService' || !o.hasOwnProperty(k)) { continue } | |
if (a.length > 1) { break } | |
if (icons.hasOwnProperty(k)) { | |
for (let v in o[k]) { | |
if (a.length > 1) { break } | |
if (icons[k].hasOwnProperty(v)) { | |
typeof icons[k][v] === 'string' && a.push([`${k}_${v}`, icons[k][v]]); | |
} | |
} | |
!a.length && typeof icons[k] === 'string' && a.push([k, icons[k]]); | |
} | |
} | |
if (!!o.hasDriveThroughService && !!o.hasDriveThroughService.yes) { | |
a.push(['hasDriveThroughService_yes','c/c4/Car-14']) | |
} | |
let w: [string, string][] = []; | |
if (!!o.wheelchair) { | |
for (let wk in o.wheelchair) { | |
o.wheelchair.hasOwnProperty(wk) && w.push([`wheelchair_${wk}`, icons.wheelchair[wk]]); | |
} | |
} else { | |
w.push(['wheelchair_und', '9/93/Wheelchair_sign_unknown']); | |
} | |
return {wheelchair: (hasLocalIcon ? w.map(withLocalIcon) : w), amenity: (hasLocalIcon ? a.map(withLocalIcon) : a)} | |
} | |
/* TODO multi-level keys ... | |
'man_made=tower + tower:type=communication': [ '2/27/Tower_cantilever_communication', 'Communication towers' ], | |
'power=generator + generator:source=wind ( + generator:method=wind_turbine)': [ 'b/b2/Generator_wind-14', 'Wind turbine' ], | |
'man_made=tower + tower:type=observation / man_made=tower + tower:type=watchtower': [ 'b/b9/Tower_observation', 'Observation tower / Watch tower' ], | |
'man_made=tower + tower:type=bell_tower': [ '1/1a/Tower_bell_tower', 'Bell tower' ], | |
'man_made=tower + tower:type=lighting': [ '3/3d/Tower_lighting', 'Towers for lighting' ], | |
'man_made=tower + tower:type=communication + tower:construction=lattice': [ | |
'9/9d/Tower_lattice_communication', | |
'Lattice communication towers' | |
], | |
'man_made=mast + tower:type=lighting': [ 'e/e9/Mast_lighting', 'Poles for lighting' ], | |
'man_made=mast + tower:type=communication': [ '2/25/Mast_communications', 'Mast with transmitters' ], | |
'man_made=tower + tower:type=defensive': [ '0/0f/Tower_defensive', 'Fortified defensive tower' ], | |
'man_made=tower + tower:type=cooling': [ 'b/be/Tower_cooling', 'Cooling tower' ], | |
'man_made=tower + tower:construction=lattice': [ | |
'e/e9/Tower_lattice', | |
'The tower is constructed from steel lattice' | |
], | |
'man_made=tower + tower:type=lighting + tower:construction=lattice': [ | |
'c/c4/Tower_lattice_lighting', | |
'Tower is constructed from steel lattice for lighting' | |
], | |
'man_made=tower + tower:construction=dish': [ 'c/c3/Tower_dish', "The 'communication tower' is a dish" ], | |
'man_made=tower + tower:construction=dome': [ 'c/c0/Tower_dome', "The 'communication tower' is a dome" ], | |
'man_made=telescope + telescope:type=radio': [ '5/59/Telescope_dish-14', 'Radio telescope' ], | |
'man_made=telescope + telescope:type=optical': [ 'e/e0/Telescope_dome-14', 'Optical telescope' ], | |
*/ | |
/* TODO check existing for _osmToWd | |
TYPE | |
// done | |
[ | |
"building", "amenity", "water", "highway", "railway", "waterway", "cycleway", "aerialway", "aeroway", "geological" | |
// Structure | |
"craft", "leisure", "shop", "tourism", "sport", "healthcare", "military", "memorial", "public_transport", | |
"diplomatic", "government", "industrial", "emergency" | |
] | |
// TODO : place, landuse, natural, man_made | |
// TODO : historic | |
// healthcare:speciality, club, cuisine, museum, vending, pedagogy, playground, fountain, | |
// surveillance, wheelchair, ramp:wheelchair, dog, smoking, opening_hours, fee | |
TODO split semicolon and combis | |
*/ | |
function isOSM(x: OsmRes | NominatimRes): x is OsmRes { | |
return !!(x?.elements && !(x as any)?.osm_type); | |
} | |
export function osmToAs(o: OsmRes | NominatimRes, osmId: string = '', isAdministrativeForCountry: false | string = false) { | |
const [domain, baseUrl] = ['openstreetmap.org', 'https://www.openstreetmap.org']; | |
const features: LocationFeatureSpecification[] = []; | |
// https://www.openstreetmap.org/way/30475956.json | |
// https://api.openstreetmap.org/api/0.6/way/30475956/full.json | |
// https://nominatim.openstreetmap.org/lookup?format=json&extratags=1&namedetails=1&osm_ids=W30475956 | |
let [id, ref] = [osmId, osmId]; | |
if (!id || typeof id !== 'string') { | |
if (isOSM(o)) { | |
const elements = o.elements.reverse(); | |
let el: any; | |
for (el of elements) { | |
if (el.type && el.id) { | |
id = `${el.type}/${el.id}`; | |
break; | |
} | |
} | |
} else { | |
id = `${o.osm_type}/${o.osm_id}`; | |
} | |
} | |
if (!id || typeof id !== 'string') { return { type: ["Place"] } } | |
const lId = id.toLowerCase(); | |
// console.log('b',lId); | |
if (lId.indexOf(domain) < 0 && (lId.startsWith('way')||lId.startsWith('node')||lId.startsWith('relation'))) { | |
id = `${baseUrl}/${lId}`; | |
} | |
if (ref && ref.indexOf(`api.${domain}`) < 0) { | |
ref = id.replace(`www.${domain}`, `api.${domain}/api/0.6`); | |
} | |
const type = ['Place', 'redaktor:Factual','redaktor:Topic']; | |
const asRes: AsObjectNormalized = { type, id, name: [] }; | |
const q = isOSM(o) | |
? o.elements.filter((el) => {return `${el.id}` === osmId.split('/')[1]})[0] | |
: o; | |
let tags: any = {}; | |
if (isOSM(o) && q?.tags) { | |
tags = q.tags; | |
} else { | |
const nq = (q as NominatimRes); | |
if (nq?.extratags) { | |
tags = nq.extratags; | |
if (nq.class && nq.type) tags[nq.class] = nq.type; | |
if (nq.category && nq.type) tags[(nq as any /* OSM JSONv2 */).category] = nq.type; | |
if (nq.name) tags.name = nq.name; | |
} | |
} | |
// console.log( findType({type, ...tags}) ) | |
let [geoQ, accuracy] = [((typeof q.lat === 'number' && typeof q.lon === 'number') || tags?.ele) && q, 100]; | |
// console.log(q, geoQ) | |
if (!geoQ) { | |
if (isOSM(o)) { | |
const geos = o.elements.filter((el) => (el.lat && typeof el.lat === 'string' && el.lon && typeof el.lon === 'string')); | |
geoQ = geos.length && geos[0]; | |
accuracy = 90; | |
} else { | |
geoQ = o; | |
} | |
} | |
const asLocation: AsLocation = {accuracy}; | |
if (geoQ && geoQ.lat && geoQ.lon) { | |
asLocation.latitude = geoQ.lat; | |
asLocation.longitude = geoQ.lon; | |
if (geoQ?.tags?.ele || geoQ?.ele || geoQ['ele:wgs84']) { | |
asLocation.altitude = geoQ?.tags?.ele | |
? parseFloat(geoQ.tags.ele) | |
: (geoQ?.ele ? parseFloat(geoQ.ele) : geoQ['ele:wgs84']); | |
} | |
} else if (isOSM(o)) { | |
const [lat, lon] = o.elements.map((o) => o.lat && o.lon ? [o.lat, o.lon] : 0).filter((a) => !!a) | |
.reduce((r: number[][], pair) => { | |
r[0].push(pair[0]); | |
r[1].push(pair[1]); | |
return r | |
}, [[], []]) | |
.map((a) => a.reduce( ( p, c ) => p + c, 0 ) / a.length); | |
asLocation.latitude = lat; | |
asLocation.longitude = lon; | |
} | |
asRes.location = asLocation; | |
const hasTag = (k: string) => | |
tags[k] && typeof tags[k] === 'string' && tags[k] !== 'no' | |
&& tags[k] !== 'proposed' && tags[k] !== 'maybe'; | |
const getLink = (href: string, name?: string, hreflang?: string,) => { | |
const l: AsLinkObject = { type: "Link", href, mediaType: "text/html" }; | |
if (typeof hreflang === 'string' && hreflang) l.hreflang = hreflang; | |
if (typeof name === 'string' && name) l.name = name; | |
return l; | |
} | |
if (isOSM(o)) { | |
let [addrQ, addrAccuracy] = [tags && Object.keys(tags||{}).filter((k) => k.startsWith('addr:')).length ? tags : null, 99]; | |
if (!addrQ) { | |
const addrs = o.elements.filter((el) => Object.keys(el.tags||{}).filter((k) => k.startsWith('addr:')).length); | |
if (addrs.length) { | |
addrQ = addrs[0].tags && Object.keys(addrs[0].tags||{}).filter((k) => k.startsWith('addr:')) ? addrs[0].tags : null; | |
addrAccuracy = 90; | |
} | |
} | |
if (addrQ) { | |
asRes.address = [{accuracy: addrAccuracy}]; | |
for (let k in (addrQ as any)) { | |
if (k.startsWith('addr:')) { | |
asRes.address[0][k.replace('addr:','')] = addrQ[k]; | |
} | |
} | |
} | |
const contactQ = Object.keys(tags||{}).filter((k) => k.startsWith('contact:')) ? tags : null; | |
if (contactQ) { | |
if (!asRes.address || !asRes.address.length) { asRes.address = [{accuracy: addrAccuracy}]; } | |
for (let k in contactQ) { | |
if (k.startsWith('contact:')) { | |
if (k === 'website') { | |
if (!asRes.url) { asRes.url = [] } | |
asRes.url.push({...getLink(contactQ[k], 'website'), rel: 'me'}); | |
} else if (k === 'email') { | |
if (!asRes.url) { asRes.url = [] } | |
asRes.url.push({...getLink(`mailto:${contactQ[k]}`, 'email'), rel: 'me'}); | |
asRes.address[0][k.replace('contact:','')] = contactQ[k]; | |
} else { | |
asRes.address[0][k.replace('contact:','')] = contactQ[k]; | |
} | |
} | |
} | |
} | |
} else if (o?.address) { | |
asRes.address = [{accuracy: 100, ...o.address}]; | |
} | |
if (hasTag('website') && typeof tags.website === 'string') { | |
if (!asRes.url) { asRes.url = []; } | |
asRes.url.push({...getLink(tags.website, 'website'), rel: 'me'}) | |
} | |
if (hasTag('opendata_portal') && typeof tags.opendata_portal === 'string') { | |
if (!asRes.url) { asRes.url = []; } | |
asRes.url.push({...getLink(tags.opendata_portal, 'Open Data Portal'), rel: 'me'}) | |
} | |
if (hasTag('wikidata') && tags.wikidata.startsWith('Q')) { | |
asRes.wd = tags.wikidata; | |
asRes.describes = [ `wd:${tags.wikidata}` ]; | |
} | |
// operator:wikidata=* network:wikidata=* brand:wikidata=* | |
if (hasTag('wikipedia') && tags.wikipedia.indexOf(':') > 0) { | |
try { | |
const [lang, title] = tags.wikipedia.split(':'); | |
const href = getSitelinkUrl(lang, title); | |
if (href) { | |
if (!asRes.url) { asRes.url = []; } | |
asRes.url.push(getLink(href, title, lang)); | |
} | |
} catch (e) { | |
// console.log(e); | |
} | |
} | |
if (hasTag('official_name')) { | |
if (!asRes.name) { asRes.name = []; } | |
asRes.name = (tags.name.split(',')||[] as string[]) | |
} | |
if (hasTag('name')) { | |
if (!asRes.name) { asRes.name = []; } | |
asRes.name = asRes.name.concat(tags.name.split(',')||[] as string[]) | |
} | |
if ( (q as NominatimRes)?.display_name) { | |
if (!asRes.name) { asRes.name = []; } | |
asRes.name.push(q['display_name']) | |
} | |
asRes.name = Array.from(new Set(asRes.name)); | |
for (let k in tags) { | |
const f: LocationFeatureSpecification = {}; | |
if (k === 'name') { continue } | |
if (k === 'loc_name' && hasTag('loc_name')) { | |
if (!asRes.tag || !Array.isArray(asRes.tag) || !asRes.tag.filter((t) => t.name === tags.loc_name).length) { | |
if (!asRes.tag) { asRes.tag = [] } | |
if (!Array.isArray(asRes.tag)) { asRes.tag = [asRes.tag] } | |
// TODO : {href: '/tagged/cats'} | |
asRes.tag.push({type: ['Hashtag', 'wdt:P1449'], name: tags.loc_name}); | |
} | |
} else if (k === 'old_name' && hasTag('old_name')) { | |
// TODO | |
} else if (k.startsWith('name:')) { | |
if (!asRes.nameMap) { asRes.nameMap = ({} as any); } | |
(asRes.nameMap as any)[k.replace('name:','') as any] = tags[k]; | |
} else if (k.startsWith('nickname:') || k.startsWith('loc_name:')) { | |
if (!asRes.alternativeNameMap) { asRes.alternativeNameMap = {}; } | |
asRes.alternativeNameMap[k.replace('nickname:','').replace('loc_name:','') as any] = tags[k]; | |
} else if (k === 'nickname' || k === 'loc_name') { | |
if (!asRes.alternativeNameMap) { asRes.alternativeNameMap = {}; } | |
asRes.alternativeNameMap.und = tags[k]; | |
} else if (k.startsWith('admin_title:')) { | |
if (!asRes.summaryMap) { asRes.summaryMap = [{}]; } | |
asRes.summaryMap[0][k.replace('admin_title:','') as any] = tags[k]; | |
} else if (k === 'admin_title') { | |
if (!asRes.summaryMap) { asRes.summaryMap = [{}]; } | |
asRes.summaryMap[0].und = tags[k]; | |
} else if (k === 'height') { | |
// "height":"110 m", | |
const fH = parseFloat(k.replace(' m','')); | |
const spaced = k.trim().split(' '); | |
if (fH && !isNaN(fH)) { | |
asRes.height = fH; | |
} else if (spaced.length === 2) { | |
const [fValue, fUnit] = spaced; | |
const [height, units] = [parseFloat(fValue), fUnit.trim().toLowerCase()]; | |
if (height && !isNaN(height) && units in ASUnits) { | |
asRes.height = height; | |
asRes.units = units; | |
} | |
} | |
} else { | |
f.propertyID = k; f.value = tags[k]; f.valueReference = ref; | |
features.push(f); | |
} | |
} | |
/** TODO | |
* nominatim: | |
* place_rank 30 (JSONv2), importance 0.00000999999999995449 | |
*/ | |
/** | |
* The value provides detail for the key-specified feature. | |
* Commonly, values are free form text (e.g., name="Jeff Memorial Highway"), | |
* one of a set of distinct values (an enumeration; e.g., highway=motorway), | |
* multiple values from an enumeration (separated by a semicolon), | |
* or a number (integer or decimal), such as a distance. | |
* The value is obligatory for the tag, even if the key is self-explanatory (e.g. motorcycle:rental=yes). | |
*/ | |
// TODO type | |
// only main for | |
// tags[k] === 'yes' || tags[k] === '*' || tags[k] === 'other' || tags[k] === 'mixed_use' | |
// TODO + wikidata + amenity = ldType | |
const feature = osmWheelchairAndAmenities(features, true, isAdministrativeForCountry); | |
let isAdministrativeArea = !!isAdministrativeForCountry; | |
if (feature.amenity.length) { | |
if (!asRes.icon) { asRes.icon = []; } | |
feature.amenity.forEach((a) => { | |
if (a[0] === 'AdministrativeArea') { | |
asRes.type.push('schema:AdministrativeArea'); | |
isAdministrativeArea = true; | |
} | |
(asRes.icon as any).push({ | |
type: 'Image', | |
name: a[0], | |
url: { | |
type: 'Link', | |
mediaType: 'image/svg+xml', | |
width: 64, | |
height: 64, | |
href: a[1] | |
} | |
}) | |
}) | |
} | |
if (!isAdministrativeArea) { | |
// TODO feature.wheelchair | |
} else { | |
if (hasTag('admin_level')) { | |
const l = parseInt(tags.admin_level, 10) || 0; | |
asRes.type.push(`osm:admin_level#${l}`); | |
if (tags.admin_level === 2) { | |
asRes.type.push('schema:Country'); | |
} else if (tags.admin_level === 4) { | |
asRes.type.push('redaktor:ADM1'); | |
asRes.type.push('schema:State'); | |
} else if (tags.admin_level === 5) { | |
asRes.type.push('redaktor:ADM2'); | |
} else if (tags.admin_level === 6) { | |
asRes.type.push('redaktor:ADM3'); | |
} | |
} else if (hasTag('boundary') && tags.boundary === 'administrative') { | |
asRes.type.push('schema:AdministrativeArea'); | |
} | |
} | |
for (const cat in feature) { | |
if (cat === 'wheelchair') { continue } | |
// asRes.type.push(`osm:${cat}`); | |
for (const a of feature[cat]) { | |
if (a[0] === 'AdministrativeArea') { continue } | |
if (a[0]) { asRes.type.push(`osm:${a[0]}`); } | |
} | |
} | |
if (hasTag('short_name') && typeof tags.short_name === 'string') { | |
asRes.tag = [{ | |
type: ['Hashtag'], | |
name: (tags.short_name.toString().toLowerCase() | |
.replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/^-+/, '').replace(/-+$/, '')) | |
}]; | |
} | |
if (hasTag('license_plate_code') && typeof tags.license_plate_code === 'string') { | |
asRes.licencePlate = tags.license_plate_code; | |
} | |
const typesMap: {[p: string]: string[]} = { | |
country: ['wd:Q6256','schema:Country'], state: ['wd:Q107390','schema:State', 'redaktor:ADM1'], region: ['wd:Q82794'], province: ['wd:Q34876'], | |
district: ['wd:Q149621'], county: ['wd:Q28575'], municipality: ['wd:Q15284', 'schema:City', 'redaktor:ADM4'], | |
city: ['wd:Q515','schema:City'], borough: ['wd:Q5195043', 'redaktor:ADM5'], suburb: ['wd:Q188509', 'redaktor:ADM5'], quarter: ['wd:Q2983893'], | |
neighbourhood: ['wd:Q123705','schema:SchoolDistrict'], city_block: ['wd:Q1348006'], plot: ['wd:Q683595'], town: ['wd:Q3957'], | |
village: ['wd:Q532'], hamlet: ['wd:Q5084'], isolated_dwelling: ['wd:Q699405'], farm: ['wd:Q131596'], allotments: ['wd:Q4404694'], | |
continent: ['wd:Q5107'], archipelago: ['wd:Q33837'], island: ['wd:Q23442'], islet: ['wd:Q207524'], square: ['wd:Q174782'], | |
locality: ['wd:Q3257686'], polder: ['wd:Q106259'], sea: ['wd:Q165'], ocean: ['wd:Q9430'] | |
} | |
if (hasTag('place') && typeof tags.place === 'string') { | |
tags.place.split(',').forEach((p) => { | |
if (typesMap[p]) { | |
asRes.type = asRes.type.concat(typesMap[p]); | |
} | |
}); | |
} else if (hasTag('border_type') && typeof tags.border_type === 'string') { | |
tags.border_type.split(',').forEach((p) => { | |
if (typesMap[p]) { | |
asRes.type = asRes.type.concat(typesMap[p]); | |
} | |
}); | |
} | |
['abandoned', 'destroyed', 'demolished', 'disused', 'removed'].forEach((h) => { | |
if (hasTag(h)) { | |
asRes.type.push('redaktor:Historic'); | |
asRes.type.push(`osm:${h}`); | |
} | |
}); | |
asRes.type = Array.from(new Set(asRes.type)); | |
if (hasTag('note') && typeof tags.note === 'string') { | |
asRes.content = tags.note; | |
} | |
if (hasTag('population')) { | |
const p = typeof tags.population === 'number' | |
? tags.population | |
: parseInt(tags.population, 10); | |
if (p) { | |
// TODO | |
//"population": "71", "population:date": "1987-05-25" source:population | |
asRes.population = p; | |
} | |
} | |
for (const k in tags) { | |
if (hasTag(k) && k.startsWith('ref:')) { | |
asRes[`osm:${k}`] = tags[k]; | |
} | |
if (hasTag(k) && (k.startsWith('ISO3166') || k.startsWith('TMC'))) { | |
asRes[k.replace('-','_')] = tags[k]; | |
} | |
if (!isAdministrativeForCountry || typeof isAdministrativeForCountry !== 'string' || k.indexOf(':') < 1) { continue } | |
const [country, _k] = k.split(':'); | |
if (isAdministrativeForCountry && country === isAdministrativeForCountry.toLowerCase()) { | |
asRes[`osm:${k}`] = tags[k]; | |
} | |
} | |
return asRes | |
} | |
function area(coordinates: number[][]){ | |
let s = 0.0; | |
const ring = coordinates.length && coordinates[0].length && typeof coordinates[0][0] === 'number' ? coordinates : coordinates[0]; | |
if (!Array.isArray(ring) || !ring) { return s; } | |
for(let i = 0; i < (ring.length-1); i++){ | |
s += (ring[i][0] * ring[i+1][1] - ring[i+1][0] * ring[i][1]); | |
} | |
return 0.5 * s; | |
} | |
function centroid(coordinates: number[][]) { | |
let c = [0,0]; | |
const ring = coordinates.length && coordinates[0].length && typeof coordinates[0][0] === 'number' ? coordinates : coordinates[0]; | |
for(let i = 0; i < (ring.length-1); i++){ | |
c[0] += (ring[i][0] + ring[i+1][0]) * (ring[i][0]*ring[i+1][1] - ring[i+1][0]*ring[i][1]); | |
c[1] += (ring[i][1] + ring[i+1][1]) * (ring[i][0]*ring[i+1][1] - ring[i+1][0]*ring[i][1]); | |
} | |
const a = area(coordinates); | |
c[0] /= a * 6; | |
c[1] /= a * 6; | |
return c; | |
} | |
export function center(geojson) { | |
let isMulti = false; | |
let coordinates = [0,0]; | |
if (geojson?.type === 'Feature' && geojson?.geometry) { | |
if (geojson.geometry?.type === 'MultiPolygon') { isMulti = true; } | |
coordinates = geojson.geometry?.coordinates; | |
} else if (geojson?.coordinates) { | |
coordinates = geojson.coordinates; | |
} else if (Array.isArray(geojson) && geojson.length && Array.isArray(geojson[0])) { | |
coordinates = geojson; | |
} else { | |
return coordinates; | |
} | |
const coord: any = coordinates; | |
return !isMulti ? centroid(coord) : coord.map(centroid) | |
.reduce((r: any, pair) => { | |
r[0].push(pair[0]); | |
r[1].push(pair[1]); | |
return r | |
}, [[], []]) | |
.map((a) => a.reduce( ( p, c ) => p + c, 0 ) / a.length); | |
} | |
/* | |
"tags": { | |
// currency; currency:EUR=yes / currency:USD=no / currency:JPY=yes+currency:others=no | |
// language; language:de=main / language:en=yes | |
// postal code for ways; incomplete boundary=postal_code | |
... | |
admin_title independent town | |
admin_title:de Kreisfreie Stadt | |
... | |
"geographical_region": "Berliner Urstromtal", | |
"coat_of_arms": "File:Wappen der Hamburgischen Bürgerschaft.svg", | |
"flag": "File:Flag of Hamburg.svg", | |
"logo": "File:Hamburg-logo.svg", | |
"source": "http://wiki.openstreetmap.org/wiki/Import/Catalogue/Kreisgrenzen_Deutschland_2005", | |
"official_status": "Land", | |
"official_status:ar": "ولاية", | |
"official_status:de": "Land", | |
"official_status:en": "State", | |
"official_status:ru": "земля" | |
} | |
Key:tourism:level | |
highway motorway aerodrome aerialway railway etc. | |
*/ | |
/* | |
"loc_name":"Elphi", | |
"name":"Elbphilharmonie", | |
"name:en":"Elbe Philharmonic Hall", | |
"name:nl":"Elbphilharmonie", | |
"old_name":"Kaispeicher A", */ | |
/* | |
"building":"public", | |
"building:levels":"26", | |
"amenity":"theatre", | |
"theatre:genre":"philharmonic", | |
"theatre:type":"concert_hall", | |
"tourism":"attraction", | |
"wheelchair":"limited", | |
"toilets:wheelchair":"yes", | |
"start_date":"2016-10-31", | |
"wikidata":"Q673223", | |
"wikimedia_commons":"Category:Elbphilharmonie", | |
"wikipedia":"de:Elbphilharmonie" | |
"architect":"Werner Kallmorgen;Herzog & de Meuron", | |
"construction_year":"2003-2016", | |
"image":"https://photos.app.goo.gl/rWdeHuVtpviFyEgh6", | |
"operator":"Stadt Hamburg", | |
*/ | |
/* { | |
wheelchair: [ | |
[ | |
"wheelchair_yes", | |
"/static/theme/svg/osm/Wheelchair_sign_yes.svg" | |
] | |
], | |
amenities: [ [ "amenity_theatre", "/static/theme/svg/osm/Theatre-16.svg" ] ] | |
} */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment