2  * Copyright (c) 2016 highstreet technologies GmbH and others. All rights reserved.
 
   4  * This program and the accompanying materials are made available under the
 
   5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 
   6  * and is available at http://www.eclipse.org/legal/epl-v10.html
 
  10  * @typedef {{id: string, siteLink: string, radio: string, polarization: string }} AirInterfaceLink
 
  11  * @typedef {{id: string, siteA: string, siteZ: string, siteNameA: string, siteNameZ: string, airInterfaceLinks: AirInterfaceLink[] }} DbLink
 
  12  * @typedef {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: DbLink, _type: string}[], max_score: number, total: number}}, status: number}} DbLinkResult */
 
  14 define(['app/mwtnTopology/mwtnTopology.module'], function (mwtnTopologyApp) {
 
  15 // module.exports = function () {
 
  16 //   const mwtnTopologyApp = require('app/mwtnTopology/mwtnTopology.module');
 
  17 //   const mwtnTopologyCommons = require('app/mwtnCommons/mwtnCommons.service');
 
  19   mwtnTopologyApp.factory('$mwtnTopology', function ($q, $mwtnCommons, $mwtnDatabase, $mwtnLog) {
 
  22     // AF/MF: Obsolete - will removed soon. All data access function
 
  23     service.getRequiredNetworkElements = $mwtnCommons.getRequiredNetworkElements;
 
  24     service.gridOptions = $mwtnCommons.gridOptions;
 
  25     service.highlightFilteredHeader = $mwtnCommons.highlightFilteredHeader;
 
  26     service.getAllData = $mwtnDatabase.getAllData;
 
  30       * Since not all browsers implement this we have our own utility that will
 
  31       * convert from degrees into radians
 
  33       * @param deg - The degrees to be converted into radians
 
  36     var _toRad = function (deg) {
 
  37       return deg * Math.PI / 180;
 
  41      * Since not all browsers implement this we have our own utility that will
 
  42      * convert from radians into degrees
 
  44      * @param rad - The radians to be converted into degrees
 
  47     var _toDeg = function (rad) {
 
  48       return rad * 180 / Math.PI;
 
  53      * Calculate the bearing between two positions as a value from 0-360
 
  55      * @param lat1 - The latitude of the first position
 
  56      * @param lng1 - The longitude of the first position
 
  57      * @param lat2 - The latitude of the second position
 
  58      * @param lng2 - The longitude of the second position
 
  60      * @return int - The bearing between 0 and 360
 
  62     service.bearing = function (lat1, lng1, lat2, lng2) {
 
  63         var dLon = (lng2 - lng1);
 
  64         var y = Math.sin(dLon) * Math.cos(lat2);
 
  65         var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
 
  66         var brng = _toDeg(Math.atan2(y, x));
 
  67         return 360 - ((brng + 360) % 360);
 
  72      * Gets the geospatial distance between two points
 
  73      * @param lat1 {number} The latitude of the first point.
 
  74      * @param lon1 {number} The longitude of the first point.
 
  75      * @param lat2 {number} The latitude of the second point.
 
  76      * @param lon2 {number} The longitude of the second point.
 
  77      * @returns {number} The distance between the two given points.
 
  79     service.getDistance = function (lat1, lon1, lat2, lon2) {
 
  81       var φ1 = _toRad(lat1);
 
  82       var φ2 = _toRad(lat2);
 
  83       var Δφ = _toRad(lat2 - lat1);
 
  84       var Δλ = _toRad(lon2 - lon1);
 
  86       var a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
 
  87         Math.cos(φ1) * Math.cos(φ2) *
 
  88         Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
 
  89       var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
 
  91       return (R * c).toFixed(3);
 
  95      * Gets a promise which is resolved if the database has been calculated the bounds containing all sites.
 
  96      * @returns {promise} The promise which is resolved if the database has completed its calculation.
 
  98     service.getOuterBoundingRectangleForSites = function () {
 
  99       var getOuterBoundingRectangleForSitesDefer = $q.defer();
 
 104               "field": "location.lat"
 
 109               "field": "location.lon"
 
 114               "field": "location.lat"
 
 119               "field": "location.lon"
 
 126       $mwtnDatabase.getAggregatedData('mwtn', 'site', aggregation).then(function (result) {
 
 127         getOuterBoundingRectangleForSitesDefer.resolve({
 
 128           top: result.data.aggregations.top.value,
 
 129           right: result.data.aggregations.right.value,
 
 130           bottom: result.data.aggregations.bottom.value,
 
 131           left: result.data.aggregations.left.value
 
 133       }, function (error) {
 
 134         getOuterBoundingRectangleForSitesDefer.reject(error);
 
 137       return getOuterBoundingRectangleForSitesDefer.promise;
 
 141      * Gets a promise which resolved with an array of sites within the given bounding box.
 
 142      * @param boundingBox {{top: number, right: number, bottom: number, left: number}} The bounding box to get all sites for.
 
 143      * @param chunkSize {number} The maximum count of sites who should return.
 
 144      * @param chunkSiteStartIndex {number} The index of the first site element to get.
 
 146     service.getSitesInBoundingBox = function (boundingBox, chunkSize, chunkSiteStartIndex) {
 
 147       var resultDefer = $q.defer();
 
 150         "geo_bounding_box": {
 
 152             "top": boundingBox.top,
 
 153             "right": boundingBox.right,
 
 154             "bottom": boundingBox.bottom,
 
 155             "left": boundingBox.left
 
 160       $mwtnDatabase.getFilteredData("mwtn", "site", chunkSiteStartIndex, chunkSize, filter)
 
 161         .then(processResult, resultDefer.reject);
 
 163       return resultDefer.promise;
 
 166        * Callback for the database request.
 
 167        * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, location: {lat: number, lon: number}, "amsl-ground": number, references: {"network-elements": string[], "site-links": string[]}}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 169       function processResult(result) {
 
 170         var hits = result && result.data && result.data.hits;
 
 172           resultDefer.reject("Invalid result.");
 
 176         var total = hits.total;
 
 179         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
 
 180           var site = hits.hits[hitIndex];
 
 183             name: site._source.name,
 
 184             type: site._source.type,
 
 186               lat: site._source.location.lat,
 
 187               lng: site._source.location.lon
 
 189             amslGround: site._source["amsl-ground"],
 
 191               siteLinks: site._source.references["site-links"]
 
 196         resultDefer.resolve({
 
 197           chunkSize: chunkSize,
 
 198           chunkSiteStartIndex: chunkSiteStartIndex,
 
 206      * Gets a promise which is resolved with an array of sites filtered by given site ids.
 
 207      * This function does not use chunks!
 
 208      * @param siteIds {string[]} The ids of the sites to return.
 
 210     service.getSitesByIds = function (siteIds) {
 
 211       var resultDefer = $q.defer();
 
 213       if (!siteIds || siteIds.length === 0) {
 
 214         resultDefer.resolve([]);
 
 215         return resultDefer.promise;
 
 220           should: siteIds.map(function (siteId) {
 
 221             return { term: { id: siteId } };
 
 226       $mwtnDatabase.getFilteredData("mwtn", "site", 0, siteIds.length, query)
 
 227         .then(processResult, resultDefer.reject);
 
 229       return resultDefer.promise;
 
 232        * Callback for the database request.
 
 233        * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, location: {lat: number, lon: number}, "amsl-ground": number, references: {"network-elements": string[], "site-links": string[]}}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 235       function processResult(result) {
 
 236         var hits = result && result.data && result.data.hits;
 
 238           resultDefer.reject("Invalid result.");
 
 242         var total = hits.total;
 
 245         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
 
 246           var site = hits.hits[hitIndex];
 
 249             name: site._source.name,
 
 250             type: site._source.type,
 
 252               lat: site._source.location.lat,
 
 253               lng: site._source.location.lon
 
 255             amslGround: site._source["amsl-ground"],
 
 257               siteLinks: site._source.references["site-links"]
 
 262         resultDefer.resolve({
 
 270      * Gets a promise which is resolved with an array of sites using the given filter expression.
 
 272     service.getSites = function (sortColumn, sortDirection, filters, chunkSize, chunkSiteStartIndex) {
 
 273       var resultDefer = $q.defer();
 
 275       // determine the sort parameter
 
 277       if (sortColumn != null && sortDirection != null) {
 
 279         switch (sortColumn) {
 
 280           case 'countNetworkElements':
 
 285             sort['amsl-ground'] = {
 
 286               order: sortDirection === 'desc' ? 'desc' : 'asc'
 
 291               order: sortDirection === 'desc' ? 'desc' : 'asc'
 
 297        // determine the query parameter
 
 299       if (filters == null || filters.length == 0) {
 
 300         query["match_all"] = {};
 
 303         filters.forEach(function (filter) {
 
 304           if (filter && filter.field) {
 
 305             regexp[filter.field] = '.*'+ filter.term + '.*';
 
 308         query["regexp"] = regexp;
 
 313         $mwtnDatabase.getFilteredSortedData("mwtn", "site", chunkSiteStartIndex, chunkSize, sort, query).then(processResult, resultDefer.reject);
 
 315         $mwtnDatabase.getFilteredData("mwtn", "site", chunkSiteStartIndex, chunkSize, query).then(processResult, resultDefer.reject);
 
 318       return resultDefer.promise;
 
 321        * Callback for the database request.
 
 322        * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, location: {lat: number, lon: number}, "amsl-ground": number, references: {"network-elements": string[], "site-links": string[]}}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 324       function processResult(result) {
 
 325         var hits = result && result.data && result.data.hits;
 
 327           resultDefer.reject("Invalid result.");
 
 331         var total = hits.total;
 
 334         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
 
 335           var site = hits.hits[hitIndex];
 
 338             name: site._source.name,
 
 339             type: site._source.type,
 
 341               lat: site._source.location.lat,
 
 342               lng: site._source.location.lon
 
 344             amslGround: site._source["amsl-ground"],
 
 346               siteLinks: site._source.references["site-links"]
 
 351         resultDefer.resolve({
 
 359      * Gets a promise which resolved with an array of site links referenced by given sites.
 
 360      * @param sites {({id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]})[]}
 
 361      * @param chunkSize {number} The maximum count of site links who should return.
 
 362      * @param chunkSiteLinkStartIndex {number} The index of the first site link element to get.
 
 364     service.getSiteLinksForSites = function (sites, chunkSize, chunkSiteLinkStartIndex) {
 
 365       var resultDefer = $q.defer();
 
 367       if (!sites || sites.length === 0) {
 
 368         resultDefer.resolve([]);
 
 369         return resultDefer.promise;
 
 372       var siteLinkIds = Object.keys(sites.reduce(function (accumulator, currentSite) {
 
 373         // Add all site link ids referenced by the current site to the accumulator object.
 
 374         currentSite.references.siteLinks.forEach(function (siteLinkId) {
 
 375           // The value "true"" isnt important, i only use the key (siteLinkId) later.
 
 376           // But this way i dont have to check, if the key is already known.
 
 377           accumulator[siteLinkId] = true;
 
 380       }, {})).map(function (siteLinkId) {
 
 381         return { term: { id: siteLinkId } };
 
 385         bool: { should: siteLinkIds }
 
 388       $mwtnDatabase.getFilteredData("mwtn", "site-link", chunkSiteLinkStartIndex, chunkSize, query).then(
 
 390          * Callback for the database request.
 
 391          * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, siteA: string, siteZ: string, siteNameA: string, siteNameZ: string, airInterfaceLinks: {id: string, siteLink: string, radio: string, polarization: string }[] }, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 394           var hits = result && result.data && result.data.hits;
 
 396             resultDefer.reject("Invalid result.");
 
 400           if (hits.total === 0) {
 
 401             resultDefer.resolve([]);
 
 405           // get additional sites that wont be given in the sites array but are referenced by the site links.
 
 406           // get all sites, referenced by the site links.
 
 407           var allReferencedSiteIds = hits.hits.reduce(function (accumulator, currentSiteLink) {
 
 408             accumulator[currentSiteLink._source.siteA] = true;
 
 409             accumulator[currentSiteLink._source.siteZ] = true;
 
 412           // remove all known sites
 
 413           sites.forEach(function (site) {
 
 414             if (allReferencedSiteIds.hasOwnProperty(site.id)) {
 
 415               delete allReferencedSiteIds[site.id];
 
 419           var additionalReferencedSiteIds = Object.keys(allReferencedSiteIds).map(function (referencedSiteId) {
 
 420             return { term: { id: referencedSiteId } };
 
 423           if (additionalReferencedSiteIds.length > 0) {
 
 425               bool: { should: additionalReferencedSiteIds }
 
 428             $mwtnDatabase.getFilteredData("mwtn", "site", 0, 400, query).then(
 
 430                * Callback for the database request.
 
 431                * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, location: {lat: number, lon: number}, "amsl-ground": number, references: {"network-elements": string[], "site-links": string[]}}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 434                 var siteHits = result && result.data && result.data.hits;
 
 436                   resultDefer.reject("Invalid result.");
 
 440                 var additionalSites = siteHits.hits.map(function (site) {
 
 443                     name: site._source.name,
 
 444                     type: site._source.type,
 
 446                       lat: site._source.location.lat,
 
 447                       lng: site._source.location.lon
 
 449                     amslGround: site._source["amsl-ground"],
 
 450                     type: site._source.type,
 
 452                       siteLinks: site._source.references["site-links"]
 
 457                 var siteLinks = hits.hits.map(function (siteLink) {
 
 459                     id: siteLink._source.id,
 
 460                     siteA: sites.find(function (site) { return site.id === siteLink._source.siteA; }) || additionalSites.find(function (site) { return site.id === siteLink._source.siteA; }),
 
 461                     siteZ: sites.find(function (site) { return site.id === siteLink._source.siteZ; }) || additionalSites.find(function (site) { return site.id === siteLink._source.siteZ; }),
 
 462                     type: siteLink._source.type,
 
 463                     length: 5000 // AF/MF: the length will be served from the database in the next version.
 
 467                 resultDefer.resolve(siteLinks);
 
 474           var siteLinks = hits.hits.map(function (siteLink) {
 
 476               id: siteLink._source.id,
 
 477               siteA: sites.find(function (site) { return site.id === siteLink._source.siteA; }),
 
 478               siteZ: sites.find(function (site) { return site.id === siteLink._source.siteZ; }),
 
 479               type: siteLink._source.type,
 
 480               length: 5000 // AF/MF: the length will be served from the database in the next version.
 
 484           resultDefer.resolve(siteLinks);
 
 488       return resultDefer.promise;
 
 492      * Gets a promise which is resolved with an array of planned filtered by given network element ids.
 
 493      * This function does not use chunks!
 
 494      * @param neIds {string[]} The ids of the site links to return.
 
 496     service.getPlannedNetworkElementsByIds = function (neIds) {
 
 497       var resultDefer = $q.defer();
 
 499       if (!neIds || neIds.length === 0) {
 
 500         resultDefer.resolve([]);
 
 501         return resultDefer.promise;
 
 506           should: neIds.map(function (neId) {
 
 507             return { term: { id: neId } };
 
 512       $mwtnDatabase.getFilteredData("mwtn", "planned-network-elements", 0, neIds.length, query).then(
 
 514          * Callback for the database request.
 
 515          * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, type: string}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 518           var hits = result && result.data && result.data.hits;
 
 520             resultDefer.reject("Invalid result.");
 
 524           if (hits.total === 0) {
 
 525             resultDefer.resolve([]);
 
 529           var plannedNetworkElements = hits.hits.map(function (plannedNetworkElement) {
 
 531               id: plannedNetworkElement._source.id,
 
 532               name: plannedNetworkElement._source.name,
 
 533               type: plannedNetworkElement._source.radioType
 
 537           resultDefer.resolve(plannedNetworkElements);
 
 538         }, resultDefer.reject);
 
 540       return resultDefer.promise;
 
 545      * Gets a promise which is resolved with an array of site links filtered by given site link ids.
 
 546      * This function does not use chunks!
 
 547      * @param siteLinkIds {string[]} The ids of the site links to return.
 
 549     service.getSiteLinksByIds = function (siteLinkIds) {
 
 550       var resultDefer = $q.defer();
 
 552       if (!siteLinkIds || siteLinkIds.length === 0) {
 
 553         resultDefer.resolve([]);
 
 554         return resultDefer.promise;
 
 559           should: siteLinkIds.map(function (siteLinkId) {
 
 560             return { term: { id: siteLinkId } };
 
 565       $mwtnDatabase.getFilteredData("mwtn", "site-link", 0, siteLinkIds.length, query).then(
 
 567          * Callback for the database request.
 
 568          * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, siteA: string, siteZ: string}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 571           var hits = result && result.data && result.data.hits;
 
 573             resultDefer.reject("Invalid result.");
 
 577           if (hits.total === 0) {
 
 578             resultDefer.resolve([]);
 
 582           var siteLinks = hits.hits.map(function (siteLink) {
 
 584               id: siteLink._source.id,
 
 585               siteA: siteLink._source.siteA,
 
 586               siteZ: siteLink._source.siteZ,
 
 587               azimuthAz: siteLink._source.azimuthAZ,
 
 588               azimuthZa: siteLink._source.azimuthZA,
 
 589               length: siteLink._source.length,
 
 590               type: siteLink._source.type
 
 594           resultDefer.resolve(siteLinks);
 
 595         }, resultDefer.reject);
 
 597       return resultDefer.promise;
 
 601      *  Gets a promise with all details for a given site by its id.
 
 602      *  @param siteId {string} The id of the site to request the details for.
 
 604     service.getSiteDetailsBySiteId = function (siteId) {
 
 605       var resultDefer = $q.defer();
 
 619       // get all site details
 
 620       $mwtnDatabase.getFilteredData("mwtn", "site", 0, 1, siteQuery).then(
 
 622         * Callback for the database request.
 
 623        * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, name: string, location: {lat: number, lon: number}, "amsl-ground": number, references: {"network-elements": string[], "site-links": string[]}}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 626           if (result.data.hits.total != 1) {
 
 627             // todo: handle this error
 
 628             resultDefer.reject("Error loading details for " + siteId + ((result.data.hits.total) ? ' Too many recoeds found.' : ' No record found.'));
 
 630           var site = result.data.hits.hits[0];
 
 633             name: site._source.name,
 
 634             type: site._source.type,
 
 636               lat: site._source.location.lat,
 
 637               lng: site._source.location.lon
 
 639             amslGround: site._source["amsl-ground"],
 
 641               siteLinks: site._source.references["site-links"],
 
 642               networkElements: site._source.references["network-elements"]
 
 646           service.getSiteLinksByIds(siteDetails.references.siteLinks).then(
 
 647             /** Callback for the database request.
 
 648              *  @param result {{id: string, siteA: string, siteZ: string }[]}
 
 651               siteDetails.siteLinks = result;
 
 652               resultDefer.resolve(siteDetails);
 
 655             service.getPlannedNetworkElementsByIds(siteDetails.references.networkElements).then(
 
 656             /** Callback for the database request.
 
 657              *  @param result {{id: string, name: string, type: string }[]}
 
 660               siteDetails.plannedNetworkElements = result;
 
 661               resultDefer.resolve(siteDetails);
 
 665       return resultDefer.promise;
 
 669      *  Gets a promise with all details for a given link by its id.
 
 670      *  @param siteId {string} The id of the link to request the details for.
 
 672     service.getLinkDetailsByLinkId = function (linkId) {
 
 673       var resultDefer = $q.defer();
 
 687       // get all site details
 
 688       $mwtnDatabase.getFilteredData("mwtn", "site-link", 0, 1, linkQuery).then(
 
 690         * Callback for the database request.
 
 691        * @param result { DbLinkResult }  The database result.
 
 694           if (result.data.hits.total != 1) {
 
 695             // todo: handle this error
 
 696             resultDefer.reject("Error loading details for " + siteId + ((result.data.hits.total) ? ' Too many recoeds found.' : ' No record found.'));
 
 698           var link = result.data.hits.hits[0];
 
 701             siteA: link._source.siteA,
 
 702             siteZ: link._source.siteZ,
 
 703             siteNameA: link._source.siteNameA,
 
 704             siteNameZ: link._source.siteNameZ,
 
 705             length: link._source.length,
 
 706             azimuthA: link._source.azimuthAZ,
 
 707             azimuthB: link._source.azimuthZA,
 
 708             airInterfaceLinks: link._source.airInterfaceLinks,
 
 709             type: link._source.type,
 
 710             airInterfaceLinks: link._source.airInterfaceLinks
 
 713           service.getSitesByIds([link._source.siteA, link._source.siteZ]).then(
 
 714             /** Callback for the database request.
 
 715              *  @param result {{ total: number, sites: { id: string, name: string } []}}
 
 718               var siteA = result.sites.find(function (site) { return site.id == linkDetails.siteA });
 
 719               var siteZ = result.sites.find(function (site) { return site.id == linkDetails.siteZ });
 
 720               if (result.total != 2 || !siteA || !siteZ) {
 
 721                 resultDefer.reject("Could not load Sites for link "+linkDetails.id);
 
 723               linkDetails.siteA = siteA;
 
 724               linkDetails.siteZ = siteZ;
 
 726               resultDefer.resolve(linkDetails);
 
 730       return resultDefer.promise;
 
 734      * Gets a promise which is resolved with an array of links using the given filter expression.
 
 736     service.getLinks = function (sortColumn, sortDirection, filters, chunkSize, chunkSiteStartIndex) {
 
 737       var resultDefer = $q.defer();
 
 739       // determine the sort parameter
 
 741       if (sortColumn != null && sortDirection != null) {
 
 743         switch (sortColumn) {
 
 746               order: sortDirection === 'desc' ? 'desc' : 'asc'
 
 751               order: sortDirection === 'desc' ? 'desc' : 'asc'
 
 756               order: sortDirection === 'desc' ? 'desc' : 'asc'
 
 762       // determine the query parameter
 
 764       if (filters == null || filters.length == 0) {
 
 765         query["match_all"] = {};
 
 768         filters.forEach(function (filter) {
 
 769           if (filter && filter.field) {
 
 770             switch (filter.field) {
 
 772                 regexp['siteIdA'] = '.*' + filter.term + '.*';  
 
 775                 regexp['siteZ'] = '.*' + filter.term + '.*';  
 
 778                 regexp[filter.field] = '.*' + filter.term + '.*';
 
 783         query["regexp"] = regexp;
 
 788         $mwtnDatabase.getFilteredSortedData("mwtn", "site-link", chunkSiteStartIndex, chunkSize, sort, query).then(processResult, resultDefer.reject);
 
 790         $mwtnDatabase.getFilteredData("mwtn", "site-link", chunkSiteStartIndex, chunkSize, query).then(processResult, resultDefer.reject);
 
 794          * Callback for the database request.
 
 795          * @param result {{data: {hits: {hits: {_id: string, _index: string, _score: number, _source: {id: string, siteA: string, siteZ: string}, _type: string}[], max_score: number, total: number}}, status: number}} The database result.
 
 797       function processResult (result) {
 
 798         var hits = result && result.data && result.data.hits;
 
 800           resultDefer.reject("Invalid result.");
 
 804         var total = hits.total;
 
 807         var siteLinks = hits.hits.map(function (siteLink) {
 
 809             id: siteLink._source.id,
 
 810             siteA: siteLink._source.siteA,
 
 811             siteZ: siteLink._source.siteZ,
 
 812             type: siteLink._source.type,
 
 816         resultDefer.resolve({
 
 822       return resultDefer.promise;
 
 828      * Determines if a coordinate is in a bounding box
 
 829      *  @param bounds {{ top: number, left: number, right: number, bottom: number}} The bounding box.
 
 830      *  @param coordinate {{ lat: number, lng: number }} The coordinate.
 
 831      *  @return if the bounding box contains the coordinate 
 
 833     service.isInBounds = function(bounds, coordinate) {
 
 834       var isLongInRange = (bounds.right < bounds.left)
 
 835         ? coordinate.lng >= bounds.left || coordinate.lng <= bounds.right 
 
 836         : coordinate.lng >= bounds.left && coordinate.lng <= bounds.right ;
 
 838       return coordinate.lat >= bounds.bottom && coordinate.lat <= bounds.top && isLongInRange;
 
 841     service.getAllEdges = function () {
 
 843       return getGenericChunk("edge", edges).then(function () {
 
 849      * Retrieves all nodes.
 
 850      * @return a Promis containing an array of nodes
 
 852     service.getAllNodes = function () {
 
 853       var resultDefer = $q.defer();
 
 855       getGenericChunk("node", nodes).then(function () {
 
 857         // recreate the tree structure from the flat list
 
 858         var finalNodes = nodes.reduce(
 
 859           /** @param acc { Node[] } */
 
 860           function (acc, cur, ind, arr) {
 
 861             // the site will be added with the first device, so it can not be missing after the first device is in
 
 862             if (!acc.some(node => node.data.type == "site" && node.data.id == cur.data.grandparent)) {
 
 866                   id: cur.data.grandparent,
 
 867                   label: cur.data.grandparent,
 
 868                   active: cur.data.active
 
 872             if (!acc.some(node => node.data.type == "device" && node.data.id == cur.data.parent)) {
 
 875                   _parent: cur.data.grandparent,
 
 878                   get parent() { return cur.data.grandparent },
 
 879                   set parent(val) { debugger; },
 
 880                   label: cur.data.parent,
 
 881                   active: cur.data.active
 
 886               data: Object.keys(cur.data).reduce(function (obj, key) {
 
 887                 if (key == 'grandparent') {
 
 890                   cur.data.hasOwnProperty(key) && (obj[key] = cur.data[key]);
 
 894               position: cur.position
 
 899         resultDefer.resolve(finalNodes);
 
 902         resultDefer.reject(err);
 
 904       return resultDefer.promise;
 
 907     /** @param nodes {{id: string,position:{ x:number, y:number}}}[]} */
 
 908     service.saveChangedNodes = function (nodes) {
 
 909       return $mwtnDatabase.getBase('topology').then(function (base) {
 
 910         var resultDefer = $q.when();
 
 912         nodes.forEach(function (node) {
 
 913           resultDefer = resultDefer.then(function () {
 
 914             return $mwtnDatabase.genericRequest({
 
 919               command: encodeURI(node.id) +'/_update',
 
 922                   "position": node.position
 
 932     // Start to initialize google maps api and save the returned promise.
 
 933     service.googleMapsApiPromise = initializeGoogleMapsApi();
 
 937     // private helper functions of $mwtnTopology
 
 939     function getGenericChunk(docType, target) {
 
 941       return $mwtnDatabase.getAllData('topology', docType, target.length || 0, size, undefined)
 
 942         .then(function (result) {
 
 943           if (result.status === 200 && result.data) {
 
 944             var total = (result.data.hits && result.data.hits.total) || 0;
 
 945             var hits = (total && result.data.hits.hits) || [];
 
 946             hits.forEach(function (hit) {
 
 947               target.push(hit._source || {});
 
 949             if (total > target.length) {
 
 950               return getGenericChunk(docType, target);
 
 952             return $q.resolve(true);
 
 954             return $q.reject("Could not load " + docType + ".");
 
 957         resultDefer.reject(err);
 
 960       return resultDefer.promise;
 
 965      * Gets a promise which is resolved if the google maps api initialization is completed.
 
 966      * @returns {promise} The promise which is resolved if the initialization is completed.
 
 968     function initializeGoogleMapsApi() {
 
 969       var googleMapsApiDefer = $q.defer();
 
 970       window.googleMapsApiLoadedEvent = new Event("googleMapsApiLoaded");
 
 972       window.addEventListener("googleMapsApiLoaded", function (event) {
 
 975          * Calculates the bounds this map would display at a given zoom level.
 
 977          * @member google.maps.Map
 
 979          * @param {Number}                 zoom         Zoom level to use for calculation.
 
 980          * @param {google.maps.LatLng}     [center]     May be set to specify a different center than the current map center.
 
 981          * @param {google.maps.Projection} [projection] May be set to use a different projection than that returned by this.getProjection().
 
 982          * @param {Element}                [div]        May be set to specify a different map viewport than this.getDiv() (only used to get dimensions).
 
 983          * @return {google.maps.LatLngBounds} the calculated bounds.
 
 986          * var bounds = map.boundsAt(5); // same as map.boundsAt(5, map.getCenter(), map.getProjection(), map.getDiv());
 
 988         google.maps.Map.prototype.boundsAt = function (zoom, center, projection, div) {
 
 989           var p = projection || this.getProjection();
 
 990           if (!p) return undefined;
 
 991           var d = $(div || this.getDiv());
 
 992           var zf = Math.pow(2, zoom) * 2;
 
 993           var dw = d.getStyle('width').toInt() / zf;
 
 994           var dh = d.getStyle('height').toInt() / zf;
 
 995           var cpx = p.fromLatLngToPoint(center || this.getCenter());
 
 996           return new google.maps.LatLngBounds(
 
 997             p.fromPointToLatLng(new google.maps.Point(cpx.x - dw, cpx.y + dh)),
 
 998             p.fromPointToLatLng(new google.maps.Point(cpx.x + dw, cpx.y - dh)));
 
1001         googleMapsApiDefer.resolve();
 
1004       var head = document.getElementsByTagName('head')[0];
 
1006       var callbackScript = document.createElement("script");
 
1007       callbackScript.appendChild(document.createTextNode("function googleMapsApiLoadedCallback() { window.dispatchEvent(window.googleMapsApiLoadedEvent); };"));
 
1009       var googleScript = document.createElement('script');
 
1010       googleScript.src = "https://maps.googleapis.com/maps/api/js?key=AIzaSyBWyNNhRUhXxQpvR7i-Roh23PaWqi-kNdQ&callback=googleMapsApiLoadedCallback";
 
1012       head.appendChild(callbackScript);
 
1013       head.appendChild(googleScript);
 
1015       return googleMapsApiDefer.promise;