195c08f03279730d73cb4848b25a157137b4d34f
[ccsdk/apps.git] / sdnr / wireless-transport / code-Carbon-SR1 / ux / mwtnTopology / mwtnTopology-module / src / main / resources / mwtnTopology / mwtnTopology.services.js
1 /*
2  * Copyright (c) 2016 highstreet technologies GmbH and others. All rights reserved.
3  *
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
7  */
8
9 /** Type Definitions 
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 */
13
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');
18
19   mwtnTopologyApp.factory('$mwtnTopology', function ($q, $mwtnCommons, $mwtnDatabase, $mwtnLog) {
20     var service = {};
21
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;
27     
28
29     /**
30       * Since not all browsers implement this we have our own utility that will
31       * convert from degrees into radians
32       *
33       * @param deg - The degrees to be converted into radians
34       * @return radians
35       */
36     var _toRad = function (deg) {
37       return deg * Math.PI / 180;
38     };
39
40     /**
41      * Since not all browsers implement this we have our own utility that will
42      * convert from radians into degrees
43      *
44      * @param rad - The radians to be converted into degrees
45      * @return degrees
46      */
47     var _toDeg = function (rad) {
48       return rad * 180 / Math.PI;
49     };
50
51     // public functions
52     /**
53      * Calculate the bearing between two positions as a value from 0-360
54      *
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
59      *
60      * @return int - The bearing between 0 and 360
61      */
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);
68       },
69
70     
71     /**
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.
78      */
79     service.getDistance = function (lat1, lon1, lat2, lon2) {
80       var R = 6371; // km
81       var φ1 = _toRad(lat1);
82       var φ2 = _toRad(lat2);
83       var Δφ = _toRad(lat2 - lat1);
84       var Δλ = _toRad(lon2 - lon1);
85
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));
90
91       return (R * c).toFixed(3);
92     };
93
94     /**
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.
97      */
98     service.getOuterBoundingRectangleForSites = function () {
99       var getOuterBoundingRectangleForSitesDefer = $q.defer();
100       var aggregation = {
101         "aggregations": {
102           "top": {
103             "max": {
104               "field": "location.lat"
105             }
106           },
107           "right": {
108             "max": {
109               "field": "location.lon"
110             }
111           },
112           "bottom": {
113             "min": {
114               "field": "location.lat"
115             }
116           },
117           "left": {
118             "min": {
119               "field": "location.lon"
120             }
121           }
122         },
123         "size": 0
124       };
125
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
132         });
133       }, function (error) {
134         getOuterBoundingRectangleForSitesDefer.reject(error);
135       });
136
137       return getOuterBoundingRectangleForSitesDefer.promise;
138     };
139
140     /**
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.
145      */
146     service.getSitesInBoundingBox = function (boundingBox, chunkSize, chunkSiteStartIndex) {
147       var resultDefer = $q.defer();
148
149       var filter = {
150         "geo_bounding_box": {
151           "location": {
152             "top": boundingBox.top,
153             "right": boundingBox.right,
154             "bottom": boundingBox.bottom,
155             "left": boundingBox.left
156           }
157         }
158       };
159
160       $mwtnDatabase.getFilteredData("mwtn", "site", chunkSiteStartIndex, chunkSize, filter)
161         .then(processResult, resultDefer.reject);
162
163       return resultDefer.promise;
164
165       /**
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.
168        */
169       function processResult(result) {
170         var hits = result && result.data && result.data.hits;
171         if (!hits) {
172           resultDefer.reject("Invalid result.");
173           return;
174         }
175
176         var total = hits.total;
177         var sites = [];
178
179         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
180           var site = hits.hits[hitIndex];
181           sites.push({
182             id: site._source.id,
183             name: site._source.name,
184             type: site._source.type,
185             location: {
186               lat: site._source.location.lat,
187               lng: site._source.location.lon
188             },
189             amslGround: site._source["amsl-ground"],
190             references: {
191               siteLinks: site._source.references["site-links"]
192             }
193           });
194         }
195
196         resultDefer.resolve({
197           chunkSize: chunkSize,
198           chunkSiteStartIndex: chunkSiteStartIndex,
199           total: total,
200           sites: sites
201         })
202       }
203     };
204
205     /**
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.
209      */
210     service.getSitesByIds = function (siteIds) {
211       var resultDefer = $q.defer();
212
213       if (!siteIds || siteIds.length === 0) {
214         resultDefer.resolve([]);
215         return resultDefer.promise;
216       }
217
218       var query = {
219         bool: {
220           should: siteIds.map(function (siteId) {
221             return { term: { id: siteId } };
222           })
223         }
224       };
225
226       $mwtnDatabase.getFilteredData("mwtn", "site", 0, siteIds.length, query)
227         .then(processResult, resultDefer.reject);
228
229       return resultDefer.promise;
230
231       /**
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.
234        */
235       function processResult(result) {
236         var hits = result && result.data && result.data.hits;
237         if (!hits) {
238           resultDefer.reject("Invalid result.");
239           return;
240         }
241
242         var total = hits.total;
243         var sites = [];
244
245         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
246           var site = hits.hits[hitIndex];
247           sites.push({
248             id: site._source.id,
249             name: site._source.name,
250             type: site._source.type,
251             location: {
252               lat: site._source.location.lat,
253               lng: site._source.location.lon
254             },
255             amslGround: site._source["amsl-ground"],
256             references: {
257               siteLinks: site._source.references["site-links"]
258             }
259           });
260         }
261
262         resultDefer.resolve({
263           total: total,
264           sites: sites
265         })
266       }
267     };
268
269     /**
270      * Gets a promise which is resolved with an array of sites using the given filter expression.
271      */
272     service.getSites = function (sortColumn, sortDirection, filters, chunkSize, chunkSiteStartIndex) {
273       var resultDefer = $q.defer();
274
275       // determine the sort parameter
276       var sort = null;
277       if (sortColumn != null && sortDirection != null) {
278         sort = {};
279         switch (sortColumn) {
280           case 'countNetworkElements':
281           case 'countLinks':
282             sort = null;
283             break;
284           case 'amslGround':
285             sort['amsl-ground'] = {
286               order: sortDirection === 'desc' ? 'desc' : 'asc'
287             }
288             break;
289           default:
290             sort[sortColumn] = {
291               order: sortDirection === 'desc' ? 'desc' : 'asc'
292             }
293             break;
294         }
295       }
296
297        // determine the query parameter
298       var query = {};
299       if (filters == null || filters.length == 0) {
300         query["match_all"] = {};
301       } else {
302         var regexp = {};
303         filters.forEach(function (filter) {
304           if (filter && filter.field) {
305             regexp[filter.field] = '.*'+ filter.term + '.*';
306           }
307         });
308         query["regexp"] = regexp;
309       }
310
311
312       if (sort) {
313         $mwtnDatabase.getFilteredSortedData("mwtn", "site", chunkSiteStartIndex, chunkSize, sort, query).then(processResult, resultDefer.reject);
314       } else {
315         $mwtnDatabase.getFilteredData("mwtn", "site", chunkSiteStartIndex, chunkSize, query).then(processResult, resultDefer.reject);
316       }
317
318       return resultDefer.promise;
319
320       /**
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.
323        */
324       function processResult(result) {
325         var hits = result && result.data && result.data.hits;
326         if (!hits) {
327           resultDefer.reject("Invalid result.");
328           return;
329         }
330
331         var total = hits.total;
332         var sites = [];
333
334         for (var hitIndex = 0; hitIndex < hits.hits.length; ++hitIndex) {
335           var site = hits.hits[hitIndex];
336           sites.push({
337             id: site._source.id,
338             name: site._source.name,
339             type: site._source.type,
340             location: {
341               lat: site._source.location.lat,
342               lng: site._source.location.lon
343             },
344             amslGround: site._source["amsl-ground"],
345             references: {
346               siteLinks: site._source.references["site-links"]
347             }
348           });
349         }
350
351         resultDefer.resolve({
352           total: total,
353           sites: sites
354         })
355       }
356     };
357
358     /**
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.
363      */
364     service.getSiteLinksForSites = function (sites, chunkSize, chunkSiteLinkStartIndex) {
365       var resultDefer = $q.defer();
366
367       if (!sites || sites.length === 0) {
368         resultDefer.resolve([]);
369         return resultDefer.promise;
370       }
371
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;
378         });
379         return accumulator;
380       }, {})).map(function (siteLinkId) {
381         return { term: { id: siteLinkId } };
382       });
383
384       var query = {
385         bool: { should: siteLinkIds }
386       };
387
388       $mwtnDatabase.getFilteredData("mwtn", "site-link", chunkSiteLinkStartIndex, chunkSize, query).then(
389         /**
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.
392          */
393         function (result) {
394           var hits = result && result.data && result.data.hits;
395           if (!hits) {
396             resultDefer.reject("Invalid result.");
397             return;
398           }
399
400           if (hits.total === 0) {
401             resultDefer.resolve([]);
402             return;
403           }
404
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;
410             return accumulator;
411           }, {});
412           // remove all known sites
413           sites.forEach(function (site) {
414             if (allReferencedSiteIds.hasOwnProperty(site.id)) {
415               delete allReferencedSiteIds[site.id];
416             }
417           });
418
419           var additionalReferencedSiteIds = Object.keys(allReferencedSiteIds).map(function (referencedSiteId) {
420             return { term: { id: referencedSiteId } };
421           });
422
423           if (additionalReferencedSiteIds.length > 0) {
424             query = {
425               bool: { should: additionalReferencedSiteIds }
426             };
427
428             $mwtnDatabase.getFilteredData("mwtn", "site", 0, 400, query).then(
429               /**
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.
432                */
433               function (result) {
434                 var siteHits = result && result.data && result.data.hits;
435                 if (!siteHits) {
436                   resultDefer.reject("Invalid result.");
437                   return;
438                 }
439
440                 var additionalSites = siteHits.hits.map(function (site) {
441                   return {
442                     id: site._source.id,
443                     name: site._source.name,
444                     type: site._source.type,
445                     location: {
446                       lat: site._source.location.lat,
447                       lng: site._source.location.lon
448                     },
449                     amslGround: site._source["amsl-ground"],
450                     type: site._source.type,
451                     references: {
452                       siteLinks: site._source.references["site-links"]
453                     }
454                   };
455                 });
456
457                 var siteLinks = hits.hits.map(function (siteLink) {
458                   return {
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.
464                   };
465                 });
466
467                 resultDefer.resolve(siteLinks);
468               },
469               resultDefer.reject);
470
471             return;
472           }
473
474           var siteLinks = hits.hits.map(function (siteLink) {
475             return {
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.
481             };
482           });
483
484           resultDefer.resolve(siteLinks);
485         },
486         resultDefer.reject);
487
488       return resultDefer.promise;
489     };
490
491     /**
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.
495      */
496     service.getPlannedNetworkElementsByIds = function (neIds) {
497       var resultDefer = $q.defer();
498
499       if (!neIds || neIds.length === 0) {
500         resultDefer.resolve([]);
501         return resultDefer.promise;
502       }
503
504       var query = {
505         bool: {
506           should: neIds.map(function (neId) {
507             return { term: { id: neId } };
508           })
509         }
510       };
511
512       $mwtnDatabase.getFilteredData("mwtn", "planned-network-elements", 0, neIds.length, query).then(
513         /**
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.
516          */
517         function (result) {
518           var hits = result && result.data && result.data.hits;
519           if (!hits) {
520             resultDefer.reject("Invalid result.");
521             return;
522           }
523
524           if (hits.total === 0) {
525             resultDefer.resolve([]);
526             return;
527           }
528
529           var plannedNetworkElements = hits.hits.map(function (plannedNetworkElement) {
530             return {
531               id: plannedNetworkElement._source.id,
532               name: plannedNetworkElement._source.name,
533               type: plannedNetworkElement._source.radioType
534             };
535           });
536
537           resultDefer.resolve(plannedNetworkElements);
538         }, resultDefer.reject);
539
540       return resultDefer.promise;
541     };
542
543
544     /**
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.
548      */
549     service.getSiteLinksByIds = function (siteLinkIds) {
550       var resultDefer = $q.defer();
551
552       if (!siteLinkIds || siteLinkIds.length === 0) {
553         resultDefer.resolve([]);
554         return resultDefer.promise;
555       }
556
557       var query = {
558         bool: {
559           should: siteLinkIds.map(function (siteLinkId) {
560             return { term: { id: siteLinkId } };
561           })
562         }
563       };
564
565       $mwtnDatabase.getFilteredData("mwtn", "site-link", 0, siteLinkIds.length, query).then(
566         /**
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.
569          */
570         function (result) {
571           var hits = result && result.data && result.data.hits;
572           if (!hits) {
573             resultDefer.reject("Invalid result.");
574             return;
575           }
576
577           if (hits.total === 0) {
578             resultDefer.resolve([]);
579             return;
580           }
581
582           var siteLinks = hits.hits.map(function (siteLink) {
583             return {
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
591             };
592           });
593
594           resultDefer.resolve(siteLinks);
595         }, resultDefer.reject);
596
597       return resultDefer.promise;
598     };
599
600     /**
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.
603      */
604     service.getSiteDetailsBySiteId = function (siteId) {
605       var resultDefer = $q.defer();
606
607       var siteQuery = {
608         "bool": {
609           "must": [
610             {
611               "term": {
612                 "id": siteId
613               }
614             }
615           ]
616         }
617       };
618
619       // get all site details
620       $mwtnDatabase.getFilteredData("mwtn", "site", 0, 1, siteQuery).then(
621         /**
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.
624         */
625         function (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.'));
629           }
630           var site = result.data.hits.hits[0];
631           var siteDetails = {
632             id: site._source.id,
633             name: site._source.name,
634             type: site._source.type,
635             location: {
636               lat: site._source.location.lat,
637               lng: site._source.location.lon
638             },
639             amslGround: site._source["amsl-ground"],
640             references: {
641               siteLinks: site._source.references["site-links"],
642               networkElements: site._source.references["network-elements"]
643             }
644           };
645           
646           service.getSiteLinksByIds(siteDetails.references.siteLinks).then(
647             /** Callback for the database request.
648              *  @param result {{id: string, siteA: string, siteZ: string }[]}
649              */
650             function (result) {
651               siteDetails.siteLinks = result;
652               resultDefer.resolve(siteDetails);
653             });
654
655             service.getPlannedNetworkElementsByIds(siteDetails.references.networkElements).then(
656             /** Callback for the database request.
657              *  @param result {{id: string, name: string, type: string }[]}
658              */
659             function (result) {
660               siteDetails.plannedNetworkElements = result;
661               resultDefer.resolve(siteDetails);
662             });
663         }
664       );
665       return resultDefer.promise;
666     };
667     
668     /**
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.
671      */
672     service.getLinkDetailsByLinkId = function (linkId) {
673       var resultDefer = $q.defer();
674
675       var linkQuery = {
676         "bool": {
677           "must": [
678             {
679               "term": {
680                 "id": linkId
681               }
682             }
683           ]
684         }
685       };
686
687       // get all site details
688       $mwtnDatabase.getFilteredData("mwtn", "site-link", 0, 1, linkQuery).then(
689         /**
690         * Callback for the database request.
691        * @param result { DbLinkResult }  The database result.
692         */
693         function (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.'));
697           }
698           var link = result.data.hits.hits[0];
699           var linkDetails = {
700             id: link._source.id,
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
711           };
712
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 } []}}
716              */
717             function (result) {
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);
722               } 
723               linkDetails.siteA = siteA;
724               linkDetails.siteZ = siteZ;
725               
726               resultDefer.resolve(linkDetails);
727             })
728         }
729       );
730       return resultDefer.promise;
731     } 
732
733     /**
734      * Gets a promise which is resolved with an array of links using the given filter expression.
735      */
736     service.getLinks = function (sortColumn, sortDirection, filters, chunkSize, chunkSiteStartIndex) {
737       var resultDefer = $q.defer();
738
739       // determine the sort parameter
740       var sort = null;
741       if (sortColumn != null && sortDirection != null) {
742         sort = {};
743         switch (sortColumn) {
744           case 'siteIdA':
745             sort['siteA'] = {
746               order: sortDirection === 'desc' ? 'desc' : 'asc'
747             }
748             break;
749           case 'siteIdZ':
750             sort['siteZ'] = {
751               order: sortDirection === 'desc' ? 'desc' : 'asc'
752             }
753             break;
754           default:
755             sort[sortColumn] = {
756               order: sortDirection === 'desc' ? 'desc' : 'asc'
757             }
758             break;
759         }
760       }
761
762       // determine the query parameter
763       var query = {};
764       if (filters == null || filters.length == 0) {
765         query["match_all"] = {};
766       } else {
767         var regexp = {};
768         filters.forEach(function (filter) {
769           if (filter && filter.field) {
770             switch (filter.field) {
771               case 'siteIdA':
772                 regexp['siteIdA'] = '.*' + filter.term + '.*';  
773                 break;  
774               case 'siteIdZ':
775                 regexp['siteZ'] = '.*' + filter.term + '.*';  
776                 break; 
777               default:
778                 regexp[filter.field] = '.*' + filter.term + '.*';
779                 break;
780             }
781           }
782         });
783         query["regexp"] = regexp;
784       }
785
786
787       if (sort) {
788         $mwtnDatabase.getFilteredSortedData("mwtn", "site-link", chunkSiteStartIndex, chunkSize, sort, query).then(processResult, resultDefer.reject);
789       } else {
790         $mwtnDatabase.getFilteredData("mwtn", "site-link", chunkSiteStartIndex, chunkSize, query).then(processResult, resultDefer.reject);
791       }
792
793       /**
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.
796          */
797       function processResult (result) {
798         var hits = result && result.data && result.data.hits;
799         if (!hits) {
800           resultDefer.reject("Invalid result.");
801           return;
802         }
803
804         var total = hits.total;
805         var links = [];
806
807         var siteLinks = hits.hits.map(function (siteLink) {
808           return {
809             id: siteLink._source.id,
810             siteA: siteLink._source.siteA,
811             siteZ: siteLink._source.siteZ,
812             type: siteLink._source.type,
813           };
814         });
815
816         resultDefer.resolve({
817           total: total,
818           links: siteLinks
819         });
820       }
821
822       return resultDefer.promise;
823
824       
825     };
826    
827     /**
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 
832      */
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 ;
837       
838       return coordinate.lat >= bounds.bottom && coordinate.lat <= bounds.top && isLongInRange;
839     } 
840
841     service.getAllEdges = function () {
842       var edges = [];
843       return getGenericChunk("edge", edges).then(function () {
844         return edges;
845       });
846     };
847
848     /**
849      * Retrieves all nodes.
850      * @return a Promis containing an array of nodes
851      */
852     service.getAllNodes = function () {
853       var resultDefer = $q.defer();
854       var nodes = [];
855       getGenericChunk("node", nodes).then(function () {
856
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)) {
863               acc.push({
864                 data: {
865                   type: "site",
866                   id: cur.data.grandparent,
867                   label: cur.data.grandparent,
868                   active: cur.data.active
869                 }
870               });
871             }
872             if (!acc.some(node => node.data.type == "device" && node.data.id == cur.data.parent)) {
873               acc.push({
874                 data: {
875                   _parent: cur.data.grandparent,
876                   type: "device",
877                   id: cur.data.parent,
878                   get parent() { return cur.data.grandparent },
879                   set parent(val) { debugger; },
880                   label: cur.data.parent,
881                   active: cur.data.active
882                 }
883               });
884             }
885             acc.push({
886               data: Object.keys(cur.data).reduce(function (obj, key) {
887                 if (key == 'grandparent') {
888                   obj['type'] = 'port'
889                 } else {
890                   cur.data.hasOwnProperty(key) && (obj[key] = cur.data[key]);
891                 }
892                 return obj;
893               }, {}),
894               position: cur.position
895             });
896             return acc;
897           }, []);
898
899         resultDefer.resolve(finalNodes);
900       }, function (err) {
901         console.error(err);
902         resultDefer.reject(err);
903       });
904       return resultDefer.promise;
905     }
906
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();
911
912         nodes.forEach(function (node) {
913           resultDefer = resultDefer.then(function () {
914             return $mwtnDatabase.genericRequest({
915               method: 'POST',
916               base: base.base,
917               index: base.index,
918               docType: 'node',
919               command: encodeURI(node.id) +'/_update',
920               data: { 
921                 "doc": {
922                   "position": node.position
923                 }
924               }
925             });
926           })
927         });
928         return resultDefer;
929       });
930     }
931
932     // Start to initialize google maps api and save the returned promise.
933     service.googleMapsApiPromise = initializeGoogleMapsApi();
934
935     return service;
936
937     // private helper functions of $mwtnTopology
938
939     function getGenericChunk(docType, target) {
940       var size = 30;
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 || {});
948             });
949             if (total > target.length) {
950               return getGenericChunk(docType, target);
951             }
952             return $q.resolve(true);
953           } else {
954             return $q.reject("Could not load " + docType + ".");
955           }
956       }, function (err) {
957         resultDefer.reject(err);
958       });
959
960       return resultDefer.promise;
961     }
962
963
964     /**
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.
967      */
968     function initializeGoogleMapsApi() {
969       var googleMapsApiDefer = $q.defer();
970       window.googleMapsApiLoadedEvent = new Event("googleMapsApiLoaded");
971
972       window.addEventListener("googleMapsApiLoaded", function (event) {
973
974         /**
975          * Calculates the bounds this map would display at a given zoom level.
976          *
977          * @member google.maps.Map
978          * @method boundsAt
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.
984          *
985          * @example
986          * var bounds = map.boundsAt(5); // same as map.boundsAt(5, map.getCenter(), map.getProjection(), map.getDiv());
987          */
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)));
999         }
1000
1001         googleMapsApiDefer.resolve();
1002       });
1003
1004       var head = document.getElementsByTagName('head')[0];
1005
1006       var callbackScript = document.createElement("script");
1007       callbackScript.appendChild(document.createTextNode("function googleMapsApiLoadedCallback() { window.dispatchEvent(window.googleMapsApiLoadedEvent); };"));
1008
1009       var googleScript = document.createElement('script');
1010       googleScript.src = "https://maps.googleapis.com/maps/api/js?key=AIzaSyBWyNNhRUhXxQpvR7i-Roh23PaWqi-kNdQ&callback=googleMapsApiLoadedCallback";
1011
1012       head.appendChild(callbackScript);
1013       head.appendChild(googleScript);
1014
1015       return googleMapsApiDefer.promise;
1016     }
1017
1018   });
1019
1020 });