web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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 }