nakarte

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

links.js (8042B)


      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$/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                 isShort = true;
    176                 const xhr = await fetch(urlViaCorsProxy(url.toString()), {method: 'HEAD'});
    177                 actualUrl = new URL(xhr.responseURL);
    178             } else {
    179                 actualUrl = url;
    180             }
    181             const lon = parseFloat(actualUrl.searchParams.get('x'));
    182             const lat = parseFloat(actualUrl.searchParams.get('y'));
    183             const zoom = Math.round(parseFloat(actualUrl.searchParams.get('z')));
    184             return makeSearchResults(lat, lon, zoom, 'Mapy.cz view');
    185         } catch (_) {
    186             return {
    187                 error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {
    188                     name: 'Mapy.cz',
    189                 }),
    190             };
    191         }
    192     },
    193 };
    194 
    195 const OpenStreetMapUrl = {
    196     isOurUrl: function (url) {
    197         return Boolean(url.hostname.match(/\bopenstreetmap\./u));
    198     },
    199 
    200     getResults: function (url) {
    201         const m = url.hash.match(/map=([\d.]+)\/([\d.-]+)\/([\d.-]+)/u);
    202         try {
    203             const zoom = Math.round(parseFloat(m[1]));
    204             const lat = parseFloat(m[2]);
    205             const lon = parseFloat(m[3]);
    206             return makeSearchResults(lat, lon, zoom, 'OpenStreetMap view');
    207         } catch (_) {
    208             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'OpenStreetMap'})};
    209         }
    210     },
    211 };
    212 
    213 const NakarteUrl = {
    214     isOurUrl: function (url) {
    215         return url.hostname.match(/\bnakarte\b/u) || !this.getResults(url).error;
    216     },
    217 
    218     getResults: function (url) {
    219         const m = url.hash.match(/\bm=([\d]+)\/([\d.-]+)\/([\d.-]+)/u);
    220         try {
    221             const zoom = Math.round(parseFloat(m[1]));
    222             const lat = parseFloat(m[2]);
    223             const lon = parseFloat(m[3]);
    224             return makeSearchResults(lat, lon, zoom, 'Nakarte view');
    225         } catch (_) {
    226             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'Nakarte'})};
    227         }
    228     },
    229 };
    230 
    231 const urlProcessors = [YandexMapsUrl, GoogleMapsUrl, MapyCzUrl, OpenStreetMapUrl, NakarteUrl];
    232 
    233 class LinksProvider {
    234     name = 'Links';
    235 
    236     isOurQuery(query) {
    237         return Boolean(query.match(/^https?:\/\//u));
    238     }
    239 
    240     async search(query) {
    241         let url;
    242         try {
    243             url = new URL(query);
    244         } catch (e) {
    245             return {error: 'Invalid link'};
    246         }
    247         for (const processor of urlProcessors) {
    248             if (processor.isOurUrl(url)) {
    249                 return processor.getResults(url);
    250             }
    251         }
    252         return {error: 'Unsupported link'};
    253     }
    254 }
    255 
    256 export {LinksProvider};