Skip to content

Instantly share code, notes, and snippets.

@sebilasse
Created January 9, 2025 19:58
Show Gist options
  • Save sebilasse/ca76c60955e5414cff2c253f1cd89af4 to your computer and use it in GitHub Desktop.
Save sebilasse/ca76c60955e5414cff2c253f1cd89af4 to your computer and use it in GitHub Desktop.
OSM Place to ActivityPub (WIP)
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