geocoder.ts (6469B)
1 import type { GeolocationPoint } from "@radroots/geo"; 2 import { asset_cache_fetch, err_msg, resolve_wasm_path } from "@radroots/utils"; 3 import type { Database } from "sql.js"; 4 import type { GeocoderConfig, GeocoderConnectConfig, GeocoderReverseResult, IGeocoder, IGeocoderConnectResolve, IGeocoderCountryCenter, IGeocoderCountryCenterResolve, IGeocoderCountryListResolve, IGeocoderCountryListResult, IGeocoderCountryResolve, IGeocoderReverseOpts, IGeocoderReverseResolve } from "./types.js"; 5 import { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result, resolve_geocoder_database_path } from "./utils.js"; 6 7 const KM_PER_DEGREE_LATITUDE = 111; 8 const DEFAULT_SQL_WASM_PATH = `https://assets.radroots.org/wasm/sql/sql-wasm-1.13.0.wasm`; 9 10 const normalize_geocoder_connect_config = (config?: GeocoderConnectConfig | string): GeocoderConnectConfig => { 11 if (!config) return {}; 12 if (typeof config === `string`) return { wasm_path: config }; 13 return config; 14 }; 15 16 export class Geocoder implements IGeocoder { 17 private _db: Database | null = null; 18 private _database_path: string; 19 20 constructor(config?: GeocoderConfig | string) { 21 const database_path = typeof config === `string` ? config : config?.database_path; 22 this._database_path = resolve_geocoder_database_path(database_path); 23 } 24 25 public async connect(config?: GeocoderConnectConfig | string): Promise<IGeocoderConnectResolve> { 26 try { 27 const connect_config = normalize_geocoder_connect_config(config); 28 const database_path = connect_config.database_path 29 ? resolve_geocoder_database_path(connect_config.database_path) 30 : this._database_path; 31 const init_sqljs = await import(`sql.js`); 32 const sql = await init_sqljs.default({ 33 locateFile: wasm_file => resolve_wasm_path(connect_config.wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH) 34 }); 35 const database_res = await asset_cache_fetch(database_path, { request_init: { cache: "force-cache" } }); 36 if (!database_res.ok) return err_msg(`*`); 37 const database_buffer = await database_res.arrayBuffer(); 38 this._db = new sql.Database(new Uint8Array(database_buffer)); 39 return true; 40 } catch (e) { 41 console.log(`Error: Geocoder connect `, e); 42 return err_msg(`*`); 43 }; 44 } 45 46 public async reverse(point: GeolocationPoint, opts?: IGeocoderReverseOpts): Promise<IGeocoderReverseResolve> { 47 try { 48 if (!this._db) return err_msg(`*-db`); 49 const limit = typeof opts?.limit === `boolean` ? `` : opts?.limit ? Math.round(opts.limit) : `1`; 50 const deg_offset = opts?.degree_offset || 0.5; 51 const query = `SELECT * FROM geonames WHERE id IN (SELECT feature_id FROM coordinates WHERE latitude BETWEEN $lat - ${deg_offset} AND $lat + ${deg_offset} AND longitude BETWEEN $lng - ${deg_offset} AND $lng + ${deg_offset} ORDER BY (($lat - latitude) * ($lat - latitude) + ($lng - longitude) * ($lng - longitude) * $scale) ASC${limit ? ` LIMIT ${limit}` : ``});` 52 const stmt = this._db.prepare(query); 53 if (!stmt) return err_msg(`*-statement`); 54 const { lat: pt_lat, lng: pt_lng } = point; 55 const lat_scale = KM_PER_DEGREE_LATITUDE; 56 const lng_scale = KM_PER_DEGREE_LATITUDE * Math.cos(pt_lat * (Math.PI / 180)); 57 const scale = (lat_scale + lng_scale) / 2; 58 stmt.bind({ $lat: pt_lat, $lng: pt_lng, $scale: scale }); 59 const results: GeocoderReverseResult[] = []; 60 while (stmt.step()) { 61 const result = parse_geocode_reverse_result(stmt.getAsObject()); 62 if (result) results.push(result); 63 }; 64 return { results }; 65 } catch (e) { 66 console.log(`Error: Geocoder reverse `, e); 67 return err_msg(`*`); 68 }; 69 } 70 71 public async country(opts: IGeocoderCountryCenter): Promise<IGeocoderCountryResolve> { 72 try { 73 if (!this._db) return err_msg(`*-db`); 74 const query = `SELECT * FROM geonames WHERE country_id = $id;` 75 const stmt = this._db.prepare(query); 76 if (!stmt) return err_msg(`*-statement`); 77 const { country_id } = opts; 78 stmt.bind({ $id: country_id }); 79 const results: GeocoderReverseResult[] = []; 80 while (stmt.step()) { 81 const result = parse_geocode_reverse_result(stmt.getAsObject()); 82 if (result) results.push(result); 83 }; 84 return { results }; 85 } catch (e) { 86 console.log(`Error: Geocoder reverse `, e); 87 return err_msg(`*`); 88 }; 89 } 90 91 public async country_list(): Promise<IGeocoderCountryListResolve> { 92 try { 93 if (!this._db) return err_msg(`*-db`); 94 const query = `SELECT country_id, country_name, AVG(latitude) AS latitude_c, AVG(longitude) AS longitude_c FROM geonames GROUP BY country_id;` 95 const stmt = this._db.prepare(query); 96 if (!stmt) return err_msg(`*-statement`); 97 const results: IGeocoderCountryListResult[] = []; 98 while (stmt.step()) { 99 const result = parse_geocode_country_list_result(stmt.getAsObject()); 100 if (result) results.push(result); 101 }; 102 return { results }; 103 } catch (e) { 104 console.log(`Error: Geocoder reverse `, e); 105 return err_msg(`*`); 106 }; 107 } 108 109 110 public async country_center(opts: IGeocoderCountryCenter): Promise<IGeocoderCountryCenterResolve> { 111 try { 112 if (!this._db) return err_msg(`*-db`); 113 const query = `SELECT AVG(latitude) AS latitude_c, AVG(longitude) AS longitude_c FROM geonames WHERE country_id = $id;`; 114 const stmt = this._db.prepare(query); 115 if (!stmt) return err_msg(`*-statement`); 116 const { country_id } = opts; 117 stmt.bind({ $id: country_id }); 118 while (stmt.step()) { 119 const result = parse_geocode_country_center_result(stmt.getAsObject()); 120 if (result) return { result }; 121 }; 122 return err_msg(`*-result`); 123 } catch (e) { 124 console.log(`Error: Geocoder reverse `, e); 125 return err_msg(`*`); 126 }; 127 } 128 }