nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

coordinates.js (15451B)


      1 import L from 'leaflet';
      2 
      3 const reInteger = '\\d+';
      4 const reFractional = '\\d+(?:\\.\\d+)?';
      5 const reSignedFractional = '-?\\d+(?:\\.\\d+)?';
      6 const reHemisphere = '[NWSE]';
      7 
      8 class Coordinates {
      9     getLatitudeLetter() {
     10         return this.latIsSouth ? 'S' : 'N';
     11     }
     12 
     13     getLongitudeLetter() {
     14         return this.lonIsWest ? 'W' : 'E';
     15     }
     16 
     17     static parseHemispheres(h1, h2, h3, allowEmpty = false) {
     18         function isLat(h) {
     19             return h === 'N' || h === 'S';
     20         }
     21         let swapLatLon = false;
     22         let hLat, hLon;
     23         if (h1 && h2 && !h3) {
     24             hLat = h1.trim();
     25             hLon = h2.trim();
     26         } else if (h1 && !h2 && h3) {
     27             hLat = h1.trim();
     28             hLon = h3.trim();
     29         } else if (!h1 && h2 && h3) {
     30             hLat = h2.trim();
     31             hLon = h3.trim();
     32         } else if (allowEmpty && !h1 && !h2 && !h3) {
     33             return {empty: true};
     34         } else {
     35             return {error: true};
     36         }
     37         if (isLat(hLat) === isLat(hLon)) {
     38             return {error: true};
     39         }
     40         if (isLat(hLon)) {
     41             [hLat, hLon] = [hLon, hLat];
     42             swapLatLon = true;
     43         }
     44         const latIsSouth = hLat === 'S';
     45         const lonIsWest = hLon === 'W';
     46         return {swapLatLon, latIsSouth, lonIsWest};
     47     }
     48 }
     49 
     50 class CoordinatesDMS extends Coordinates {
     51     static regexp = new RegExp(
     52         // eslint-disable-next-line max-len
     53         `^(${reHemisphere} )?(${reInteger}) (${reInteger}) (${reFractional}) (${reHemisphere} )?(${reInteger}) (${reInteger}) (${reFractional})( ${reHemisphere})?$`,
     54         'u'
     55     );
     56 
     57     constructor(latDeg, latMin, latSec, latIsSouth, lonDeg, lonMin, lonSec, lonIsWest) {
     58         super();
     59         Object.assign(this, {latDeg, latMin, latSec, latIsSouth, lonDeg, lonMin, lonSec, lonIsWest});
     60     }
     61 
     62     equalTo(other) {
     63         return (
     64             this.latDeg === other.latDeg &&
     65             this.latMin === other.latMin &&
     66             this.latSec === other.latSec &&
     67             this.latIsSouth === other.latIsSouth &&
     68             this.lonDeg === other.lonDeg &&
     69             this.lonMin === other.lonMin &&
     70             this.lonSec === other.lonSec &&
     71             this.lonIsWest === other.lonIsWest
     72         );
     73     }
     74 
     75     format() {
     76         return {
     77             latitude: `${this.getLatitudeLetter()} ${this.latDeg}°${this.latMin}′${this.latSec}″`,
     78             longitude: `${this.getLongitudeLetter()} ${this.lonDeg}°${this.lonMin}′${this.lonSec}″`,
     79         };
     80     }
     81 
     82     isValid() {
     83         return (
     84             this.latDeg >= 0 &&
     85             this.latDeg <= 90 &&
     86             this.latMin >= 0 &&
     87             this.latMin <= 59 &&
     88             this.latSec >= 0 &&
     89             this.latSec < 60 &&
     90             this.lonDeg >= 0 &&
     91             this.lonDeg <= 180 &&
     92             this.lonMin >= 0 &&
     93             this.lonMin <= 59 &&
     94             this.lonSec >= 0 &&
     95             this.lonSec < 60 &&
     96             (this.latDeg <= 89 || (this.latMin === 0 && this.latSec === 0)) &&
     97             (this.lonDeg <= 179 || (this.lonMin === 0 && this.lonSec === 0))
     98         );
     99     }
    100 
    101     getLatLng() {
    102         let lat = this.latDeg + this.latMin / 60 + this.latSec / 3600;
    103         if (this.latIsSouth) {
    104             lat = -lat;
    105         }
    106         let lon = this.lonDeg + this.lonMin / 60 + this.lonSec / 3600;
    107         if (this.lonIsWest) {
    108             lon = -lon;
    109         }
    110         return L.latLng(lat, lon);
    111     }
    112 
    113     static fromString(s) {
    114         const m = s.match(CoordinatesDMS.regexp);
    115         if (!m) {
    116             return {error: true};
    117         }
    118         const [h1, d1Str, m1Str, s1Str, h2, d2Str, m2Str, s2Str, h3] = m.slice(1);
    119         const hemispheres = CoordinatesDMS.parseHemispheres(h1, h2, h3, true);
    120         if (hemispheres.error) {
    121             return {error: true};
    122         }
    123         let [d1, m1, s1, d2, m2, s2] = [d1Str, m1Str, s1Str, d2Str, m2Str, s2Str].map(parseFloat);
    124         const coords = [];
    125         if (hemispheres.empty) {
    126             const coord1 = new CoordinatesDMS(d1, m1, s1, false, d2, m2, s2, false);
    127             const coord2 = new CoordinatesDMS(d2, m2, s2, false, d1, m1, s1, false);
    128             if (coord1.isValid()) {
    129                 coords.push(coord1);
    130             }
    131             if (!coord1.equalTo(coord2) && coord2.isValid()) {
    132                 coords.push(coord2);
    133             }
    134         } else {
    135             if (hemispheres.swapLatLon) {
    136                 [d1, m1, s1, d2, m2, s2] = [d2, m2, s2, d1, m1, s1];
    137             }
    138             const coord = new CoordinatesDMS(d1, m1, s1, hemispheres.latIsSouth, d2, m2, s2, hemispheres.lonIsWest);
    139             if (coord.isValid()) {
    140                 coords.push(coord);
    141             }
    142         }
    143         if (coords.length > 0) {
    144             return {coordinates: coords};
    145         }
    146         return {error: true};
    147     }
    148 }
    149 
    150 class CoordinatesDM extends Coordinates {
    151     static regexp = new RegExp(
    152         `^(${reHemisphere} )?(${reInteger}) (${reFractional}) (${reHemisphere} )?(${reInteger}) (${reFractional})( ${reHemisphere})?$`, // eslint-disable-line max-len
    153         'u'
    154     );
    155 
    156     constructor(latDeg, latMin, latIsSouth, lonDeg, lonMin, lonIsWest) {
    157         super();
    158         Object.assign(this, {latDeg, latMin, latIsSouth, lonDeg, lonMin, lonIsWest});
    159     }
    160 
    161     equalTo(other) {
    162         return (
    163             this.latDeg === other.latDeg &&
    164             this.latMin === other.latMin &&
    165             this.latIsSouth === other.latIsSouth &&
    166             this.lonDeg === other.lonDeg &&
    167             this.lonMin === other.lonMin &&
    168             this.lonIsWest === other.lonIsWest
    169         );
    170     }
    171 
    172     format() {
    173         return {
    174             latitude: `${this.getLatitudeLetter()} ${this.latDeg}°${this.latMin}′`,
    175             longitude: `${this.getLongitudeLetter()} ${this.lonDeg}°${this.lonMin}′`,
    176         };
    177     }
    178 
    179     isValid() {
    180         return (
    181             this.latDeg >= 0 &&
    182             this.latDeg <= 90 &&
    183             this.latMin >= 0 &&
    184             this.latMin < 60 &&
    185             this.lonDeg >= 0 &&
    186             this.lonDeg <= 180 &&
    187             this.lonMin >= 0 &&
    188             this.lonMin < 60 &&
    189             (this.latDeg <= 89 || this.latMin === 0) &&
    190             (this.lonDeg <= 179 || this.lonMin === 0)
    191         );
    192     }
    193 
    194     getLatLng() {
    195         let lat = this.latDeg + this.latMin / 60;
    196         if (this.latIsSouth) {
    197             lat = -lat;
    198         }
    199         let lon = this.lonDeg + this.lonMin / 60;
    200         if (this.lonIsWest) {
    201             lon = -lon;
    202         }
    203         return L.latLng(lat, lon);
    204     }
    205 
    206     static fromString(s) {
    207         const m = s.match(CoordinatesDM.regexp);
    208         if (!m) {
    209             return {error: true};
    210         }
    211         const [h1, d1Str, m1Str, h2, d2Str, m2Str, h3] = m.slice(1);
    212         const hemispheres = CoordinatesDM.parseHemispheres(h1, h2, h3, true);
    213         if (hemispheres.error) {
    214             return {error: true};
    215         }
    216         let [d1, m1, d2, m2] = [d1Str, m1Str, d2Str, m2Str].map(parseFloat);
    217         const coords = [];
    218         if (hemispheres.empty) {
    219             const coord1 = new CoordinatesDM(d1, m1, false, d2, m2, false);
    220             const coord2 = new CoordinatesDM(d2, m2, false, d1, m1, false);
    221             if (coord1.isValid()) {
    222                 coords.push(coord1);
    223             }
    224             if (!coord1.equalTo(coord2) && coord2.isValid()) {
    225                 coords.push(coord2);
    226             }
    227         } else {
    228             if (hemispheres.swapLatLon) {
    229                 [d1, m1, d2, m2] = [d2, m2, d1, m1];
    230             }
    231             const coord = new CoordinatesDM(d1, m1, hemispheres.latIsSouth, d2, m2, hemispheres.lonIsWest);
    232             if (coord.isValid()) {
    233                 coords.push(coord);
    234             }
    235         }
    236         if (coords.length > 0) {
    237             return {coordinates: coords};
    238         }
    239         return {error: true};
    240     }
    241 }
    242 
    243 class CoordinatesD extends Coordinates {
    244     static regexp = new RegExp(
    245         `^(${reHemisphere} )?(${reFractional}) (${reHemisphere} )?(${reFractional})( ${reHemisphere})?$`,
    246         'u'
    247     );
    248 
    249     constructor(latDeg, latIsSouth, lonDeg, lonIsWest) {
    250         super();
    251         Object.assign(this, {latDeg, latIsSouth, lonDeg, lonIsWest});
    252     }
    253 
    254     format() {
    255         return {
    256             latitude: `${this.getLatitudeLetter()} ${this.latDeg}°`,
    257             longitude: `${this.getLongitudeLetter()} ${this.lonDeg}°`,
    258         };
    259     }
    260 
    261     equalTo(other) {
    262         return (
    263             this.latDeg === other.latDeg &&
    264             this.latIsSouth === other.latIsSouth &&
    265             this.lonDeg === other.lonDeg &&
    266             this.lonIsWest === other.lonIsWest
    267         );
    268     }
    269 
    270     isValid() {
    271         return this.latDeg >= 0 && this.latDeg <= 90 && this.lonDeg >= 0 && this.lonDeg <= 180;
    272     }
    273 
    274     getLatLng() {
    275         let lat = this.latDeg;
    276         if (this.latIsSouth) {
    277             lat = -lat;
    278         }
    279         let lon = this.lonDeg;
    280         if (this.lonIsWest) {
    281             lon = -lon;
    282         }
    283         return L.latLng(lat, lon);
    284     }
    285 
    286     static fromString(s) {
    287         const m = s.match(CoordinatesD.regexp);
    288         if (!m) {
    289             return {error: true};
    290         }
    291         const [h1, d1Str, h2, d2Str, h3] = m.slice(1);
    292         const hemispheres = CoordinatesD.parseHemispheres(h1, h2, h3);
    293         if (hemispheres.error) {
    294             return {error: true};
    295         }
    296         let [d1, d2] = [d1Str, d2Str].map(parseFloat);
    297         if (hemispheres.swapLatLon) {
    298             [d1, d2] = [d2, d1];
    299         }
    300         const coord = new CoordinatesD(d1, hemispheres.latIsSouth, d2, hemispheres.lonIsWest);
    301         if (coord.isValid()) {
    302             return {
    303                 coordinates: [coord],
    304             };
    305         }
    306         return {error: true};
    307     }
    308 }
    309 
    310 class CoordinatesDSigned extends Coordinates {
    311     static regexp = new RegExp(`^(${reSignedFractional}) (${reSignedFractional})$`, 'u');
    312 
    313     constructor(latDegSigned, lonDegSigned) {
    314         super();
    315         Object.assign(this, {latDegSigned, lonDegSigned});
    316     }
    317 
    318     equalTo(other) {
    319         return this.latDegSigned === other.latDegSigned && this.lonDegSigned === other.lonDegSigned;
    320     }
    321 
    322     isValid() {
    323         return (
    324             this.latDegSigned >= -90 && this.latDegSigned <= 90 && this.lonDegSigned >= -180 && this.lonDegSigned <= 180
    325         );
    326     }
    327 
    328     format() {
    329         return {
    330             latitude: `${this.latDegSigned}°`,
    331             longitude: `${this.lonDegSigned}°`,
    332         };
    333     }
    334 
    335     getLatLng() {
    336         return L.latLng(this.latDegSigned, this.lonDegSigned);
    337     }
    338 
    339     static fromString(s) {
    340         const m = s.match(CoordinatesDSigned.regexp);
    341         if (!m) {
    342             return {error: true};
    343         }
    344         const coords = [];
    345         const [d1, d2] = m.slice(1).map(parseFloat);
    346         const coord1 = new CoordinatesDSigned(d1, d2);
    347         if (coord1.isValid()) {
    348             coords.push(coord1);
    349         }
    350         const coord2 = new CoordinatesDSigned(d2, d1);
    351         if (!coord1.equalTo(coord2)) {
    352             if (coord2.isValid()) {
    353                 coords.push(coord2);
    354             }
    355         }
    356         if (coords.length === 0) {
    357             return {error: true};
    358         }
    359         return {
    360             coordinates: coords,
    361         };
    362     }
    363 }
    364 
    365 class CoordinatesProvider {
    366     name = 'Coordinates';
    367 
    368     static regexps = {
    369         // This regexp wag generated using script at https://gist.github.com/wladich/3d15edc8fcd8b735ac883ef60fe10bfe
    370         // It matches all unicode characters except Lu (Uppercase Letter), Ll (Lowercase Letter)
    371         // and [0123456789,.-]. It ignores unassigned code points (Cn) and characters that are removed after NFKC
    372         // normalization.
    373         // Manually added: "oO" (lat), "оО" (rus)
    374         // eslint-disable-next-line max-len, no-control-regex, no-misleading-character-class, prettier/prettier
    375         symbols: /[OoОо\u0000-\u002b\u002f\u003a-\u0040\u005b-\u0060\u007b-\u00bf\u00d7\u00f7\u01bb\u01c0-\u01cc\u0294\u02b9-\u036f\u0375\u03f6\u0482-\u0489\u0559-\u055f\u0589-\u109f\u10fb\u10fc\u1100-\u139f\u1400-\u1c7f\u1cc0-\u1cff\u1d2f-\u1d6a\u1dc0-\u1dff\u1f88-\u1f8f\u1f98-\u1f9f\u1fa8-\u1faf\u1fbc-\u1fc1\u1fcc-\u1fcf\u1ffc-\u2131\u213a-\u214d\u214f-\u2182\u2185-\u2bff\u2ce5-\u2cea\u2cef-\u2cf1\u2cf9-\u2cff\u2d30-\ua63f\ua66e-\ua67f\ua69e-\ua721\ua788-\ua78a\ua78f\ua7f7-\ua7f9\ua7fb-\uab2f\uab5b-\uab5f\uabc0-\uffff]/gu,
    376         northernHemishphere: /[Nn]|[СсCc] *[Шш]?/gu,
    377         southernHemishphere: /[Ss]|[Юю] *[Шш]?/gu,
    378         westernHemishphere: /[Ww]|[Зз] *[Дд]?/gu,
    379         easternHemishphere: /[EeЕе]|[ВвB] *[Дд]?/gu, // second Ее is cyrillic
    380     };
    381 
    382     static parsers = [CoordinatesDMS, CoordinatesDM, CoordinatesD, CoordinatesDSigned];
    383 
    384     normalizeInput(inp) {
    385         let s = inp.normalize('NFKC'); // convert subscripts and superscripts to normal chars
    386         s = ' ' + s + ' ';
    387         // replace everything that is not letter, number, minus, dot or comma to space
    388         s = s.replace(CoordinatesProvider.regexps.symbols, ' ');
    389         // remove all dots and commas if they are not between digits
    390         s = s.replace(/[,.](?=\D)/gu, ' ');
    391         s = s.replace(/(\D)[,.]/gu, '$1 '); // lookbehind is not supported in all browsers
    392         // if dot is likely to be used as decimal separator, remove all commas
    393         if (s.includes('.')) {
    394             s = s.replace(/,/gu, ' ');
    395         } else {
    396             // otherwise comma is decimal separator
    397             s = s.replace(/,/gu, '.');
    398         }
    399         s = s.replace(/-(?=\D)/gu, ' '); // remove all minuses that are not in the beginning of number
    400         s = s.replace(/([^ ])-/gu, '$1 '); // lookbehind is not supported in all browsers
    401 
    402         s = s.replace(CoordinatesProvider.regexps.northernHemishphere, ' N ');
    403         s = s.replace(CoordinatesProvider.regexps.southernHemishphere, ' S ');
    404         s = s.replace(CoordinatesProvider.regexps.westernHemishphere, ' W ');
    405         s = s.replace(CoordinatesProvider.regexps.easternHemishphere, ' E ');
    406 
    407         s = s.replace(/ +/gu, ' '); // compress whitespaces
    408         s = s.trim();
    409         return s;
    410     }
    411 
    412     isOurQuery(query) {
    413         const coordFieldRe = new RegExp(`^((${reHemisphere})|(${reSignedFractional}))$`, 'u');
    414         const coordNumbersFieldRe = new RegExp(`^(${reSignedFractional})$`, 'u');
    415         const fields = this.normalizeInput(query).split(' ');
    416         return (
    417             fields.length > 1 &&
    418             fields.every((field) => field.match(coordFieldRe)) &&
    419             fields.some((field) => field.match(coordNumbersFieldRe))
    420         );
    421     }
    422 
    423     async search(query) {
    424         const s = this.normalizeInput(query);
    425         for (const parser of CoordinatesProvider.parsers) {
    426             const result = parser.fromString(s);
    427             if (!result.error) {
    428                 const resultItems = result.coordinates.map((it) => {
    429                     const coordStrings = it.format();
    430                     return {
    431                         title: `${coordStrings.latitude} ${coordStrings.longitude}`,
    432                         latlng: it.getLatLng(),
    433                         zoom: 17,
    434                         category: 'Coordinates',
    435                         address: null,
    436                         icon: null,
    437                     };
    438                 });
    439                 return {
    440                     results: resultItems,
    441                 };
    442             }
    443         }
    444         return {error: 'Invalid coordinates'};
    445     }
    446 }
    447 
    448 export {CoordinatesProvider};