nakarte

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

links.js (9080B)


      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         let isShort = false;
     49         let actualUrl;
     50         try {
     51             if (url.pathname.match(/^\/maps\/-\//u)) {
     52                 isShort = true;
     53                 const pageText = (await fetch(urlViaCorsProxy(url.toString()))).response;
     54                 try {
     55                     const dom = new DOMParser().parseFromString(pageText, 'text/html');
     56                     actualUrl = new URL(dom.querySelector('meta[property="og:image:secure_url"]').content);
     57                 } catch (_) {
     58                     let propertyContent = pageText.match(
     59                         /<meta\s+property\s*=\s*["']?og:image:secure_url["']?\s+content\s*=\s*["']?([^"' >]+)/u
     60                     )[1];
     61                     propertyContent = propertyContent.replaceAll('&amp;', '&');
     62                     actualUrl = new URL(decodeURIComponent(propertyContent));
     63                 }
     64             } else {
     65                 actualUrl = url;
     66             }
     67             const paramLl = actualUrl.searchParams.get('ll');
     68             const paramZ = actualUrl.searchParams.get('z');
     69             const [lon, lat] = paramLl.split(',').map(parseFloat);
     70             const zoom = Math.round(parseFloat(paramZ));
     71             return makeSearchResults(lat, lon, zoom, 'Yandex map view');
     72         } catch (_) {
     73             return {
     74                 error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {
     75                     name: 'Yandex',
     76                 }),
     77             };
     78         }
     79     },
     80 };
     81 
     82 const GoogleMapsSimpleMapUrl = {
     83     viewRe: /\/@([-\d.]+),([-\d.]+),(?:([\d.]+)([mz]))?/u,
     84     placeRe: /\/place\/([^/]+)/u,
     85     placeZoom: 14,
     86     panoramaZoom: 16,
     87 
     88     isOurUrl: function (url) {
     89         return Boolean(url.pathname.match(this.viewRe)) || Boolean(url.pathname.match(this.placeRe));
     90     },
     91 
     92     getResults: function (url) {
     93         const results = [];
     94         const path = url.pathname;
     95 
     96         try {
     97             const placeTitleMatch = path.match(this.placeRe);
     98             const placeCoordinatesMatch = path.match(/\/data=[^/]*!8m2!3d([-\d.]+)!4d([-\d.]+)/u);
     99             const title = 'Google map - ' + decodeURIComponent(placeTitleMatch[1]).replace(/\+/gu, ' ');
    100             const lat = parseFloat(placeCoordinatesMatch[1]);
    101             const lon = parseFloat(placeCoordinatesMatch[2]);
    102             results.push(makeSearchResult(lat, lon, this.placeZoom, title));
    103         } catch (e) {
    104             // pass
    105         }
    106 
    107         try {
    108             const viewMatch = path.match(this.viewRe);
    109             const lat = parseFloat(viewMatch[1]);
    110             const lon = parseFloat(viewMatch[2]);
    111             let zoom;
    112             // no need to check viewMatch[4] as they are together in same group
    113             if (viewMatch[3] === undefined) {
    114                 zoom = this.panoramaZoom;
    115             } else {
    116                 zoom = parseFloat(viewMatch[3]);
    117                 // zoom for satellite images is expressed in meters
    118                 if (viewMatch[4] === 'm') {
    119                     zoom = Math.log2((149175296 / zoom) * Math.cos((lat / 180) * Math.PI));
    120                 }
    121                 zoom = Math.round(zoom);
    122             }
    123             results.push(makeSearchResult(lat, lon, zoom, 'Google map view'));
    124         } catch (e) {
    125             // pass
    126         }
    127         if (results.length === 0) {
    128             throw new Error('No results extracted from Google link');
    129         }
    130         return {results};
    131     },
    132 };
    133 
    134 const GoogleMapsQueryUrl = {
    135     zoom: 17,
    136     title: 'Google map view',
    137 
    138     isOurUrl: function (url) {
    139         return url.searchParams.has('q');
    140     },
    141 
    142     getResults: function (url) {
    143         const data = url.searchParams.get('q');
    144         const m = data.match(/^(?:loc:)?([-\d.]+),([-\d.]+)$/u);
    145         const lat = parseFloat(m[1]);
    146         const lon = parseFloat(m[2]);
    147         return makeSearchResults(lat, lon, this.zoom, this.title);
    148     },
    149 };
    150 
    151 const GoogleMapsUrl = {
    152     subprocessors: [GoogleMapsSimpleMapUrl, GoogleMapsQueryUrl],
    153 
    154     isOurUrl: function (url) {
    155         return (url.hostname.match(/\bgoogle\./u) || url.hostname === 'goo.gl') && url.pathname.match(/^\/maps(\/|$)/u);
    156     },
    157 
    158     getResults: async function (url) {
    159         let isShort = false;
    160         let actualUrl;
    161         try {
    162             if (url.hostname === 'goo.gl') {
    163                 isShort = true;
    164                 const xhr = await fetch(urlViaCorsProxy(url.toString()), {method: 'HEAD'});
    165                 actualUrl = new URL(xhr.responseURL);
    166             } else {
    167                 actualUrl = url;
    168             }
    169         } catch (e) {
    170             // pass
    171         }
    172         for (const subprocessor of this.subprocessors) {
    173             try {
    174                 if (subprocessor.isOurUrl(actualUrl)) {
    175                     return subprocessor.getResults(actualUrl);
    176                 }
    177             } catch (e) {
    178                 // pass
    179             }
    180         }
    181         return {
    182             error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {name: 'Google'}),
    183         };
    184     },
    185 };
    186 
    187 const MapyCzUrl = {
    188     isOurUrl: function (url) {
    189         return Boolean(url.hostname.match(/\bmapy\.cz$/u));
    190     },
    191 
    192     getResults: async function (url) {
    193         let isShort = false;
    194         let actualUrl;
    195         try {
    196             if (url.pathname.match(/^\/s\//u)) {
    197                 isShort = true;
    198                 const xhr = await fetch(urlViaCorsProxy(url.toString()), {method: 'HEAD'});
    199                 actualUrl = new URL(xhr.responseURL);
    200             } else {
    201                 actualUrl = url;
    202             }
    203             const lon = parseFloat(actualUrl.searchParams.get('x'));
    204             const lat = parseFloat(actualUrl.searchParams.get('y'));
    205             const zoom = Math.round(parseFloat(actualUrl.searchParams.get('z')));
    206             return makeSearchResults(lat, lon, zoom, 'Mapy.cz view');
    207         } catch (_) {
    208             return {
    209                 error: L.Util.template(isShort ? MESSAGE_SHORT_LINK_MALFORMED : MESSAGE_LINK_MALFORMED, {
    210                     name: 'Mapy.cz',
    211                 }),
    212             };
    213         }
    214     },
    215 };
    216 
    217 const OpenStreetMapUrl = {
    218     isOurUrl: function (url) {
    219         return Boolean(url.hostname.match(/\bopenstreetmap\./u));
    220     },
    221 
    222     getResults: function (url) {
    223         const m = url.hash.match(/map=([\d.]+)\/([\d.-]+)\/([\d.-]+)/u);
    224         try {
    225             const zoom = Math.round(parseFloat(m[1]));
    226             const lat = parseFloat(m[2]);
    227             const lon = parseFloat(m[3]);
    228             return makeSearchResults(lat, lon, zoom, 'OpenStreetMap view');
    229         } catch (_) {
    230             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'OpenStreetMap'})};
    231         }
    232     },
    233 };
    234 
    235 const NakarteUrl = {
    236     isOurUrl: function (url) {
    237         return url.hostname.match(/\bnakarte\b/u) || !this.getResults(url).error;
    238     },
    239 
    240     getResults: function (url) {
    241         const m = url.hash.match(/\bm=([\d]+)\/([\d.-]+)\/([\d.-]+)/u);
    242         try {
    243             const zoom = Math.round(parseFloat(m[1]));
    244             const lat = parseFloat(m[2]);
    245             const lon = parseFloat(m[3]);
    246             return makeSearchResults(lat, lon, zoom, 'Nakarte view');
    247         } catch (_) {
    248             return {error: L.Util.template(MESSAGE_LINK_MALFORMED, {name: 'Nakarte'})};
    249         }
    250     },
    251 };
    252 
    253 const urlProcessors = [YandexMapsUrl, GoogleMapsUrl, MapyCzUrl, OpenStreetMapUrl, NakarteUrl];
    254 
    255 class LinksProvider {
    256     name = 'Links';
    257 
    258     isOurQuery(query) {
    259         return Boolean(query.match(/^https?:\/\//u));
    260     }
    261 
    262     async search(query) {
    263         let url;
    264         try {
    265             url = new URL(query);
    266         } catch (e) {
    267             return {error: 'Invalid link'};
    268         }
    269         for (const processor of urlProcessors) {
    270             if (processor.isOurUrl(url)) {
    271                 return processor.getResults(url);
    272             }
    273         }
    274         return {error: 'Unsupported link'};
    275     }
    276 }
    277 
    278 export {LinksProvider};