nakarte

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

links.js (8225B)


      1 import L from 'leaflet';
      2 
      3 import {urlViaCorsProxy} from '~/lib/CORSProxy';
      4 import {fetch} from '~/lib/xhr-promise';
      5 
      6 const MAX_ZOOM = 18;
      7 const MESSAGE_LINK_MALFORMED = 'Invalid coordinates in {name} link';
      8 const MESSAGE_SHORT_LINK_MALFORMED = 'Broken {name} short link';
      9 
     10 function makeSearchResult(lat, lon, zoom, title) {
     11     if (
     12         isNaN(zoom) ||
     13         isNaN(lat) ||
     14         isNaN(lon) ||
     15         zoom < 0 ||
     16         zoom > 25 ||
     17         lat < -90 ||
     18         lat > 90 ||
     19         lon < -180 ||
     20         lon > 180
     21     ) {
     22         throw new Error('Invalid view state value');
     23     }
     24 
     25     return {
     26         latlng: L.latLng(lat, lon),
     27         zoom: zoom > MAX_ZOOM ? MAX_ZOOM : zoom,
     28         title,
     29         category: null,
     30         address: null,
     31         icon: null,
     32     };
     33 }
     34 
     35 function makeSearchResults(lat, lon, zoom, title) {
     36     return {results: [makeSearchResult(lat, lon, zoom, title)]};
     37 }
     38 
     39 const YandexMapsUrl = {
     40     isOurUrl: function (url) {
     41         return (
     42             (url.hostname.match(/\byandex\./u) && url.pathname.match(/^\/maps\//u)) ||
     43             url.hostname.match(/static-maps\.yandex\./u)
     44         );
     45     },
     46 
     47     getResults: async function (url) {
     48         try {
     49             const paramLl = url.searchParams.get('ll');
     50             const paramZ = url.searchParams.get('z');
     51             const [lon, lat] = paramLl.split(',').map(parseFloat);
     52             const zoom = Math.round(parseFloat(paramZ));
     53             return makeSearchResults(lat, lon, zoom, 'Yandex map view');
     54         } catch (_) {
     55             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'Yandex'})};
     56         }
     57     },
     58 };
     59 
     60 const GoogleMapsSimpleMapUrl = {
     61     viewRe: /\/@([-\d.]+),([-\d.]+),(?:([\d.]+)([mz]))?/u,
     62     placeRe: /\/place\/([^/]+)/u,
     63     placeZoom: 14,
     64     panoramaZoom: 16,
     65 
     66     isOurUrl: function (url) {
     67         return Boolean(url.pathname.match(this.viewRe)) || Boolean(url.pathname.match(this.placeRe));
     68     },
     69 
     70     getResults: function (url) {
     71         const results = [];
     72         const path = url.pathname;
     73 
     74         try {
     75             const placeTitleMatch = path.match(this.placeRe);
     76             const placeCoordinatesMatch = path.match(/\/data=[^/]*!8m2!3d([-\d.]+)!4d([-\d.]+)/u);
     77             const title = 'Google map - ' + decodeURIComponent(placeTitleMatch[1]).replace(/\+/gu, ' ');
     78             const lat = parseFloat(placeCoordinatesMatch[1]);
     79             const lon = parseFloat(placeCoordinatesMatch[2]);
     80             results.push(makeSearchResult(lat, lon, this.placeZoom, title));
     81         } catch (e) {
     82             // pass
     83         }
     84 
     85         try {
     86             const viewMatch = path.match(this.viewRe);
     87             const lat = parseFloat(viewMatch[1]);
     88             const lon = parseFloat(viewMatch[2]);
     89             let zoom;
     90             // no need to check viewMatch[4] as they are together in same group
     91             if (viewMatch[3] === undefined) {
     92                 zoom = this.panoramaZoom;
     93             } else {
     94                 zoom = parseFloat(viewMatch[3]);
     95                 // zoom for satellite images is expressed in meters
     96                 if (viewMatch[4] === 'm') {
     97                     zoom = Math.log2((149175296 / zoom) * Math.cos((lat / 180) * Math.PI));
     98                 }
     99                 zoom = Math.round(zoom);
    100             }
    101             results.push(makeSearchResult(lat, lon, zoom, 'Google map view'));
    102         } catch (e) {
    103             // pass
    104         }
    105         if (results.length === 0) {
    106             throw new Error('No results extracted from Google link');
    107         }
    108         return {results};
    109     },
    110 };
    111 
    112 const GoogleMapsQueryUrl = {
    113     zoom: 17,
    114     title: 'Google map view',
    115 
    116     isOurUrl: function (url) {
    117         return url.searchParams.has('q');
    118     },
    119 
    120     getResults: function (url) {
    121         const data = url.searchParams.get('q');
    122         const m = data.match(/^(?:loc:)?([-\d.]+),([-\d.]+)$/u);
    123         const lat = parseFloat(m[1]);
    124         const lon = parseFloat(m[2]);
    125         return makeSearchResults(lat, lon, this.zoom, this.title);
    126     },
    127 };
    128 
    129 const GoogleMapsUrl = {
    130     subprocessors: [GoogleMapsSimpleMapUrl, GoogleMapsQueryUrl],
    131 
    132     isOurUrl: function (url) {
    133         return (url.hostname.match(/\bgoogle\./u) || url.hostname === 'goo.gl') && url.pathname.match(/^\/maps(\/|$)/u);
    134     },
    135 
    136     getResults: async function (url) {
    137         let isShort = false;
    138         let actualUrl;
    139         try {
    140             if (url.hostname === 'goo.gl') {
    141                 isShort = true;
    142                 const xhr = await fetch(urlViaCorsProxy(url.toString()), {method: 'HEAD'});
    143                 actualUrl = new URL(xhr.responseURL);
    144             } else {
    145                 actualUrl = url;
    146             }
    147         } catch (e) {
    148             // pass
    149         }
    150         for (const subprocessor of this.subprocessors) {
    151             try {
    152                 if (subprocessor.isOurUrl(actualUrl)) {
    153                     return subprocessor.getResults(actualUrl);
    154                 }
    155             } catch (e) {
    156                 // pass
    157             }
    158         }
    159         return {
    160             error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {name: 'Google'}),
    161         };
    162     },
    163 };
    164 
    165 const MapyCzUrl = {
    166     isOurUrl: function (url) {
    167         return Boolean(url.hostname.match(/\bmapy\.(cz|com)$/u));
    168     },
    169 
    170     getResults: async function (url) {
    171         let isShort = false;
    172         let actualUrl;
    173         try {
    174             if (url.pathname.match(/^\/s\//u)) {
    175                 // Remove lang subdomain as Seznam broke support for such links when transitioned to mapy.com
    176                 url.host = url.host.replace(/^[a-z]{2}\./u, '');
    177                 isShort = true;
    178                 const xhr = await fetch(urlViaCorsProxy(url.toString()), {method: 'HEAD'});
    179                 actualUrl = new URL(xhr.responseURL);
    180             } else {
    181                 actualUrl = url;
    182             }
    183             const lon = parseFloat(actualUrl.searchParams.get('x'));
    184             const lat = parseFloat(actualUrl.searchParams.get('y'));
    185             const zoom = Math.round(parseFloat(actualUrl.searchParams.get('z')));
    186             return makeSearchResults(lat, lon, zoom, 'Mapy.com view');
    187         } catch (_) {
    188             return {
    189                 error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {
    190                     name: 'Mapy.com',
    191                 }),
    192             };
    193         }
    194     },
    195 };
    196 
    197 const OpenStreetMapUrl = {
    198     isOurUrl: function (url) {
    199         return Boolean(url.hostname.match(/\bopenstreetmap\./u));
    200     },
    201 
    202     getResults: function (url) {
    203         const m = url.hash.match(/map=([\d.]+)\/([\d.-]+)\/([\d.-]+)/u);
    204         try {
    205             const zoom = Math.round(parseFloat(m[1]));
    206             const lat = parseFloat(m[2]);
    207             const lon = parseFloat(m[3]);
    208             return makeSearchResults(lat, lon, zoom, 'OpenStreetMap view');
    209         } catch (_) {
    210             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'OpenStreetMap'})};
    211         }
    212     },
    213 };
    214 
    215 const NakarteUrl = {
    216     isOurUrl: function (url) {
    217         return url.hostname.match(/\bnakarte\b/u) || !this.getResults(url).error;
    218     },
    219 
    220     getResults: function (url) {
    221         const m = url.hash.match(/\bm=([\d]+)\/([\d.-]+)\/([\d.-]+)/u);
    222         try {
    223             const zoom = Math.round(parseFloat(m[1]));
    224             const lat = parseFloat(m[2]);
    225             const lon = parseFloat(m[3]);
    226             return makeSearchResults(lat, lon, zoom, 'Nakarte view');
    227         } catch (_) {
    228             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'Nakarte'})};
    229         }
    230     },
    231 };
    232 
    233 const urlProcessors = [YandexMapsUrl, GoogleMapsUrl, MapyCzUrl, OpenStreetMapUrl, NakarteUrl];
    234 
    235 class LinksProvider {
    236     name = 'Links';
    237 
    238     isOurQuery(query) {
    239         return Boolean(query.match(/^https?:\/\//u));
    240     }
    241 
    242     async search(query) {
    243         let url;
    244         try {
    245             url = new URL(query);
    246         } catch (e) {
    247             return {error: 'Invalid link'};
    248         }
    249         for (const processor of urlProcessors) {
    250             if (processor.isOurUrl(url)) {
    251                 return processor.getResults(url);
    252             }
    253         }
    254         return {error: 'Unsupported link'};
    255     }
    256 }
    257 
    258 export {LinksProvider};