f8aa303abd2fc78ca98076ea3749a19cb19054f9
[ccsdk/apps.git] / sdnr / wireless-transport / code-Carbon-SR1 / ux / mwtnTopology / mwtnTopology-module / src / main / resources / mwtnTopology / mwtnTopology.controller.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 if (typeof (Number.prototype.toRad) === "undefined") {
9   Number.prototype.toRadians = function () {
10     return this * Math.PI / 180;
11   }
12 }
13
14 if (Number.prototype.toDegrees === undefined) {
15   Number.prototype.toDegrees = function () {
16     return this * 180 / Math.PI;
17   };
18 }
19
20 // If array is empty, undefined is returned.  If not empty, the first element
21 // that evaluates to false is returned.  If no elements evaluate to false, the
22 // last element in the array is returned.
23 Array.prototype.and = function (defaultValue) {
24   for (var i = 0, len = this.length - 1; i < len && this[i]; i++);
25   return this.length ? this[i] : defaultValue;
26 };
27
28 // If array is empty, undefined is returned.  If not empty, the first element
29 // that evaluates to true is returned.  If no elements evaluate to true, the
30 // last element in the array is returned.
31 Array.prototype.or = function () {
32   for (var i = 0, len = this.length - 1; i < len && !this[i]; i++);
33   return this[i];
34 };
35
36 var eventsFabric = function () {
37   var topics = {};
38   var hOP = topics.hasOwnProperty;
39
40   return {
41     subscribe: function (topic, listener) {
42       // Create the topic's object if not yet created
43       if (!hOP.call(topics, topic)) topics[topic] = [];
44
45       // Add the listener to queue
46       var index = topics[topic].push(listener) - 1;
47
48       // Provide handle back for removal of topic
49       return {
50         remove: function () {
51           delete topics[topic][index];
52         }
53       };
54     },
55     publish: function (topic, info) {
56       // If the topic doesn't exist, or there's no listeners in queue, just leave
57       if (!hOP.call(topics, topic)) return;
58
59       // Cycle through topics queue, fire!
60       topics[topic].forEach(function (item) {
61         item(info != undefined ? info : {});
62       });
63     }
64   };
65 };
66
67 var lastOpenedInfoWindow = null;
68
69 /*********************************************************************************************/
70
71 /** @typedef  {{ id: string, siteA: string, siteZ: string, siteNameA: string, siteNameZ: string, airInterfaceLinks: {id: string, siteLink: string, radio: string, polarization: string }[]}} SiteLinkDetails */
72
73 define(['app/mwtnCommons/bower_components/lodash/dist/lodash',
74   'app/mwtnCommons/bower_components/cytoscape/dist/cytoscape',
75   'app/mwtnTopology/mwtnTopology.module',
76   'app/mwtnTopology/mwtnTopology.services',
77   'app/mwtnCommons/mwtnCommons.services'
78 ], function (_, cytoscape, mwtnTopologyApp) {
79
80   // module.exports = function () {
81   //   const mwtnTopologyApp = require('app/mwtnTopology/mwtnTopology.module');
82   //   const mwtnTopologyService = require('app/mwtnTopology/mwtnTopology.services');
83   //   const cytoscape = require('cytoscape');
84
85   // remove '_' from global scope but use it here as '_'.
86   _.noConflict();
87
88   /********************************************* Sites *************************/
89
90   mwtnTopologyApp.directive('mwtnTopologyMap', ["$timeout", "$q", "$mwtnTopology", function ($timeout, $q, $mwtnTopology) {
91     return {
92       restrict: 'E',
93       replace: true,
94       template: '<div style="position:realtive;width:100%;height:700px" ng-transclude=""></div>',
95       controller: function () {
96
97       },
98       transclude: true,
99       scope: {
100         manualMapBounds: "=?",
101         onBoundsChanged: "&?"
102       },
103       link: function (scope, element, attrs, ctrl) {
104
105         var mapApiReadyDefer = $q.defer();
106         var disbaleNotifications = true;
107         //var includeCenter = false;
108
109         var initialMapOptions = {
110           center: new google.maps.LatLng(0, 0),
111           mapTypeId: google.maps.MapTypeId.HYBRID,
112           panControl: false,
113           rotateControl: false,
114           streetViewControl: false,
115           tilt: 0,
116           zoom: 2,
117           minZoom: 2
118         };
119
120         // Wait at least one digest cycle before init the google map.
121         $timeout(function () {
122           ctrl.map = new google.maps.Map(element[0], initialMapOptions);
123
124           // Wait until google maps is fully intialized to attach nessessary event handler.
125           waitUntilGoogleMapsIsReady();
126         });
127
128         element.on("$destroy", function () {
129           if (scope.waitUntilGoogleMapsIsReadyTimeoutId) {
130             $timeout.cancel(scope.waitUntilGoogleMapsIsReadyTimeoutId);
131           }
132           if (scope.onDragEndListener) {
133             google.maps.event.removeListener(scope.onDragEndListener);
134             delete (scope.onDragEndListener);
135           }
136           if (scope.onZoomChangedListener) {
137             google.maps.event.removeListener(scope.onZoomChangedListener);
138             delete (scope.onZoomChangedListener);
139           }
140
141           // window.removeEventListener('resize', onBoundsChanged);
142
143         });
144
145         scope.$watch("manualMapBounds", function (newBounds, oldBounds) {
146           mapApiReadyDefer.promise.then(function () {
147
148             // if this was an uri change from an not user originated action just return;            
149             if (newBounds.internal) return;
150
151             // values must exist and at least one must be different or initial load (new === old)
152             if (newBounds.bottom != null && newBounds.left != null && newBounds.top != null && newBounds.right != null) {
153               fitBounds(ctrl.map, new google.maps.LatLngBounds(
154                 new google.maps.LatLng(newBounds.bottom, newBounds.left),
155                 new google.maps.LatLng(newBounds.top, newBounds.right)));
156             } else if (newBounds.lat != null && newBounds.lng != null && newBounds.zoom != null && (newBounds.lat != oldBounds.lat || newBounds.lng != oldBounds.lng || newBounds.zoom != oldBounds.zoom || newBounds === oldBounds)) {
157               disbaleNotifications = true;
158               google.maps.event.addListenerOnce(ctrl.map, 'bounds_changed', function (event) {
159                 disbaleNotifications = false;
160                 onBoundsChanged(false);
161
162               });
163               ctrl.map.setCenter({ lat: newBounds.lat, lng: newBounds.lng });
164               ctrl.map.setZoom(newBounds.zoom);
165
166             }
167             console.log("manualMapBounds", newBounds);
168           });
169         });
170
171         /**
172          * An asynchronous function that is called continuously every 100 milliseconds until the google maps control has finished its first complete draw.
173          * Then it sets up all event handlers and reports its bounds.
174          */
175         function waitUntilGoogleMapsIsReady() {
176           var getBoundsApi = ctrl.map.getBounds();
177           if (getBoundsApi) {
178             delete (scope.waitUntilGoogleMapsIsReadyTimeoutId);
179             // Change to dragend https://developers.google.com/maps/documentation/javascript/events?hl=de
180             scope.onDragEndListener = ctrl.map.addListener('dragend', function () {
181               onBoundsChanged(true);
182             });
183             scope.onZoomChangedListener = ctrl.map.addListener('zoom_changed', function () {
184               onBoundsChanged(true);
185             });
186             // window.addEventListener('resize', function () {
187             //   onBoundsChanged(false);
188             // });
189
190             disbaleNotifications = false;
191             mapApiReadyDefer.resolve();
192           } else {
193             scope.waitUntilGoogleMapsIsReadyTimeoutId = $timeout(waitUntilGoogleMapsIsReady, 100);
194           }
195         }
196
197         /**
198          * Gets the current bound and zoom level of the map control and reports it to its parent component.
199          * Hint: do not call directly use fitBounds helper function since there is a google bug in the fit bounds function.
200          */
201         function onBoundsChanged(userOriginated) {
202           if (!disbaleNotifications && scope.onBoundsChanged && angular.isFunction(scope.onBoundsChanged)) {
203
204             var getBoundsApi = ctrl.map.getBounds();
205             var northEastApi = getBoundsApi.getNorthEast();
206             var southWestApi = getBoundsApi.getSouthWest();
207             var center = ctrl.map.getCenter();
208
209             var data = {
210               top: northEastApi.lat(),
211               right: northEastApi.lng(),
212               bottom: southWestApi.lat(),
213               left: southWestApi.lng(),
214               lat: center.lat(),
215               lng: center.lng(),
216               zoom: ctrl.map.getZoom()
217             };
218
219             console.log("onBoundsChanged", data);
220
221             scope.onBoundsChanged({
222               data: data,
223               userOriginated: userOriginated === true
224             });
225           }
226         }
227
228         // bug: the original google code will fit bounds will add some extra space, this function will fix it
229         function fitBounds(map, bounds) {
230           var oldBounds = map.getBounds();
231
232           // ensure there is any change
233           if (oldBounds.toUrlValue() == bounds.toUrlValue()) {
234             return;
235           }
236
237           disbaleNotifications = true;
238           google.maps.event.addListenerOnce(map, 'bounds_changed', function (event) {
239             // bugfix: this is a fix for the https://issuetracker.google.com/issues/35820423 
240             //         the map does sometime zoom out if it gets its one bounds
241
242             // var newSpan = map.getBounds().toSpan();              // the span of the map set by Google fitBounds (always larger by what we ask)
243             // var askedSpan = bounds.toSpan();                     // the span of what we asked for
244             // var latRatio = (newSpan.lat() / askedSpan.lat()) - 1;  // the % of increase on the latitude
245             // var lngRatio = (newSpan.lng() / askedSpan.lng()) - 1;  // the % of increase on the longitude
246             // // if the % of increase is too big (> to a threshold) we zoom in
247             // if (Math.min(latRatio, lngRatio) > 0.46) {
248             //   // 0.46 is the threshold value for zoming in. It has been established empirically by trying different values.
249             //   this.setZoom(this.getZoom() + 1);
250             // }
251             disbaleNotifications = false;
252             onBoundsChanged(false);
253
254           });
255           map.fitBounds(bounds); // does the job asynchronously
256         }
257       }
258     };
259   }]);
260
261   mwtnTopologyApp.directive('mwtnTopologyMapSites', ['$compile', '$rootScope', function ($compile, $rootScope) {
262
263     return {
264       restrict: 'E',
265       require: '^mwtnTopologyMap',
266       scope: {
267         sites: "=",
268         selectedSite: "=",
269         api: "="
270       },
271       link: function (scope, element, attrs, mwtnTopologyMapController) {
272         // todo: shouild come from a service
273         var normalIcon = {
274           path: google.maps.SymbolPath.CIRCLE,
275           scale: 10,
276           strokeColor: '#00ccff',
277           strokeOpacity: 0.8,
278           strokeWeight: 2,
279           fillColor: '#00ccff',
280           fillOpacity: 0.35,
281           zIndex: 2
282         };
283
284         var highlightIcon = {
285           path: google.maps.SymbolPath.CIRCLE,
286           scale: 10,
287           strokeColor: '#eaae3c',
288           strokeOpacity: 0.8,
289           strokeWeight: 3,
290           fillColor: '#eaae3c',
291           fillOpacity: 0.65,
292           zIndex: 2
293         };
294
295         var selectedIcon = {
296           path: google.maps.SymbolPath.CIRCLE,
297           scale: 10,
298           strokeColor: '#00FD30',
299           strokeOpacity: 0.8,
300           strokeWeight: 2,
301           fillColor: '#00FD30',
302           fillOpacity: 0.35,
303           zIndex: 2
304         };
305
306         scope.displayedSites = {};
307         scope.knownSites = {};
308
309         scope.$watch("selectedSite", function (newSiteId, oldSiteId) {
310
311           if (oldSiteId && scope.displayedSites[oldSiteId]) {
312             scope.displayedSites[oldSiteId].setOptions({
313               icon: normalIcon
314             });
315           }
316
317           if (newSiteId && scope.displayedSites[newSiteId]) {
318             scope.displayedSites[newSiteId].setOptions({
319               icon: selectedIcon
320             });
321           }
322
323         });
324
325         if (!scope.api) return;
326
327         /**
328          * Updates the google map markers
329          * @param addedSiteIds {string[]} The ids of the sites which are added to the scope.displayedSites dictionary.
330          * @param removedFromVisibleIds {string[]} The ids of sites which are removed from the visible bounding rectangle.
331          * @param movedFromVisibleToKnownIds {string[]} The ids of sites which are moved from the visible bounding rectangle to the extended bounding rectangle.
332          * @param removedFromKnownIds {string[]} The ids of sites which are removed from the extended bounding rectangle.
333          * @param movedFromKnownToVisibleIds {string[]} The ids of sites which are moved from the extended bounding rectangle to the visible bounding rectangle.
334          */
335         scope.api.updateSites = function (addedSiteIds, removedFromVisibleIds, movedFromVisibleToKnownIds, removedFromKnownIds, movedFromKnownToVisibleIds) {
336           if (!addedSiteIds) addedSiteIds = [];
337           if (!removedFromVisibleIds) removedFromVisibleIds = [];
338           if (!movedFromVisibleToKnownIds) movedFromVisibleToKnownIds = [];
339           if (!removedFromKnownIds) removedFromKnownIds = [];
340           if (!movedFromKnownToVisibleIds) movedFromKnownToVisibleIds = [];
341
342           removedFromVisibleIds.forEach(function (siteId) {
343             var marker = scope.displayedSites[siteId];
344             if (marker) {
345               marker.setMap(null);
346               //delete marker;
347               delete scope.displayedSites[siteId];
348             }
349           });
350
351           removedFromKnownIds.forEach(function (siteId) {
352             var marker = scope.knownSites[siteId];
353             if (marker) {
354               marker.setMap(null);
355               //delete marker;
356               delete scope.knownSites[siteId];
357             }
358           });
359
360           movedFromVisibleToKnownIds.forEach(function (siteId) {
361             var marker = scope.displayedSites[siteId];
362             if (marker) {
363               marker.setVisible(false);
364               scope.knownSites[siteId] = marker;
365               delete scope.displayedSites[siteId];
366             }
367           });
368
369           movedFromKnownToVisibleIds.forEach(function (siteId) {
370             var marker = scope.knownSites[siteId];
371             if (marker) {
372               marker.setVisible(true);
373               scope.displayedSites[siteId] = marker;
374               delete scope.knownSites[siteId];
375             }
376           });
377
378           addedSiteIds.forEach(function (newSiteId) {
379             var newSite = scope.sites[newSiteId];
380             var newSiteNormalIcon = Object.assign({}, normalIcon, {
381               fillColor: (newSite.type === 'data-center') ? "#ffff00": "#00ccff",
382               strokeColor: (newSite.type === 'data-center') ? "#ffff00" : "#00ccff"
383             });
384             var newSiteHighlightIcon = Object.assign({}, highlightIcon, { 
385               fillColor: (newSite.type === 'data-center') ? "#ffff00": "#00ccff",
386               strokeColor: (newSite.type === 'data-center') ? "#ffff00" : "#00ccff"
387              });
388             if (newSite && !scope.displayedSites[newSiteId]) {
389
390               // create the marker
391               var marker = new google.maps.Marker({
392                 map: mwtnTopologyMapController.map,
393                 position: newSite.location,
394                 title: newSite.name,
395                 icon: newSiteNormalIcon
396               });
397
398               // add event listeners to the marker
399               marker.addListener('click', function () {
400                 // cloase all already opened windows
401                 if (lastOpenedInfoWindow) {
402                   lastOpenedInfoWindow.close();
403                   lastOpenedInfoWindow = null;
404                 }
405
406                 // compile the content
407                 var infoWindowTemplate = '<mwtn-topology-site-details site-id="siteId" />';
408                 var infoWindowScope = $rootScope.$new();
409                 infoWindowScope['siteId'] = newSiteId;
410                 var infoWindowContent = $compile(infoWindowTemplate)(infoWindowScope)[0];
411
412                 // create the info window
413                 var infowindow = new google.maps.InfoWindow({
414                   content: infoWindowContent,
415                   disableAutoPan: true
416                 });
417
418                 // open the window and keek a refenrece to close it if new window opens               
419                 infowindow.open(mwtnTopologyMapController.map, marker);
420                 lastOpenedInfoWindow = infowindow;
421
422                 // remove the reference if the windows is closed                
423                 infowindow.addListener('closeclick', function () {
424                   lastOpenedInfoWindow = null;
425                 });
426               });
427
428               marker.addListener('mouseover', function () {
429                 marker.setOptions({ icon: newSiteHighlightIcon });
430               });
431
432               marker.addListener('mouseout', function () {
433                 marker.setOptions({ icon: newSiteNormalIcon });
434               });
435
436               // store marker
437               scope.displayedSites[newSiteId] = marker;
438             }
439           });
440         };
441
442       }
443     }
444   }]);
445
446   mwtnTopologyApp.directive('mwtnTopologyMapSiteLinks', ['$compile', '$rootScope', function ($compile, $rootScope) {
447     return {
448       restrict: 'E',
449       require: '^mwtnTopologyMap',
450       scope: {
451         selectedSiteLink: "=",
452         siteLinks: "=",
453         api: "="
454       },
455       link: function (scope, element, attrs, mwtnTopologyMapController) {
456
457         var normalColor = "#FF0000";
458         var selectedColor = "#00FD30";
459
460         scope.$watch("selectedSiteLink", function (newSiteLinkId, oldSiteLinkId) {
461
462           if (oldSiteLinkId && scope.displayedSiteLinks[oldSiteLinkId]) {
463             scope.displayedSiteLinks[oldSiteLinkId].setOptions({
464               strokeColor: normalColor,
465             });
466           }
467
468           if (newSiteLinkId && scope.displayedSiteLinks[newSiteLinkId]) {
469             scope.displayedSiteLinks[newSiteLinkId].setOptions({
470               strokeColor: selectedColor,
471             });
472           }
473
474         });
475
476         scope.displayedSiteLinks = {};
477
478         /**
479          * Updates the google map Polylines
480          * @param addedSiteLinkIds {string[]} The ids of sites links which are added to the scope.displayedSiteLinks dictionary and the google map.
481          * @param removedSiteLinkIds {string[]} The ids of sites links which are removed from the visible bounding rectangle within the google map.
482          */
483         scope.api.updateSiteLinks = function (addedSiteLinkIds, removedSiteLinkIds) {
484           if (!addedSiteLinkIds) addedSiteLinkIds = [];
485           if (!removedSiteLinkIds) removedSiteLinkIds = [];
486
487           removedSiteLinkIds.forEach(function (siteLinkId) {
488             var polyline = scope.displayedSiteLinks[siteLinkId];
489             if (polyline) {
490               polyline.setMap(null);
491               //delete polyline;
492               delete scope.displayedSiteLinks[siteLinkId];
493             }
494           });
495
496           addedSiteLinkIds.forEach(function (siteLinkId) {
497             var siteLink = scope.siteLinks[siteLinkId];
498             console.log(siteLink);
499             
500             if (siteLink && !scope.displayedSiteLinks[siteLinkId]) {
501
502               // create the poly line
503               var polyline = new google.maps.Polyline({
504                 map: mwtnTopologyMapController.map,
505                 path: [siteLink.siteA.location, siteLink.siteZ.location],
506                 strokeColor: siteLink.type === 'traffic' ? '#00ccff': '#ffff00',
507                 strokeOpacity: 0.8,
508                 strokeWeight: siteLink.type === 'traffic' ? 4 : 2,
509                 zIndex: 1,
510                 geodesic: true
511               });
512               console.log(siteLink.type, polyline.strokeColor);
513               
514               // add event listeners to the polyline
515               polyline.addListener('click', function (event) {
516                 // cloase all already opened windows
517                 if (lastOpenedInfoWindow) {
518                   lastOpenedInfoWindow.close();
519                   lastOpenedInfoWindow = null;
520                 }
521
522                 // compile the content
523                 var infoWindowTemplate = '<mwtn-topology-link-details link-id="linkId"></mwtn-topology-link-details>';
524                 var infoWindowScope = $rootScope.$new();
525                 infoWindowScope['linkId'] = siteLinkId;
526                 var infoWindowContent = $compile(infoWindowTemplate)(infoWindowScope)[0];
527
528                 // create the info window
529                 var infowindow = new google.maps.InfoWindow({
530                   content: infoWindowContent,
531                   disableAutoPan: true
532                 });
533
534                 // open the window and keek a refenrece to close it if new window opens               
535                 // adjust and show the info window
536                 infowindow.setPosition(event.latLng);
537                 infowindow.open(mwtnTopologyMapController.map);
538                 lastOpenedInfoWindow = infowindow;
539
540                 // remove the reference if the windows is closed                
541                 infowindow.addListener('closeclick', function () {
542                   lastOpenedInfoWindow = null;
543                 });
544
545               });
546
547               polyline.addListener('mouseover', function () {
548                 polyline.setOptions({ strokeWeight: 6 });
549               });
550
551               polyline.addListener('mouseout', function () {
552                 polyline.setOptions({ strokeWeight: 3 });
553               });
554
555               // store the ployline              
556               scope.displayedSiteLinks[siteLinkId] = polyline;
557             }
558           });
559         };
560
561       }
562     };
563   }]);
564
565   mwtnTopologyApp.controller('mwtnTopologySiteDetailsController', ['$scope', '$timeout', '$mwtnTopology', function ($scope, $timeout, $mwtnTopology) {
566     var vm = this;
567     vm.site = null;
568
569     $scope.$watch(function () { return vm.siteId }, function (newSiteId, oldSiteId) {
570       if (!newSiteId) return;
571
572       vm.status = {
573         message: 'Searching...',
574         type: 'warning',
575         isWorking: true
576       };
577
578       $mwtnTopology.getSiteDetailsBySiteId(newSiteId).then(function (siteDetails) {
579         vm.site = siteDetails;
580
581         vm.status = {
582           message: undefined,
583           type: 'success',
584           isWorking: false
585         };
586       }, function (err) {
587         vm.status = {
588           message: err,
589           type: 'error',
590           isWorking: false
591         };
592       });
593     });
594
595   }]);
596
597   mwtnTopologyApp.directive('mwtnTopologySiteDetails', function () {
598     return {
599       scope: { siteId: "=" },
600       restrict: 'E',
601       controller: 'mwtnTopologySiteDetailsController',
602       controllerAs: 'vm',
603       bindToController: true,
604       templateUrl: 'src/app/mwtnTopology/templates/siteDetails.tpl.html'
605     };
606   });
607
608   mwtnTopologyApp.controller('mwtnTopologyLinkDetailsController', ['$scope', '$timeout', '$mwtnTopology', function ($scope, $timeout, $mwtnTopology) {
609     /** @type {{ link: SiteLinkDetails } & {[key: string]: any}} */
610     var vm = this;
611     vm.site = null;
612     vm.link = null;
613
614     $scope.$watch(function () { return vm.linkId }, function (newLinkId, oldLinkId) {
615       if (!newLinkId) return;
616
617       vm.status = {
618         message: 'Searching...',
619         type: 'warning',
620         isWorking: true
621       };
622
623       // getLinkDetailsByLinkId
624       $mwtnTopology.getLinkDetailsByLinkId(newLinkId).then(
625         /** @param {SiteLinkDetails} linkDetails*/
626         function (linkDetails) {
627           vm.link = linkDetails;
628
629           vm.status = {
630             message: undefined,
631             type: 'success',
632             isWorking: false
633           };
634         }, function (err) {
635           vm.status = {
636             message: err,
637             type: 'error',
638             isWorking: false
639           };
640         });
641     });
642
643   }]);
644
645   mwtnTopologyApp.directive('mwtnTopologyLinkDetails', function () {
646     return {
647       scope: { linkId: "=" },
648       restrict: 'E',
649       controller: 'mwtnTopologyLinkDetailsController',
650       controllerAs: 'vm',
651       bindToController: true,
652       templateUrl: 'src/app/mwtnTopology/templates/linkDetails.tpl.html'
653     };
654   });
655
656   mwtnTopologyApp.controller('mwtnTopologyFrameController', ['$scope', '$rootScope', '$timeout', '$state', '$window', '$mwtnTopology', function ($scope, $rootScope, $timeout, $state, $window, $mwtnTopology) {
657     var vm = this;
658
659     $rootScope.section_logo = 'src/app/mwtnTopology/images/mwtnTopology.png'; // Add your topbar logo location here such as 'assets/images/logo_topology.gif'
660  
661     /** @type {{ [tabName: string] : { [parameterName : string] : any } }} */
662     var tabParameters = {};
663
664     var tabs;
665     (function (tabs) {
666       tabs[tabs["site"] = 0] = "site";
667       tabs[tabs["physical"] = 1] = "physical";
668       tabs[tabs["ethernet"] = 2] = "ethernet";
669       tabs[tabs["ieee1588"] = 3] = "ieee1588";
670     })(tabs || (tabs = {}));
671
672     $scope.$watch(function () { return vm.activeTab; }, function (newVal, oldVal, scope) {
673       if (newVal == null) return;
674
675       var newName = tabs[newVal];
676
677       if (oldVal != null && tabs[oldVal]) {
678         var oldName = tabs[oldVal];
679         tabParameters[oldName] = $state.params;
680       }
681
682       var newParameters = Object.keys($state.params).reduce(function (acc, cur, ind, arr) { acc[cur] = null; return acc; }, {});
683       Object.assign(newParameters, tabParameters[newName] || {}, { tab: newName, internal: false });
684
685       $state.go('main.mwtnTopology', newParameters, { notify: false });
686       console.log("activeTab: ", newName);
687     });
688
689     $scope.$watch(function () { return $state.params.tab; }, function (newVal, oldVal, scope) {
690       if (newVal == oldVal) return;
691       if (newVal && !$state.params.internal) {
692         vm.activeTab = tabs[newVal || "site"];
693         console.log("navigationTab: ", vm.activeTab);
694       }
695     });
696
697     // hide all tabs until the google api is fully loaded.
698     // initialize with true is the promise is resolved === 1
699     vm.googleIsReady = $mwtnTopology.googleMapsApiPromise.$$state.status === 1;
700
701     if (!vm.googleIsReady) $mwtnTopology.googleMapsApiPromise.then(function () {
702       $timeout(function () {
703         vm.googleIsReady = true;
704       });
705     });
706
707   }]);
708
709   mwtnTopologyApp.directive('mwtnTopologyFrame', function () {
710     return {
711       restrict: 'E',
712       controller: 'mwtnTopologyFrameController',
713       controllerAs: 'vm',
714       templateUrl: 'src/app/mwtnTopology/templates/frame.tpl.html'
715     };
716   });
717
718   mwtnTopologyApp.controller('mwtnTopologySiteViewController', ['$scope', '$q', '$timeout', '$state', '$window', '$mwtnTopology', function ($scope, $q, $timeout, $state, $window, $mwtnTopology) {
719     var vm = this;
720     // Determines if a database request loop should be canceld.
721     var cancelRequested = false;
722     // Represents a defer which is resolved, if a cancel was requested and fulfilled.
723     var cancelDefer = $q.defer();
724     // Represents the new bounding rectangle who was set with the last user action to change the map bounds.
725     // The value is saved until the cancel operation is fulfilled.
726     /** @type {{top: number, right: number, bottom: number, left: number, zoom: number}} */
727     var waitForCancelMapBoundAndZoom;
728     // While getting all sites collect all site link ids which are available through visible and known sites.
729     // This dictionary will be cleared each time a new map bound is changed and a new database request loop is started.
730     var collectedSiteLinkIds = {};
731
732     vm.status = {
733       // Determines the bounds of the map which is manually set by the code. These are the initial values.
734       manualMapBounds: {
735         top: !Number.isNaN(+$state.params.top) ? +$state.params.top : undefined,
736         bottom: !Number.isNaN(+$state.params.bottom) ? +$state.params.bottom : undefined,
737         left: !Number.isNaN(+$state.params.left) ? +$state.params.left : undefined,
738         right: !Number.isNaN(+$state.params.right) ? +$state.params.right : undefined,
739         zoom: !Number.isNaN(+$state.params.zoom) ? +$state.params.zoom : undefined,
740         lat: !Number.isNaN(+$state.params.lat) ? +$state.params.lat : undefined,
741         lng: !Number.isNaN(+$state.params.lng) ? +$state.params.lng : undefined
742       },
743
744       // Determines if the accordeon group element for SiteMap, SiteGrid or SiteLinkGrid is open.
745       siteMapIsOpen: true,
746       siteGridIsOpen: true,
747       siteLinkGridIsOpen: true,
748       sitePathGridIsOpen: false,
749
750       // Determines if a processing is running.
751       processing: false,
752       totalSitesInBoundingBox: 0,
753       loadedSitesInBoundingBox: 0,
754       maximumCountOfVisibleSites: 300,
755
756       // selectet Elements
757       site: null,
758       siteLink: null,
759       sitePath: null
760     };
761
762     // Contains the known and visible sites as well as known site links as dictionaries.
763     vm.knownSites = {};
764     vm.visibleSites = {};
765     vm.knownSiteLinks = {};
766
767     // Api-Object that will be filled with update methods from the mwtnTopologyMapSites Component.
768     vm.mapSitesComponentApi = {};
769     // Api-Object that will be filled with update methods from the mwtnTopologyMapSiteLinks Component.
770     vm.mapSiteLinksComponentApi = {};
771
772     //addedSiteLinkIds, removedSiteLinkIds
773
774     initializeMapBounds();
775
776     function initializeMapBounds() {
777
778       vm.status.manualMapBounds = {
779         top: !Number.isNaN(+$state.params.top) ? +$state.params.top : undefined,
780         bottom: !Number.isNaN(+$state.params.bottom) ? +$state.params.bottom : undefined,
781         left: !Number.isNaN(+$state.params.left) ? +$state.params.left : undefined,
782         right: !Number.isNaN(+$state.params.right) ? +$state.params.right : undefined,
783         zoom: !Number.isNaN(+$state.params.zoom) ? +$state.params.zoom : undefined,
784         lat: !Number.isNaN(+$state.params.lat) ? +$state.params.lat : undefined,
785         lng: !Number.isNaN(+$state.params.lng) ? +$state.params.lng : undefined
786       };
787
788       // Calculate the initial map control bound, if we have no initial bounds 
789       if ((vm.status.manualMapBounds.top == null && vm.status.manualMapBounds.bottom == null && vm.status.manualMapBounds.right == null && vm.status.manualMapBounds.left == null && vm.status.manualMapBounds.lat == null && vm.status.manualMapBounds.lng == null && $state.params.site == null && $state.params.siteLink == null && $state.params.sitePath == null)) {
790         $mwtnTopology.getOuterBoundingRectangleForSites().then(function (bounds) {
791           // Ensures that the new value is set within a digest cycle. 
792           vm.status.manualMapBounds = bounds;
793         });
794       }
795     }
796
797     $scope.$watch(function () { return $state.params.site; }, function (newVal, oldVal, scope) {
798       if (newVal) {
799         $mwtnTopology.getSiteDetailsBySiteId(newVal).then(function (siteDetails) {
800           if (siteDetails) {
801             // just update the location of the browser if the location has changed      
802             $state.go('main.mwtnTopology', {
803               tab: "site",
804               lat: siteDetails.location.lat,
805               lng: siteDetails.location.lng,
806               zoom: 19,
807               top: null,
808               bottom: null,
809               right: null,
810               left: null,
811               site: $state.params.site,
812               siteLink: null,
813               sitePath: null,
814               internal: false
815             }, {
816                 notify: false
817               });
818           };
819         }, function (err) {
820
821         });
822       }
823     });
824
825     $scope.$watch(function () { return $state.params.siteLink; }, function (newVal, oldVal, scope) {
826       if (newVal) {
827         $mwtnTopology.getLinkDetailsByLinkId(newVal).then(function (link) {
828           if (link) {
829
830             var top = link.siteA.location.lat >= link.siteZ.location.lat ? link.siteA.location.lat : link.siteZ.location.lat;
831             var bottom = link.siteA.location.lat >= link.siteZ.location.lat ? link.siteZ.location.lat : link.siteA.location.lat;
832
833             // todo: this code is not able to handle links which overlap the -100|180 ° borderline
834             var left = link.siteA.location.lng <= link.siteZ.location.lng ? link.siteA.location.lng : link.siteZ.location.lng;
835             var right = link.siteA.location.lng <= link.siteZ.location.lng ? link.siteZ.location.lng : link.siteA.location.lng;
836             // just update the location of the browser if the location has changed      
837             $state.go('main.mwtnTopology', {
838               tab: "site",
839               lat: null,
840               lng: null,
841               zoom: $state.params.zoom,
842               top: top,
843               bottom: bottom,
844               right: right,
845               left: left,
846               site: null,
847               siteLink: $state.params.siteLink,
848               sitePath: null,
849               internal: false
850             }, {
851                 notify: false
852               });
853           };
854         }, function (err) {
855
856         });
857       }
858     });
859
860     $scope.$watchCollection(function () { return [vm.status.siteMapIsOpen, vm.status.siteGridIsOpen, vm.status.siteLinkGridIsOpen, vm.status.sitePathGridIsOpen] }, function (newVal, oldVal) {
861       if (newVal[1] || newVal[2] || newVal[3]) {
862         $timeout(function () {
863           $window.dispatchEvent(new Event("resize"));
864         });
865       }
866     });
867
868     $scope.$watchCollection(function () { return [+$state.params.top, +$state.params.bottom, +$state.params.left, +$state.params.right, +$state.params.lat, +$state.params.lng, +$state.params.zoom]; }, function (newVal, oldVal, scope) {
869
870       if (oldVal == null || oldVal === newVal) return; // ignore initial value
871
872       if (!vm.status.siteMapIsOpen) {
873         console.log("open Site Map")
874         vm.status.siteMapIsOpen = true;
875         return;
876       }
877
878       vm.status.manualMapBounds = {
879         top: !Number.isNaN(newVal[0]) ? newVal[0] : undefined,
880         bottom: !Number.isNaN(newVal[1]) ? newVal[1] : undefined,
881         left: !Number.isNaN(newVal[2]) ? newVal[2] : undefined,
882         right: !Number.isNaN(newVal[3]) ? newVal[3] : undefined,
883         zoom: !Number.isNaN(newVal[6]) ? newVal[6] : undefined,
884         internal: !!$state.params.internal
885       };
886
887       if (!Number.isNaN(newVal[4]) && !Number.isNaN(newVal[5])) {
888         vm.status.manualMapBounds.lat = newVal[4];
889         vm.status.manualMapBounds.lng = newVal[5];
890       } else {
891         vm.status.manualMapBounds.lat = undefined;
892         vm.status.manualMapBounds.lng = undefined;
893       }
894
895       if (!!$state.params.internal) $state.params.internal = false;
896
897     });
898
899     $scope.$watchCollection(function () { return vm.status.siteMapIsOpen }, function (newVal, oldVal, scope) {
900       if (oldVal == newVal) return; // ignore initial value
901       if (newVal) {
902         // Map (re)-opend
903         initializeMapBounds();
904       } else {
905         // Map closed
906         vm.status.loadedSitesInBoundingBox = 0;
907         vm.status.totalSitesInBoundingBox = 0;
908         vm.status.manualMapBounds = {};
909         vm.knownSites = {};
910         vm.visibleSites = {};
911         vm.knownSiteLinks = {};
912       };
913       // tell all sub components the visuability of the map view
914       $scope.$broadcast('mapViewVisuability', newVal);
915     });
916
917     /* callback and helper methods */
918     /**
919      * A callback function which is called by the map control of the user changed the bounds of the map.
920      * @param mapBoundsAndZoom {{top: number, right: number, bottom: number, left: number, zoom: number}} The new bounds and the new zoom level of the map control.
921      */
922     vm.mapBoundsChanged = function (mapBoundsAndZoom, userOriginated) {
923
924       // just update the location of the browser if the location has changed      
925       $state.go('main.mwtnTopology', {
926         tab: "site",
927         lat: mapBoundsAndZoom.lat,
928         lng: mapBoundsAndZoom.lng,
929         zoom: mapBoundsAndZoom.zoom,
930         top: null,
931         bottom: null,
932         right: null,
933         left: null,
934         site: !userOriginated ? $state.params.site : null,
935         siteLink: !userOriginated ? $state.params.siteLink : null,
936         sitePath: !userOriginated ? $state.params.sitePath : null,
937         internal: true
938       }, {
939           notify: false
940         });
941       console.log("handleBoundsChanged", mapBoundsAndZoom);
942
943       if (vm.status.siteMapIsOpen) refreshAllSites(mapBoundsAndZoom);
944     };
945
946     /**
947      * Refreshes all visible elements in the given view port
948      * @param mapBoundsAndZoom {{top: number, right: number, bottom: number, left: number, zoom: number}} The new bounds and the new zoom level of the map control.
949      */
950     function refreshAllSites(mapBoundsAndZoom) {
951
952       // Datenbankabfragen werden werden in kleine Chunks zerlegt.
953       // Jeder Chunk enthält maximal ${chunkSize} (100) Sites und die Anzahl aller Sites für die spezifische Abfrage in der Antwort.
954       // Sind noch nicht alle Chunks von der Datenbank angefordert worden, wird zuerst der nächste Chunk angefordert.
955       // Der aktuell zurückgegebene Chunk wird aber erst verarbeitet, bevor in einer weiteren Callback-Situation der nächste Datenbank-Chunk abgeholt wird.
956       // Dadurch werden kontinuierlich alle Sites auch auf der Map ergänzt, sobald sie verarbeitet wurden und es kommt zu keiner Verzögerung im Benutzerinterface.
957       // Im Maximum werden aber ${vm.status.maximumCountOfVisibleSites} (2500) Sites abgerufen, um die Google Map nicht zu überlasten.
958
959       // Fallunterscheidung: gibt es bereits eine aktive Datenbankabfrage zu den Sites?
960       // => Nein: Starte eine neue Datenbankabfrage
961       // => Ja: Breche die aktuelle Verarbeitung ab, aktualisiere den Status und starte dann eine neue Datenbankabfrage.
962       if (vm.status.processing) {
963         waitForCancelMapBoundAndZoom = mapBoundsAndZoom;
964
965         if (cancelRequested) {
966           console.log("Cancel is already requested.");
967           return;
968         }
969
970         // Request a database loop cancel.
971         cancelRequested = true;
972         console.log("New Cancel requested.")
973
974         // Wait until the current database loos was canceled.
975         cancelDefer.promise.then(function () {
976           // The database loop is canceled. Create a new defer for the next possible cancel request.
977           console.log("Cancel fulfilled.");
978           cancelDefer = $q.defer();
979           // Restart the database loop with the new bounding rectangle.
980           beginProcessing(waitForCancelMapBoundAndZoom);
981         });
982
983         // Do not start a database loop here - so leave the method.
984         return;
985       }
986
987       // Start a new database loop here.
988       beginProcessing(mapBoundsAndZoom);
989
990       /**
991        * The private function which begins a new database processing loop.
992        * @param mapBoundsAndZoom {{top: number, right: number, bottom: number, left: number, zoom: number}} The new bounds and the new zoom level of the map control.
993        */
994       function beginProcessing(mapBoundsAndZoom) {
995         // Set the processing flag to true.
996         vm.status.processing = true;
997
998         var chunkSize = 100;
999         var chunkSiteStartIndex = 0;
1000
1001         // If a site link cannot convert its siteA or siteZ (id) Parameter to a real site object these ids will collected.
1002         // After a subsequent database call to get these sites the queued site links will be converted.
1003         var additionalSiteIds = [];
1004         var queuedSiteLinksToConvert = [];
1005
1006         // Entferne Sites, welche aufgrund des neuen Bounds leicht außer Sicht sind von den sichtbaren Sites.
1007         // Hinzufügen von Sites aus den leicht außer Sicht Bereich zum sichtbaren Bereich.
1008         var movedOrRemovedSites = refreshOutOfSightSites(mapBoundsAndZoom);
1009         var removedFromVisibleIds = movedOrRemovedSites.removedFromVisibleIds;
1010         var movedFromVisibleToKnownIds = movedOrRemovedSites.movedFromVisibleToKnownIds;
1011         var removedFromKnownIds = movedOrRemovedSites.removedFromKnownIds;
1012         var movedFromKnownToVisibleIds = movedOrRemovedSites.movedFromKnownToVisibleIds;
1013         var removedSiteLinkIds = movedOrRemovedSites.removedSiteLinkIds;
1014
1015         // While getting all sites collect all site link ids which are available through visible and known sites.
1016         collectedSiteLinkIds = {};
1017
1018         doSiteRequestLoop();
1019
1020         /* internal function within beginProcessing */
1021         /**
1022          * Starts a new database request loop to get all sites.
1023          */
1024         function doSiteRequestLoop() {
1025           requestNextSiteChunk(mapBoundsAndZoom, chunkSize, chunkSiteStartIndex).then(
1026             /**
1027              * @param result {{addedSiteIds: string[], total: number}} An array with the ids of the sites which are added to the vm.visibleSites dictionary.
1028              */
1029             function (result) {
1030               // update the frontend before continue with the next database request.
1031               vm.mapSitesComponentApi.updateSites && vm.mapSitesComponentApi.updateSites(result.addedSiteIds, removedFromVisibleIds, movedFromVisibleToKnownIds, removedFromKnownIds, movedFromKnownToVisibleIds);
1032               removedSiteLinkIds && removedSiteLinkIds.length && vm.mapSiteLinksComponentApi.updateSiteLinks && vm.mapSiteLinksComponentApi.updateSiteLinks([], removedSiteLinkIds);
1033               // to not re-remove and re-move already removed and moved sites clear the arrays for any subsequent database requests.
1034               removedFromVisibleIds = [];
1035               movedFromVisibleToKnownIds = [];
1036               removedFromKnownIds = [];
1037               movedFromKnownToVisibleIds = [];
1038               removedSiteLinkIds = [];
1039
1040               if (cancelRequested) {
1041                 // reset the cancelRequested flag and resolve the cancel request.
1042                 cancelRequested = false;
1043                 cancelDefer.resolve();
1044                 return;
1045               }
1046
1047               // Enforce angular to process a new digest cycle.
1048               $timeout(function () {
1049                 vm.status.totalSitesInBoundingBox = result.total;
1050                 vm.status.loadedSitesInBoundingBox = Math.min(chunkSiteStartIndex + chunkSize, result.total);
1051                 if (chunkSiteStartIndex + chunkSize >= result.total || chunkSiteStartIndex + chunkSize >= vm.status.maximumCountOfVisibleSites) {
1052                   // This was the last database request to get sites.
1053                   // Remove all known site link ids from the collectedSiteLinkIds dictionary.
1054                   var knownSiteLinkIds = Object.keys(vm.knownSiteLinks);
1055                   knownSiteLinkIds.forEach(function (knownSiteLinkId) {
1056                     if (collectedSiteLinkIds[knownSiteLinkId]) {
1057                       delete collectedSiteLinkIds[knownSiteLinkId];
1058                     }
1059                   });
1060
1061                   // Start the request loop for site links now.
1062                   doSiteLinkRequestLoop(Object.keys(collectedSiteLinkIds));
1063                 } else {
1064                   // There are more data, request the database again.
1065                   chunkSiteStartIndex += chunkSize;
1066
1067                   doSiteRequestLoop();
1068                 }
1069               });
1070
1071             }, processError);
1072         }
1073
1074         function endProcessing() {
1075           vm.status.processing = false;
1076
1077           $timeout(function () {
1078             vm.status.site = $state.params.site;
1079             vm.status.siteLink = $state.params.siteLink;
1080           });
1081         }
1082
1083         /**
1084          * Starts a new database request loop to get all site links.
1085          */
1086         function doSiteLinkRequestLoop(siteLinkIds) {
1087           var requestedSiteLinkIds = siteLinkIds.splice(0, chunkSize);
1088
1089           $mwtnTopology.getSiteLinksByIds(requestedSiteLinkIds).then(
1090             /**
1091              * Processes the database result.
1092              * The returned site links doesn't contain site objects but site ids.
1093              * In a subsequent step these ids will be converted into real site objects.
1094              * @param result {{id: string, siteA: string, siteZ: string}[]} The database result.
1095              */
1096             function (result) {
1097               // Subsequent convert.
1098               // All site links which siteA and siteZ site ids are known could be added to vm.knownSiteLinks.
1099               // The rest has to saved until all requests are finished.
1100               // Another database call will get the additional sites to convert these site links.
1101               var addedSiteLinkIds = result.reduce(function (accumulator, currentSiteLink) {
1102                 var siteA = vm.visibleSites[currentSiteLink.siteA] || vm.knownSites[currentSiteLink.siteA];
1103                 var siteZ = vm.visibleSites[currentSiteLink.siteZ] || vm.knownSites[currentSiteLink.siteZ];
1104                 if (siteA && siteZ) {
1105                   vm.knownSiteLinks[currentSiteLink.id] = {
1106                     id: currentSiteLink.id,
1107                     siteA: siteA,
1108                     siteZ: siteZ,
1109                     type: currentSiteLink.type
1110                   };
1111                   accumulator.push(currentSiteLink.id);
1112                 } else {
1113                   // Theoretically, only siteA or siteZ can be unknown. I still check both separately.
1114                   var isPushed = false;
1115                   if (!siteA) {
1116                     additionalSiteIds.push(currentSiteLink.siteA);
1117                     queuedSiteLinksToConvert.push(currentSiteLink);
1118                     isPushed = true;
1119                   }
1120                   if (!siteZ) {
1121                     additionalSiteIds.push(currentSiteLink.siteZ);
1122                     isPushed || queuedSiteLinksToConvert.push(currentSiteLink);
1123                   }
1124                 }
1125                 return accumulator;
1126               }, []);
1127
1128               // update the frontend before continue with the next database request.
1129               vm.mapSiteLinksComponentApi.updateSiteLinks && vm.mapSiteLinksComponentApi.updateSiteLinks(addedSiteLinkIds);
1130
1131               if (cancelRequested) {
1132                 // reset the cancelRequested flag and resolve the cancel request.
1133                 cancelRequested = false;
1134                 cancelDefer.resolve();
1135                 return;
1136               }
1137
1138               // Enforce angular to process a new digest cycle. (And to let angular time to redraw the map.)
1139               $timeout(function () {
1140                 if (siteLinkIds.length === 0) {
1141                   if (additionalSiteIds.length === 0) {
1142                     endProcessing();
1143                     return;
1144                   }
1145
1146                   // Only get a maximum of chunkSize additional sites.
1147                   // Site links more than that will not be drawed in the map due to performance reasons.
1148                   // If the user zooms in the additional site links will be requested again.
1149                   $mwtnTopology.getSitesByIds(additionalSiteIds.splice(0, chunkSize)).then(
1150                     /**
1151                      * Processes the database result to gain new sites within the bounding box of the database request.
1152                      * @param result {{total: number, sites: {id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}}[]}} The database result.
1153                      */
1154                     function (result) {
1155                       var addedSiteLinkIds = queuedSiteLinksToConvert.reduce(function (accumulator, currentSiteLink) {
1156                         var siteA = vm.visibleSites[currentSiteLink.siteA] || vm.knownSites[currentSiteLink.siteA] || result.sites.find(
1157                           function (site) {
1158                             return site.id === currentSiteLink.siteA
1159                           });
1160                         var siteZ = vm.visibleSites[currentSiteLink.siteZ] || vm.knownSites[currentSiteLink.siteZ] || result.sites.find(
1161                           function (site) {
1162                             return site.id === currentSiteLink.siteZ
1163                           });
1164                         if (siteA && siteZ) {
1165                           vm.knownSiteLinks[currentSiteLink.id] = {
1166                             id: currentSiteLink.id,
1167                             siteA: siteA,
1168                             siteZ: siteZ,
1169                             type: currentSiteLink.type
1170                           };
1171                           accumulator.push(currentSiteLink.id);
1172                         }
1173                         return accumulator;
1174                       }, []);
1175                       // update the frontend the last time until the user changed the map position.
1176                       vm.mapSiteLinksComponentApi.updateSiteLinks && vm.mapSiteLinksComponentApi.updateSiteLinks(addedSiteLinkIds);
1177
1178                       // all done - puh.
1179                       endProcessing();
1180                     }, processError);
1181
1182                 } else {
1183                   // There are more data, request the database again.
1184                   doSiteLinkRequestLoop(siteLinkIds);
1185                 }
1186               });
1187
1188             }, processError);
1189         }
1190
1191       }
1192     };
1193
1194     /**
1195      * Recalculates the targets site container for Sites within knownSites and visibleSites.
1196      * @param mapBoundsAndZoom {{top: number, right: number, bottom: number, left: number, zoom: number}} The new bounds and the new zoom level of the map control.
1197      */
1198     function refreshOutOfSightSites(mapBoundsAndZoom) {
1199       var additionalWidth = (mapBoundsAndZoom.right - mapBoundsAndZoom.left) / 4;
1200       var additionalHeight = (mapBoundsAndZoom.top - mapBoundsAndZoom.bottom) / 4;
1201       var slightlyTop = mapBoundsAndZoom.top + additionalHeight;
1202       var slightlyRight = mapBoundsAndZoom.right + additionalWidth;
1203       var slightlyBottom = mapBoundsAndZoom.bottom - additionalHeight;
1204       var slightlyLeft = mapBoundsAndZoom.left - additionalWidth;
1205
1206       var removedFromVisibleIds = [];
1207       var movedFromVisibleToKnownIds = [];
1208       var removedFromKnownIds = [];
1209       var movedFromKnownToVisibleIds = [];
1210       var removedSiteLinkIds = [];
1211
1212       var visibleSiteKeys = Object.keys(vm.visibleSites);
1213       var knownSiteKeys = Object.keys(vm.knownSites);
1214       var knownSiteLinkKeys = Object.keys(vm.knownSiteLinks);
1215
1216       visibleSiteKeys.forEach(function (siteId) {
1217         /** @var {{id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}} site */
1218         var site = vm.visibleSites[siteId];
1219
1220         if (site.location.lat > slightlyTop || site.location.lng > slightlyRight || site.location.lat < slightlyBottom || site.location.lng < slightlyLeft) {
1221           // This site is completly out of sight - so remove it and add the id to the removedFromVisibleIds list.
1222           delete vm.visibleSites[siteId];
1223           removedFromVisibleIds.push(siteId);
1224           return;
1225         }
1226
1227         if (site.location.lat > mapBoundsAndZoom.top || site.location.lng > mapBoundsAndZoom.right || site.location.lat < mapBoundsAndZoom.bottom || site.location.lng < mapBoundsAndZoom.left) {
1228           // This site is moved from the visible map bounds to the extended map bounds and will only hide but stay in the google object.
1229           vm.knownSites[siteId] = site;
1230           delete vm.visibleSites[siteId];
1231           movedFromVisibleToKnownIds.push(siteId);
1232           return;
1233         }
1234       });
1235
1236       knownSiteKeys.forEach(function (siteId) {
1237         /** @type {{id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}} site */
1238         var site = vm.knownSites[siteId];
1239
1240         if (site.location.lat > slightlyTop || site.location.lng > slightlyRight || site.location.lat < slightlyBottom || site.location.lng < slightlyLeft) {
1241           // This site is completly out of sight - so remove it and add the id to the removedFromKnownIds list.
1242           delete vm.knownSites[siteId];
1243           removedFromKnownIds.push(siteId);
1244           return;
1245         }
1246
1247         if (site.location.lat <= mapBoundsAndZoom.top && site.location.lng <= mapBoundsAndZoom.right && site.location.lat >= mapBoundsAndZoom.bottom && site.location.lng >= mapBoundsAndZoom.left) {
1248           // This site is moved from the extended map bounds to the visible map bounds. Therefore the hidden google maps object should set visible again.
1249           vm.visibleSites[siteId] = site;
1250           delete vm.knownSites[siteId];
1251           movedFromKnownToVisibleIds.push(siteId);
1252           return;
1253         }
1254       });
1255
1256       knownSiteLinkKeys.forEach(function (siteLinkId) {
1257         /** @type {{id: string, siteA: {id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}}, siteZ: {id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}}}} siteLink */
1258         var siteLink = vm.knownSiteLinks[siteLinkId];
1259         var siteA = siteLink.siteA;
1260         var siteZ = siteLink.siteZ;
1261
1262         if ((siteA.location.lat > slightlyTop || siteA.location.lng > slightlyRight || siteA.location.lat < slightlyBottom || siteA.location.lng < slightlyLeft) &&
1263           (siteZ.location.lat > slightlyTop || siteZ.location.lng > slightlyRight || siteZ.location.lat < slightlyBottom || siteZ.location.lng < slightlyLeft)) {
1264           // This site link is completly out of sight - so remove it and add the id to the removedSiteLinkIds list.
1265           delete vm.knownSiteLinks[siteLinkId];
1266           removedSiteLinkIds.push(siteLinkId);
1267         }
1268       });
1269
1270       return {
1271         removedFromVisibleIds: removedFromVisibleIds,
1272         movedFromVisibleToKnownIds: movedFromVisibleToKnownIds,
1273         removedFromKnownIds: removedFromKnownIds,
1274         movedFromKnownToVisibleIds: movedFromKnownToVisibleIds,
1275         removedSiteLinkIds: removedSiteLinkIds
1276       };
1277     }
1278
1279     /**
1280      * Requests a chunk of sites from the database.
1281      * @param mapBoundsAndZoom {{top: number, right: number, bottom: number, left: number, zoom: number}} The bounds and the zoom level of the map control to request the next chunk for.
1282      * @param chunkSize {number} The chunk size of a single database request. To big values will block the user interface, to small values will lead in to many database requests.
1283      * @param chunkSiteStartIndex {number} The index of the first site returned by the database.
1284      */
1285     function requestNextSiteChunk(mapBoundsAndZoom, chunkSize, chunkSiteStartIndex) {
1286       var requestNextSiteChunkDefer = $q.defer();
1287       var addedSiteIds = [];
1288
1289       // Request the next chunk.
1290       $mwtnTopology.getSitesInBoundingBox(mapBoundsAndZoom, chunkSize, chunkSiteStartIndex).then(
1291         /**
1292          * Processes the database result to gain new sites within the bounding box of the database request.
1293          * @param result {{chunkSize: number, chunkSiteStartIndex: number, total: number, sites: {id: string, name: string, location: {lat: number, lng: number}, amslGround: number, references: {siteLinks: string[]}}[]}} The database result.
1294          */
1295         function (result) {
1296           result.sites.forEach(function (site) {
1297             // The map bounds may have changed since the start of the database request.
1298             // Therefore check if the site is within the bounding rectangle.
1299
1300             if (!$mwtnTopology.isInBounds(mapBoundsAndZoom, site.location)) {
1301               return;
1302             }
1303
1304             // add all site link ids to the selectedSiteLinkIds dictionary.
1305             site.references.siteLinks.forEach(function (siteLinkId) {
1306               collectedSiteLinkIds[siteLinkId] = true;
1307             });
1308
1309             // check, if the site is within the knownSites dictionary.
1310             if (vm.knownSites[site.id]) {
1311               delete vm.knownSites[site.id];
1312             }
1313
1314             // check if the site is within the visibleSites dictionary.
1315             if (vm.visibleSites[site.id]) {
1316               // Override the site (refresh)
1317               vm.visibleSites[site.id] = site;
1318             } else {
1319               // Add the site to the dictionary and remember the siteId.
1320               vm.visibleSites[site.id] = site;
1321               addedSiteIds.push(site.id);
1322             }
1323           });
1324
1325           requestNextSiteChunkDefer.resolve({
1326             addedSiteIds: addedSiteIds,
1327             total: result.total
1328           });
1329         }, requestNextSiteChunkDefer.reject);
1330
1331       return requestNextSiteChunkDefer.promise;
1332     }
1333
1334     /**
1335      * Handles error messages by writing the information to the javascript console.
1336      * Resets the processing flag.
1337      * @param error Information about the error.
1338      */
1339     function processError(error) {
1340       // Reset the processing flag.
1341       vm.status.processing = false;
1342       // Write the error information to the console.
1343       console.error(error);
1344     }
1345
1346   }]);
1347
1348   mwtnTopologyApp.directive('mwtnTopologySiteView', function () {
1349     return {
1350       restrict: 'E',
1351       controller: 'mwtnTopologySiteViewController',
1352       controllerAs: 'vm',
1353       bindToController: true,
1354       templateUrl: 'src/app/mwtnTopology/templates/siteView.tpl.html',
1355       scope: {
1356         initialMapBounds: "="
1357       }
1358     };
1359   });
1360
1361   mwtnTopologyApp.controller('mwtnTopologySiteGridController', ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants) {
1362     var vm = this;
1363
1364     // The page number to show in the grid.
1365     var paginationPage = 1;
1366     // The page size.
1367     var paginationPageSize = 100;
1368     // The grid column object with current sorting informations.
1369     var sortColumn = null;
1370     // The grid column object with current sorting informations.
1371     var gridFilters = [];
1372     // caches all sites at the current grid page
1373     var sitesAtCurrentPageCache = {};
1374
1375     vm.showAllSites = false;
1376
1377     vm.onNavigateToSite = function (row) {
1378       var site = sitesAtCurrentPageCache[row.entity.id];
1379       $state.go("main.mwtnTopology", {
1380         tab: "site",
1381         lat: site.location.lat,
1382         lng: site.location.lng,
1383         zoom: 19,
1384         site: site.id,
1385         internal: false
1386       }, { notify: false });
1387     };
1388
1389     // see http://ui-grid.info/docs/#/tutorial/317_custom_templates
1390     var buttonCellTemplate = '<div class="ui-grid-cell-contents tooltip-uigrid" title="TOOLTIP"><i ng-click="grid.appScope.vm.onNavigateToSite(row)" ng-class="{\'fa\':true, \'fa-map-marker\': true, \'{{COL_FIELD}}\':true}" aria-hidden="true"></i></div>';
1391
1392     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
1393       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
1394       paginationPageSizes: [10, 25, 50, 100],
1395       paginationPageSize: paginationPageSize,
1396       useExternalPagination: true,
1397       useExternalFiltering: true,
1398       useExternalSorting: true,
1399       totalItems: 0,
1400       columnDefs: [{
1401         field: "id",
1402         type: "string",
1403         displayName: "Id"
1404       },
1405       {
1406         field: "name",
1407         type: "string",
1408         displayName: "Name"
1409       },
1410       {
1411         field: "location",
1412         type: "string",
1413         displayName: "Location"
1414       },
1415       {
1416         field: "amslGround",
1417         type: "string",
1418         displayName: "AmslGround"
1419       },
1420       {
1421         field: "countLinks",
1422         type: "number",
1423         displayName: "Count (Links)"
1424       },
1425       {
1426         field: "countNetworkElements",
1427         type: "number",
1428         displayName: "Count (Network elements)"
1429       },
1430       {
1431         field: "buttons",
1432         type: "string",
1433         displayName: "",
1434         width: 40,
1435         enableFiltering: false,
1436         enableSorting: false,
1437         cellTemplate: buttonCellTemplate,
1438         pinnedRight: true
1439       }
1440       ],
1441       data: [],
1442       onRegisterApi: function (gridApi) {
1443         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
1444         vm.gridApi = gridApi;
1445
1446         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
1447           // Save the current sort column for later use.
1448           sortColumn = (!sortColumns || sortColumns.length === 0)
1449             ? null
1450             : sortColumns[0];
1451           loadPage();
1452         });
1453
1454         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
1455           // Save the pagination informations for later use.
1456           paginationPage = newPage;
1457           paginationPageSize = newPageSize;
1458           loadPage();
1459         });
1460
1461         vm.gridApi.core.on.filterChanged($scope, function () {
1462           // Save the all filters for later use.
1463           var filters = [];
1464           this.grid.columns.forEach(function (col, ind) {
1465             if (col.filters[0] && col.filters[0].term) {
1466               filters.push({ field: col.field, term: col.filters[0].term });
1467             }
1468           });
1469           gridFilters = filters;
1470           loadPage();
1471         });
1472
1473         loadPage();
1474       }
1475     });
1476
1477     $scope.$on("mapViewVisuability", function (event, data) {
1478       vm.showAllSites = !data;
1479     });
1480
1481     $scope.$watch("visibleSites", function (newVisibleSites, oldVisibleSites) {
1482       console.log("watch: visibleSites");
1483       if (!vm.showAllSites) loadPage();
1484     }, true); // deep watch, maybe find a better solution; e.g. with an api object like the site in the map.
1485
1486     $scope.$watch(function () { return vm.showAllSites; }, loadPage);
1487
1488     function loadPage() {
1489       if (vm.showAllSites) {
1490         loadRemotePage();
1491       } else {
1492         loadLocalPage();
1493       }
1494     }
1495
1496     /**
1497      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
1498      */
1499     function loadLocalPage() {
1500       var siteIds = Object.keys($scope.visibleSites);
1501
1502       var tempData = siteIds.filter(function (siteId, ind, arr) {
1503         var site = $scope.visibleSites[siteId];
1504         return gridFilters.map(function (filter) {
1505           switch (filter.field) {
1506             case "countLinks":
1507               return (site.references.siteLinks ? site.references.siteLinks.length : 0) == +filter.term;
1508             default:
1509               return site[filter.field].contains(filter.term);
1510           }
1511         }).and(true);
1512       }).map(function (siteId) {
1513         var site = $scope.visibleSites[siteId];
1514         var orderBy;
1515
1516         if (!sortColumn || !sortColumn.sort.direction) {
1517           return {
1518             id: siteId
1519           }
1520         }
1521
1522         switch (sortColumn.field) {
1523           case "countLinks":
1524             orderBy = site.references.siteLinks ? site.references.siteLinks.length : 0;
1525             break;
1526           case "countNetworkElements":
1527             orderBy = site.references.networkElements ? site.references.networkElements.length : 0;
1528             break;
1529           case "buttons":
1530             orderBy = siteId;
1531             break;
1532           default:
1533             orderBy = site[sortColumn.field];
1534             break;
1535         }
1536
1537         return {
1538           id: siteId,
1539           orderBy: orderBy
1540         };
1541       });
1542
1543       if (sortColumn && sortColumn.sort.direction) {
1544         tempData.sort(function (left, right) {
1545           if (left === right || left.orderBy === right.orderBy) {
1546             return 0;
1547           }
1548           if (left.orderBy > right.orderBy) {
1549             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
1550           }
1551           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
1552         });
1553       }
1554
1555       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
1556       var currentPage = Math.min(maxPageNumber, paginationPage);
1557       var orderedSitesAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
1558
1559       sitesAtCurrentPageCache = {};
1560       var orderedData = [];
1561
1562       orderedSitesAtCurrentPage.forEach(function (orderedSite) {
1563         var site = $scope.visibleSites[orderedSite.id];
1564         sitesAtCurrentPageCache[site.id] = site;
1565         orderedData.push({
1566           id: orderedSite.id,
1567           name: site.name,
1568           location: site.location.lat.toLocaleString("en-US", {
1569             minimumFractionDigits: 4
1570           }) + ", " + site.location.lng.toLocaleString("en-US", {
1571             minimumFractionDigits: 4
1572           }),
1573           amslGround: site.amslGround,
1574           countLinks: site.references.siteLinks ? site.references.siteLinks.length : 0,
1575           countNetworkElements: site.references.networkElements ? site.references.networkElements.length : 0
1576         });
1577       });
1578
1579       $timeout(function () {
1580         vm.gridOptions.data = orderedData;
1581         vm.gridOptions.totalItems = tempData.length;
1582       });
1583     }
1584
1585     /**
1586      * Loads the page content for the grid and sets the values to the gridOprions.data object.
1587      */
1588     function loadRemotePage() {
1589
1590       $mwtnTopology.getSites((sortColumn && sortColumn.field), sortColumn && ((sortColumn.sort.direction && sortColumn.sort.direction === uiGridConstants.ASC) ? 'asc' : 'desc'), gridFilters, paginationPageSize, (paginationPage - 1) * paginationPageSize).then(function (result) {
1591         sitesAtCurrentPageCache = result.sites.reduce(function (acc, cur, ind, arr) {
1592           acc[cur.id] = cur;
1593           return acc;
1594         }, {});
1595         vm.gridOptions.data = result.sites.map(function (site) {
1596           return {
1597             id: site.id,
1598             name: site.name,
1599             location: site.location.lat.toLocaleString("en-US", {
1600               minimumFractionDigits: 4
1601             }) + ", " + site.location.lng.toLocaleString("en-US", {
1602               minimumFractionDigits: 4
1603             }),
1604             amslGround: site.amslGround,
1605             countLinks: site.references.siteLinks ? site.references.siteLinks.length : 0,
1606             countNetworkElements: site.references.networkElements ? site.references.networkElements.length : 0
1607           }
1608         });
1609         vm.gridOptions.totalItems = result.total;
1610       });
1611     }
1612   }]);
1613
1614   mwtnTopologyApp.directive('mwtnTopologySiteGrid', function () {
1615     return {
1616       restrict: 'E',
1617       replace: false,
1618       controller: 'mwtnTopologySiteGridController',
1619       controllerAs: 'vm',
1620       scope: {
1621         visibleSites: "="
1622       },
1623       templateUrl: 'src/app/mwtnTopology/templates/siteGrid.tpl.html'
1624     };
1625   });
1626
1627   mwtnTopologyApp.controller('mwtnTopologySiteLinkGridController', ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants) {
1628     var vm = this;
1629
1630     // The page number to show in the grid.
1631     var paginationPage = 1;
1632     // The page size.
1633     var paginationPageSize = 100;
1634     // The grid column object with current sorting informations.
1635     var sortColumn = null;
1636     // The grid column object with current sorting informations.
1637     var gridFilters = [];
1638
1639     var linksAtCurrentPageCache = {};
1640
1641     vm.showAllLinks = false;
1642
1643     vm.onNavigateToLink = function (row) {
1644       var link = linksAtCurrentPageCache[row.entity.id];
1645
1646       var top = link.siteA.location.lat >= link.siteZ.location.lat ? link.siteA.location.lat : link.siteZ.location.lat;
1647       var bottom = link.siteA.location.lat >= link.siteZ.location.lat ? link.siteZ.location.lat : link.siteA.location.lat;
1648
1649       // todo: this code is not able to handle links which overlap the -100|180 ° borderline
1650       var left = link.siteA.location.lng <= link.siteZ.location.lng ? link.siteA.location.lng : link.siteZ.location.lng;
1651       var right = link.siteA.location.lng <= link.siteZ.location.lng ? link.siteZ.location.lng : link.siteA.location.lng;
1652
1653       $state.go("main.mwtnTopology", {
1654         tab: "site",
1655         top: top,
1656         bottom: bottom,
1657         left: left,
1658         right: right,
1659         siteLink: link.id,
1660         internal: false
1661       }, { notify: false });
1662       console.log(link);
1663     };
1664
1665     // see http://ui-grid.info/docs/#/tutorial/317_custom_templates
1666     var buttonCellTemplate = '<div class="ui-grid-cell-contents tooltip-uigrid" title="TOOLTIP"><i ng-click="grid.appScope.vm.onNavigateToLink(row)" ng-class="{\'fa\':true, \'fa-map-marker\': true, \'{{COL_FIELD}}\':true}" aria-hidden="true"></i></div>';
1667
1668     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
1669       // enableFiltering: false,
1670       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
1671       paginationPageSizes: [10, 25, 50, 100],
1672       paginationPageSize: paginationPageSize,
1673       useExternalPagination: true,
1674       useExternalFiltering: true,
1675       useExternalSorting: true,
1676       totalItems: 0,
1677       columnDefs: [{
1678         field: "id",
1679         type: "string",
1680         displayName: "Id"
1681       },
1682       {
1683         field: "name",
1684         type: "string",
1685         displayName: "Name"
1686       },
1687       {
1688         field: "siteIdA",
1689         type: "string",
1690         displayName: "SiteA"
1691       },
1692       {
1693         field: "siteIdZ",
1694         type: "string",
1695         displayName: "SiteZ"
1696       },
1697       {
1698         field: "buttons",
1699         type: "string",
1700         displayName: "",
1701         width: 40,
1702         enableFiltering: false,
1703         enableSorting: false,
1704         cellTemplate: buttonCellTemplate,
1705         pinnedRight: true
1706       }
1707       ],
1708       data: [],
1709       onRegisterApi: function (gridApi) {
1710         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
1711         vm.gridApi = gridApi;
1712
1713         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
1714           // save the current sort column for later use.
1715           sortColumn = (!sortColumns || sortColumns.length === 0) ?
1716             null :
1717             sortColumns[0];
1718
1719           loadPage();
1720         });
1721
1722         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
1723           paginationPage = newPage;
1724           paginationPageSize = newPageSize;
1725           loadPage();
1726         });
1727
1728         vm.gridApi.core.on.filterChanged($scope, function () {
1729           // Save the all filters for later use.
1730           var filters = [];
1731           this.grid.columns.forEach(function (col, ind) {
1732             if (col.filters[0] && col.filters[0].term) {
1733               filters.push({ field: col.field, term: col.filters[0].term });
1734             }
1735           });
1736           gridFilters = filters;
1737           loadPage();
1738         });
1739
1740         loadPage();
1741       }
1742     });
1743
1744     $scope.$on("mapViewVisuability", function (event, data) {
1745       vm.showAllLinks = !data;
1746     });
1747
1748     $scope.$watch("knownSiteLinks", function (newKnownSiteLinks, oldKnownSiteLinks) {
1749       console.log("watch: knownSiteLinks");
1750       loadPage();
1751     }, true); // deep watch, maybe find a better solution; e.g. with an api object like the site in the map.
1752
1753     $scope.$watch(function () { return vm.showAllLinks; }, loadPage);
1754
1755     function loadPage() {
1756       if (vm.showAllLinks) {
1757         loadRemotePage();
1758       } else {
1759         loadLocalPage();
1760       }
1761     }
1762
1763     /**
1764         * Calculates the page content of the grid and sets the values to the gridOprions.data object.
1765         */
1766     function loadLocalPage() {
1767       var linkIds = Object.keys($scope.knownSiteLinks);
1768
1769       var tempData = linkIds.filter(function (linkId, ind, arr) {
1770         var link = $scope.knownSiteLinks[linkId];
1771         return gridFilters.map(function (filter) {
1772           switch (filter.field) {
1773             case 'siteIdA':
1774               return link.siteA.id.contains(filter.term);
1775             case 'siteIdZ':
1776               return link.siteZ.id.contains(filter.term);
1777             default:
1778               return link[filter.field].contains(filter.term);
1779           }
1780         }).and(true);
1781       }).map(function (linkId) {
1782         var link = $scope.knownSiteLinks[linkId];
1783         var orderBy;
1784
1785         if (!sortColumn || !sortColumn.sort.direction) {
1786           return {
1787             id: linkId
1788           }
1789         }
1790
1791         switch (sortColumn.field) {
1792           case 'siteIdA':
1793             orderBy = link.siteA.id;
1794             break;
1795           case 'siteIdZ':
1796             orderBy = link.siteZ.id;
1797             break;
1798           default:
1799             orderBy = link[sortColumn.field];
1800             break;
1801         }
1802
1803         return {
1804           id: linkId,
1805           orderBy: orderBy
1806         };
1807       });
1808
1809       if (sortColumn && sortColumn.sort.direction) {
1810         tempData.sort(function (left, right) {
1811           if (left === right || left.orderBy === right.orderBy) {
1812             return 0;
1813           }
1814           if (left.orderBy > right.orderBy) {
1815             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
1816           }
1817           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
1818         });
1819       }
1820
1821       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
1822       var currentPage = Math.min(maxPageNumber, paginationPage);
1823       var orderedSitesAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
1824
1825       linksAtCurrentPageCache = {};
1826       var orderedData = [];
1827
1828       orderedSitesAtCurrentPage.forEach(function (orderdLink) {
1829         var link = $scope.knownSiteLinks[orderdLink.id];
1830         linksAtCurrentPageCache[link.id] = link;
1831         orderedData.push({
1832           id: link.id,
1833           name: link.name,
1834           siteIdA: link.siteA.id,
1835           siteIdZ: link.siteZ.id,
1836           type: link.type
1837         });
1838       });
1839
1840       $timeout(function () {
1841         vm.gridOptions.data = orderedData;
1842         vm.gridOptions.totalItems = tempData.length;
1843       });
1844     }
1845
1846     /**
1847      * Loads the page content for the grid and sets the values to the gridOprions.data object.
1848      */
1849     function loadRemotePage() {
1850       linksAtCurrentPageCache = {};
1851
1852       // the links for one page
1853       $mwtnTopology.getLinks((sortColumn && sortColumn.field), sortColumn && ((sortColumn.sort.direction && sortColumn.sort.direction === uiGridConstants.ASC) ? 'asc' : 'desc'), gridFilters, paginationPageSize, (paginationPage - 1) * paginationPageSize).then(function (result) {
1854         // get all site id s  
1855         var siteIds = {};
1856         result.links.forEach(function (link) {
1857           siteIds[link.siteA] = link.siteA;
1858           siteIds[link.siteZ] = link.siteZ;
1859         });
1860
1861         // load all sites         
1862         $mwtnTopology.getSitesByIds(Object.keys(siteIds)).then(function (sitesResult) {
1863           var sites = sitesResult.sites.reduce(function (acc, cur, ind, arr) {
1864             acc[cur.id] = cur;
1865             return acc;
1866           }, {});
1867           result.links.forEach(function (link) {
1868             linksAtCurrentPageCache[link.id] = {
1869               id: link.id,
1870               name: link.name,
1871               siteA: sites[link.siteA],
1872               siteZ: sites[link.siteZ],
1873               type: link.type
1874             };
1875           });
1876           vm.gridOptions.data = result.links.map(function (link) {
1877             return {
1878               id: link.id,
1879               name: link.name,
1880               siteIdA: link.siteA,
1881               siteIdZ: link.siteZ,
1882               type: link.type
1883             }
1884           });
1885           vm.gridOptions.totalItems = result.total;
1886
1887         });
1888       });
1889     }
1890
1891
1892   }]);
1893
1894   mwtnTopologyApp.directive('mwtnTopologySiteLinkGrid', function () {
1895     return {
1896       restrict: 'E',
1897       replace: false,
1898       controller: 'mwtnTopologySiteLinkGridController',
1899       controllerAs: 'vm',
1900       templateUrl: 'src/app/mwtnTopology/templates/siteLinkGrid.tpl.html',
1901       scope: {
1902         knownSiteLinks: "="
1903       },
1904     };
1905   });
1906
1907   mwtnTopologyApp.controller('mwtnTopologySitePathGridController', ['$scope', function ($scope) {
1908     var vm = this;
1909
1910     vm.showAllPaths = false;
1911
1912     $scope.$on("mapViewVisuability", function (event, data) {
1913       vm.showAllPaths = !data;
1914     });
1915
1916   }]);
1917
1918   mwtnTopologyApp.directive('mwtnTopologySitePathGrid', function () {
1919     return {
1920       restrict: 'E',
1921       replace: false,
1922       controller: 'mwtnTopologySitePathGridController',
1923       controllerAs: 'vm',
1924       templateUrl: 'src/app/mwtnTopology/templates/sitePathGrid.tpl.html'
1925     };
1926   });
1927
1928   /********************************************* Physical ***********************************/
1929
1930   mwtnTopologyApp.controller('mwtnTopologyPhysicalViewController', ['$scope', '$q', '$timeout', '$state', '$window', '$mwtnTopology', function ($scope, $q, $timeout, $state, $window, $mwtnTopology) {
1931     var vm = this;
1932     vm.status = {
1933       graphIsOpen: false,
1934       networkElementsIsOpen: false,
1935       LinksIsOpen: false
1936     };
1937
1938     $scope.$watchCollection(function () { return [vm.status.graphIsOpen, vm.status.networkElementsIsOpen, vm.status.LinksIsOpen] }, function (newVal, oldVal) {
1939       if (newVal[1] || newVal[2]) {
1940         $timeout(function () {
1941           $window.dispatchEvent(new Event("resize"));
1942         });
1943       }
1944     });
1945
1946   }]);
1947
1948   mwtnTopologyApp.directive('mwtnTopologyPhysicalView', function () {
1949     return {
1950       restrict: 'E',
1951       controller: 'mwtnTopologyPhysicalViewController',
1952       controllerAs: 'vm',
1953       bindToController: true,
1954       templateUrl: 'src/app/mwtnTopology/templates/physicalView.tpl.html',
1955       scope: {
1956
1957       }
1958     };
1959   });
1960
1961   /** 
1962    * Typedefinitions for the mwtnTopologyPhysicalPathData service.
1963    * @typedef { { 'site' | 'device' |  'port' } } NodeLayerType
1964    * @typedef { { x: number, y: number } } PositionType
1965    * @typedef { { id: string, label: string, parent: string, grentparent: string, active: string, latitude: number, longitude: number  } } NodeDataVo
1966    * @typedef { { data: NodeDataVo, position: PositionType  } } NodeVo
1967    * @typedef { { id: string, label: string, parent: string, type: string, layer: NodeLayerType, active: boolean, latitude?: number, longitude?: number  } } NodeData
1968    * @typedef { { data: NodeData, position: PositionType  } } Node
1969    * @typedef { { id: string, source: string, target: string, label: string , lentgh: string, azimuthAZ: string , azimuthZA: string , layer: string , active: string } } EdgeData
1970    * @typedef { { data: EdgeData } Edge
1971    */
1972   mwtnTopologyApp.factory("mwtnTopologyPhysicalPathData", ['$q','$mwtnTopology',
1973     /** @param $q { ng.IQService } */
1974     function ($q, $mwtnTopology) {
1975       var colors = {
1976         root: '#f54',
1977         port: '#377',
1978         device: '#252',
1979         site: '#525',
1980         edge: '#49a',
1981         white: '#eed',
1982         grey: '#555',
1983         selected: '#ff0'
1984       };
1985
1986       var styles = [
1987         {
1988           selector: 'node',
1989           css: {
1990             'content': 'data(label)',
1991             'text-valign': 'center',
1992             'text-halign': 'center',
1993             'background-color': '#666666',
1994             'border-color': '#000000',
1995             'border-width': '1px',
1996             'color': '#ffffff'
1997           }
1998         },
1999         {
2000           selector: 'node[layer = "MWPS"]',
2001           css: {
2002             'content': 'data(label)',
2003             'text-valign': 'center',
2004             'text-halign': 'center',
2005             'background-color': '#316ac5',
2006             'border-color': '#000000',
2007             'border-width': '1px',
2008             'color': '#ffffff'
2009           }
2010         },
2011         {
2012           selector: '$node > node',
2013           css: {
2014             'shape': 'roundrectangle',
2015             'padding-top': '10px',
2016             'padding-left': '10px',
2017             'padding-bottom': '10px',
2018             'padding-right': '10px',
2019             'text-valign': 'top',
2020             'text-halign': 'center',
2021             'background-color': '#eeeeee',
2022             'color': '#444444',
2023             'border-color': '#888888'
2024           }
2025         },
2026         {
2027           selector: 'node[type = "site"]',
2028           css: {
2029             'shape': 'roundrectangle',
2030             'padding-top': '10px',
2031             'padding-left': '10px',
2032             'padding-bottom': '10px',
2033             'padding-right': '10px',
2034             'text-valign': 'center',
2035             'text-halign': 'center',
2036             'background-color': '#fefefe',
2037             'color': '#444444',
2038             'border-color': '#888888',
2039             'font-weight': 'bold'
2040           }
2041         },
2042         {
2043           selector: 'node[type = "device"][active = "true"]',
2044           css: {
2045             'background-color': '#316ac5',
2046             'background-opacity': '0.3',
2047             'border-color': '#316ac5',
2048             'border-width': '2px',
2049             'color': '#444444'
2050           }
2051         },
2052         {
2053           selector: 'node[type = "port"][active = "true"]',
2054           css: {
2055             'background-opacity': '1.0',
2056           }
2057         },
2058         {
2059           selector: 'node[active = "false"]',
2060           css: {
2061             'background-opacity': '0.3',
2062             'border-opacity': '0.5'
2063           }
2064         },
2065
2066         {
2067           selector: 'edge',
2068           css: {
2069             'content': 'data(id)',
2070             'target-arrow-shape': 'triangle',
2071             'line-color': '#666666',
2072             'color': '#444444'
2073           }
2074         },
2075         {
2076           selector: 'edge[active = "false"]',
2077           css: {
2078             'line-color': '#cccccc',
2079             'text-opacity': '0.9'
2080           }
2081         },
2082         {
2083           selector: 'edge[layer = "MWPS"]',
2084           css: {
2085             'content': 'data(id)',
2086             'target-arrow-shape': 'triangle',
2087             'width': '5px',
2088             'line-color': '#316ac5',
2089             'color': '#444444'
2090           }
2091         },
2092         {
2093           selector: 'edge[layer = "MWPS"][active = "false"]',
2094           css: {
2095             'line-color': '#C0D1EC',
2096             'text-opacity': '0.9'
2097           }
2098         },
2099         {
2100           selector: ':selected',
2101           css: {
2102             'background-color': 'black',
2103             'line-color': 'black',
2104             'target-arrow-color': 'black',
2105             'source-arrow-color': 'black'
2106           }
2107         }
2108       ];
2109
2110         var events = eventsFabric();
2111
2112       /** Heplerfunction to retrive all elements from the database and convert to the structure needed */
2113       function getElements() {
2114         var res = $q.defer();
2115
2116         $q.all([
2117           $mwtnTopology.getAllNodes(),
2118           $mwtnTopology.getAllEdges()
2119          ]).then(function (results) {
2120            res.resolve({ nodes: results[0], edges: results[1] });
2121         });
2122         
2123         return res.promise;
2124       }
2125
2126       var result = {
2127         colors: colors,
2128         getElements: getElements,
2129         styles: styles,
2130         events: events
2131       };
2132
2133       var someMethodChangingTheElements = function () {
2134         // @Martin: hier kannst Du die Elements ändern, anschließend mußt Du das Ereignis veröffentlichen
2135         //          das Ereigniss wird in der Directive aufgefangen und die Grig wird neu gezeichnet
2136
2137         // Hinweis: Die Reihenfolge muss so bleiben und du kannst NUR result.elements ändern.
2138
2139         events.publish("elementsChanged", {
2140           elements: result.elements
2141         });
2142       };
2143
2144       return result;
2145     }]);
2146
2147     mwtnTopologyApp.directive("mwtnTopologyPhysicalPathGraph", ["mwtnTopologyPhysicalPathData", "$mwtnTopology", "$mwtnCommons", function (pathGraphData, $mwtnTopology, $mwtnCommons) {
2148
2149     return {
2150       restrict: 'E',
2151       replace: true,
2152       template: '<div style="height:750px; width: 100%;"></div>',
2153       controller: function () {
2154
2155       },
2156       scope: {
2157
2158       },
2159       link: function (scope, element, attrs, ctrl) {
2160
2161         var cy = cytoscape({
2162           container: element[0],
2163
2164           boxSelectionEnabled: false,
2165           autounselectify: true,
2166
2167           style: pathGraphData.styles,
2168           elements: [],
2169           layout: {
2170             name: 'preset',
2171             padding: 5
2172           }
2173         });
2174
2175         cy.viewport({
2176           zoom: 0.50,
2177           pan: { x: 100, y: 50 }
2178         });
2179   
2180         pathGraphData.getElements().then(function (elements) {
2181           cy.json({
2182             elements: elements
2183           });
2184
2185           // disable drag & drop
2186           cy.nodes().ungrabify();
2187         });
2188
2189         var filterActiveMountPoints = function (mountpoints) {
2190           return mountpoints.filter(function (mountpoint) {
2191             if (!mountpoint) return false;
2192             // console.warn(mountpoint['node-id'], mountpoint['netconf-node-topology:connection-status']);
2193             return mountpoint['netconf-node-topology:connection-status'] === 'connected';
2194           }).map(function (mountpoint) {
2195             return mountpoint['node-id'];
2196           });
2197         };
2198
2199         var setDevicesActive = function (nodeIds) {
2200           // console.warn(nodeIds);
2201           cy.nodes().filter(function (node) {
2202             node.data('active', 'false');
2203             return node.data('type') === 'device' && nodeIds.contains(node.data('id'));
2204           }).map(function (node) {
2205             node.data('active', 'true');
2206           });
2207         };
2208
2209         var setAllDevicesInactive = function () {
2210           cy.nodes().map(function (node) {
2211             node.data('active', 'false');
2212           });
2213         };
2214
2215         var setPortAndEdgedActive = function () {
2216           cy.edges().map(function (edge) {
2217             var active = 'true';
2218             edge.connectedNodes().map(function (port) {
2219               // console.log('  node', JSON.stringify(edge.data()));
2220               var parent = cy.getElementById(port.data('parent'));
2221               if (parent.data('active') === 'false') {
2222                 port.data('active', 'false');
2223                 edge.data('active', 'false');
2224               } else {
2225                 port.data('active', 'true');
2226               }
2227             });
2228           });
2229         };
2230
2231         var setCss = function () {
2232           var width = 5 / cy.zoom();
2233           var stroke = 1 / cy.zoom();
2234           cy.edges().css('width', width);
2235           cy.nodes().css('border-width', stroke);
2236         };
2237
2238         var init = function () {
2239           $mwtnCommons.getMountPoints().then(function (mountpoints) {
2240             var filtered = filterActiveMountPoints(mountpoints);
2241             setDevicesActive(filtered);
2242             setPortAndEdgedActive();
2243           }, function (error) {
2244             setAllDevicesInactive();
2245             setPortAndEdgedActive();
2246           });
2247         };
2248
2249         init();
2250         setCss();
2251
2252         var orderLtps = function () {
2253           var done = [];
2254           var x = 0;
2255           var y = 0;
2256           var row = 0;
2257           var offset = 100;
2258           var selector = "[type = 'device']";
2259           var devices = cy.nodes(selector).sort(function (a, b) {
2260             if (a.children().length > b.children().length) return -1;
2261             if (a.children().length < b.children().length) return 1;
2262             return 0;
2263           }).map(function (device) {
2264             device.children().filter(function (ltp) {
2265               return done.indexOf(ltp.id()) === -1;
2266             }).map(function (ltp) {
2267               ltp.position({ x: x, y: y });
2268               y = y + offset;
2269               var remeberX = x;
2270               ltp.connectedEdges().map(function (edge) {
2271                 edge.connectedNodes().sort(function (a, b) {
2272                   if (a.data("parent") === device.id()) return -1;
2273                   return 1;
2274                 }).map(function (node) {
2275                   // x = remeberX + 0 * offset;
2276                   node.position({ x: x, y: y });
2277                   x = x + 3 * offset;
2278                   y = y + offset;
2279                   done.push(node.id());
2280                 });
2281                 y = y - 2 * offset;
2282
2283               });
2284               x = remeberX;
2285               done.push(ltp.id());
2286             });
2287             y = row * 6 * offset;
2288             x = x + 6 * offset;
2289
2290             if (x > 4000) {
2291               row = row + 1;
2292               x = 0;
2293               y = row * 6 * offset;
2294             }
2295             return device;
2296           });
2297         };
2298
2299         var dragedNodes = [];    
2300         // add an event handler for 'tabdrag' for all ports 
2301         cy.on('drag', 'node[type = "port"]', function (event) {
2302           var id = event.target.data().id;
2303           dragedNodes.indexOf(id) <= -1 && dragedNodes.push(id);
2304         });
2305
2306         cy.on('tap', function (event) {
2307           if (event.target !== cy) {
2308             console.info('click', JSON.stringify(event.target.data()));
2309           } else {
2310             orderLtps();
2311             // cy.zoom(0.1);
2312             cy.fit();
2313             cy.center();
2314           }
2315         });
2316
2317         cy.on('zoom', function (event) {
2318           setCss();
2319         });
2320
2321         // global keyboard event handler
2322         function handleKey(e) {
2323           if (!e.ctrlKey && !e.commandKey) return;
2324           switch (e.which) {
2325             case 69:
2326               dragedNodes = [];
2327               cy.nodes().grabify();
2328               e.preventDefault();
2329               return false;
2330               break;
2331             case 83:
2332               cy.nodes().ungrabify();
2333               e.preventDefault();
2334               var modifiedNodes = dragedNodes.map(id => ({ id: id, position: cy.nodes().getElementById(id).position() }));
2335               $mwtnTopology.saveChangedNodes(modifiedNodes);
2336               console.log("dragedNodes", modifiedNodes);
2337               return false;
2338               break;
2339             // default:
2340             //   console.log(e.which);
2341             //   e.preventDefault();
2342             //   return false;
2343             //   break;
2344           }
2345         }
2346
2347         // register global keyboard event handler
2348         window.addEventListener('keydown', handleKey, false);
2349
2350         scope.$on('$destroy', function () {
2351           // un-register global keyboard event handler
2352           window.removeEventListener('keydown', handleKey, false);
2353         });
2354
2355       }
2356     }
2357   }]);
2358
2359   mwtnTopologyApp.controller("mwtnTopologyNetworkElementsGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyPhysicalPathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyPhysicalPathData) {
2360     var vm = this;
2361
2362     // The page number to show in the grid.
2363     var paginationPage = 1;
2364     // The page size.
2365     var paginationPageSize = 100;
2366     // The grid column object with current sorting informations.
2367     var sortColumn = null;
2368     // The grid column object with current sorting informations.
2369     var gridFilters = [];
2370     // caches all sites at the current grid page
2371
2372     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
2373       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
2374       paginationPageSizes: [10, 25, 50, 100],
2375       paginationPageSize: paginationPageSize,
2376       useExternalPagination: true,
2377       useExternalFiltering: true,
2378       useExternalSorting: true,
2379       totalItems: 0,
2380       columnDefs: [{
2381         field: "id",
2382         type: "string",
2383         displayName: "Id"
2384       },
2385       {
2386         field: "layer",
2387         type: "string",
2388         displayName: "Layer",
2389         width: 100,
2390       },
2391       {
2392         field: "active",
2393         type: "string",
2394         displayName: "Active",
2395         width: 100,
2396       },
2397       {
2398         field: "latitude",
2399         type: "string",
2400         displayName: "Latitude",
2401         visible: false
2402       },
2403       {
2404         field: "longitude",
2405         type: "string",
2406         displayName: "Longitude",
2407         visible: false
2408       },
2409       {
2410         field: "installed", // capacity in Mbit/s
2411         type: "number",
2412         displayName: "Installed [Mbit/s]",
2413         className: "number",
2414         width: 150
2415       },
2416       {
2417         field: "configured", // capacity in Mbit/s
2418         type: "number",
2419         displayName: "Configured [Mbit/s]",
2420         className: "number",
2421         width: 150
2422       },
2423       {
2424         field: "effective", // capacity in Mbit/s
2425         type: "number",
2426         displayName: "Effective [Mbit/s]",
2427         className: "number",
2428         width: 150
2429       }
2430       ],
2431       data: [],
2432       onRegisterApi: function (gridApi) {
2433         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
2434         vm.gridApi = gridApi;
2435
2436         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
2437           // Save the current sort column for later use.
2438           sortColumn = (!sortColumns || sortColumns.length === 0) ?
2439             null :
2440             sortColumns[0];
2441           loadPage();
2442         });
2443
2444         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
2445           // Save the pagination informations for later use.
2446           paginationPage = newPage;
2447           paginationPageSize = newPageSize;
2448           loadPage();
2449         });
2450
2451         vm.gridApi.core.on.filterChanged($scope, function () {
2452           // Save the all filters for later use.
2453           var filters = [];
2454           this.grid.columns.forEach(function (col, ind) {
2455             if (col.filters[0] && col.filters[0].term) {
2456               filters.push({
2457                 field: col.field,
2458                 term: col.filters[0].term
2459               });
2460             }
2461           });
2462           gridFilters = filters;
2463           loadPage();
2464         });
2465
2466         loadPage();
2467       }
2468     });
2469
2470     /**
2471      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
2472      */
2473     function loadPage() {
2474
2475       // extract all ports        
2476       var ports = mwtnTopologyPhysicalPathData.elements.nodes.filter(function (node, ind, arr) {
2477         return node && node.data && node.data.type === 'port';
2478       }).reduce(function (acc, cur, ind, arr) {
2479         if (cur.data) acc[cur.data.id] = cur.data;
2480         return acc;
2481       }, {});
2482
2483       // get all port ids
2484       var portIds = Object.keys(ports);
2485
2486       // apply the grid filters
2487       var tempData = portIds.filter(function (portId, ind, arr) {
2488         var port = ports[portId];
2489         return gridFilters.map(function (filter) {
2490           switch (filter.field) {
2491             case "active":
2492               return port[filter.field].toString().contains(filter.term);
2493             default:
2494               return port[filter.field].contains(filter.term);
2495           }
2496         }).and(true);
2497       }).map(function (portId) {
2498         var port = ports[portId];
2499         var orderBy;
2500
2501         if (!sortColumn || !sortColumn.sort.direction) {
2502           return {
2503             id: port.id
2504           }
2505         }
2506
2507         switch (sortColumn.field) {
2508           case "active":
2509             orderBy = port[sortColumn.field] ? 1 : 0;
2510             break;
2511           default:
2512             orderBy = port[sortColumn.field];
2513             break;
2514         }
2515
2516         return {
2517           id: port.id,
2518           orderBy: orderBy
2519         };
2520       });
2521
2522       if (sortColumn && sortColumn.sort.direction) {
2523         tempData.sort(function (left, right) {
2524           if (left === right || left.orderBy === right.orderBy) {
2525             return 0;
2526           }
2527           if (left.orderBy > right.orderBy) {
2528             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
2529           }
2530           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
2531         });
2532       }
2533
2534       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
2535       var currentPage = Math.min(maxPageNumber, paginationPage);
2536       var orderedPortsAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
2537
2538       portsAtCurrentPageCache = {};
2539       var orderedData = [];
2540
2541       orderedPortsAtCurrentPage.forEach(function (orderedPort) {
2542         var port = ports[orderedPort.id];
2543         portsAtCurrentPageCache[port.id] = port;
2544         orderedData.push({
2545           id: orderedPort.id,
2546           layer: port.layer,
2547           active: port.active,
2548           latitude: port.latitude.toLocaleString("en-US", {
2549             minimumFractionDigits: 4
2550           }),
2551           longitude: port.longitude.toLocaleString("en-US", {
2552             minimumFractionDigits: 4
2553           }),
2554           installed: 0,
2555           configured: 0,
2556           effective: 0
2557         });
2558       });
2559
2560       $timeout(function () {
2561         vm.gridOptions.data = orderedData;
2562         vm.gridOptions.totalItems = tempData.length;
2563       });
2564     }
2565
2566     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
2567     mwtnTopologyPhysicalPathData.events.subscribe("elementsChanged", function (data) {
2568       loadPage();
2569     });
2570
2571   }]);
2572
2573   mwtnTopologyApp.directive("mwtnTopologyNetworkElementsGrid", [function () {
2574     return {
2575       restrict: 'E',
2576       replace: false,
2577       controller: 'mwtnTopologyNetworkElementsGridController',
2578       controllerAs: 'vm',
2579       scope: {
2580
2581       },
2582       templateUrl: 'src/app/mwtnTopology/templates/networkElementsGrid.tpl.html'
2583     };
2584   }]);
2585
2586   mwtnTopologyApp.controller("mwtnTopologyLinksGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyPhysicalPathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyPhysicalPathData) {
2587     var vm = this;
2588
2589     // The page number to show in the grid.
2590     var paginationPage = 1;
2591     // The page size.
2592     var paginationPageSize = 100;
2593     // The grid column object with current sorting informations.
2594     var sortColumn = null;
2595     // The grid column object with current sorting informations.
2596     var gridFilters = [];
2597     // caches all sites at the current grid page
2598
2599     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
2600       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
2601       paginationPageSizes: [10, 25, 50, 100],
2602       paginationPageSize: paginationPageSize,
2603       useExternalPagination: true,
2604       useExternalFiltering: true,
2605       useExternalSorting: true,
2606       totalItems: 0,
2607       columnDefs: [{
2608         field: "id",
2609         type: "string",
2610         displayName: "Id"
2611       },
2612       {
2613         field: "source",
2614         type: "string",
2615         displayName: "PortA"
2616       },
2617       {
2618         field: "target",
2619         type: "string",
2620         displayName: "PortZ"
2621       },
2622       {
2623         field: "layer",
2624         type: "string",
2625         displayName: "Layer"
2626       },
2627       {
2628         field: "length",
2629         type: "string",
2630         displayName: "Length"
2631       },
2632       {
2633         field: "azimuthAZ",
2634         type: "string",
2635         displayName: "azimuthAZ"
2636       },
2637       {
2638         field: "azimuthZA",
2639         type: "string",
2640         displayName: "azimuthZA"
2641       }
2642       ],
2643       data: [],
2644       onRegisterApi: function (gridApi) {
2645         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
2646         vm.gridApi = gridApi;
2647
2648         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
2649           // Save the current sort column for later use.
2650           sortColumn = (!sortColumns || sortColumns.length === 0) ?
2651             null :
2652             sortColumns[0];
2653           loadPage();
2654         });
2655
2656         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
2657           // Save the pagination informations for later use.
2658           paginationPage = newPage;
2659           paginationPageSize = newPageSize;
2660           loadPage();
2661         });
2662
2663         vm.gridApi.core.on.filterChanged($scope, function () {
2664           // Save the all filters for later use.
2665           var filters = [];
2666           this.grid.columns.forEach(function (col, ind) {
2667             if (col.filters[0] && col.filters[0].term) {
2668               filters.push({
2669                 field: col.field,
2670                 term: col.filters[0].term
2671               });
2672             }
2673           });
2674           gridFilters = filters;
2675           loadPage();
2676         });
2677
2678         loadPage();
2679       }
2680     });
2681
2682     /**
2683      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
2684      */
2685     function loadPage() {
2686       // extract all ports        
2687       var ports = mwtnTopologyPhysicalPathData.elements.nodes.filter(function (node, ind, arr) {
2688         return node && node.data && node.data.type === 'port';
2689       }).reduce(function (acc, cur, ind, arr) {
2690         if (cur.data) acc[cur.data.id] = cur.data;
2691         return acc;
2692       }, {});
2693
2694       // extract all links        
2695       var links = mwtnTopologyPhysicalPathData.elements.edges.filter(function (node, ind, arr) {
2696         return true; // node && node.data && node.data.type === 'port';
2697       }).reduce(function (acc, cur, ind, arr) {
2698         if (cur.data) {
2699
2700           if (cur.data.layer === 'MWPS') {
2701             var portA = ports[cur.data.source];
2702             var portZ = ports[cur.data.target];
2703
2704             // calculate length
2705             cur.data.length = (portA && portZ) ? $mwtnTopology.getDistance(portA.latitude, portA.longitude, portZ.latitude, portZ.longitude) : 0;
2706             cur.data.azimuthAZ = (portA && portZ) ? $mwtnTopology.bearing(portA.latitude, portA.longitude, portZ.latitude, portZ.longitude) : 0;
2707             cur.data.azimuthZA = (portA && portZ) ? $mwtnTopology.bearing(portZ.latitude, portZ.longitude, portA.latitude, portA.longitude) : 0;
2708
2709           } else {
2710             cur.data.length = 0;
2711             cur.data.azimuthAZ = 0;
2712             cur.data.azimuthZA = 0;
2713           }
2714           acc[cur.data.id] = cur.data;
2715         }
2716         return acc;
2717       }, {});
2718
2719       // get all link ids
2720       var linkIds = Object.keys(links);
2721
2722       // apply the grid filters
2723       var tempData = linkIds.filter(function (linkId, ind, arr) {
2724         var link = links[linkId];
2725         return gridFilters.map(function (filter) {
2726           switch (filter.field) {
2727             case "length":
2728             case "azimuthAZ":
2729             case "azimuthZA":
2730               return true;
2731             default:
2732               return link[filter.field].contains(filter.term);
2733           }
2734         }).and(true);
2735       }).map(function (linkId) {
2736         var link = links[linkId];
2737         var orderBy;
2738
2739         if (!sortColumn || !sortColumn.sort.direction) {
2740           return {
2741             id: link.id
2742           }
2743         }
2744
2745         switch (sortColumn.field) {
2746           default:
2747             orderBy = link[sortColumn.field];
2748             break;
2749         }
2750
2751         return {
2752           id: link.id,
2753           orderBy: orderBy
2754         };
2755       });
2756
2757       if (sortColumn && sortColumn.sort.direction) {
2758         tempData.sort(function (left, right) {
2759           if (left === right || left.orderBy === right.orderBy) {
2760             return 0;
2761           }
2762           if (left.orderBy > right.orderBy) {
2763             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
2764           }
2765           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
2766         });
2767       }
2768
2769       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
2770       var currentPage = Math.min(maxPageNumber, paginationPage);
2771       var orderedLinksAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
2772
2773       linksAtCurrentPageCache = {};
2774       var orderedData = [];
2775
2776       orderedLinksAtCurrentPage.forEach(function (orderedLink) {
2777         var link = links[orderedLink.id];
2778         linksAtCurrentPageCache[link.id] = link;
2779         orderedData.push({
2780           id: orderedLink.id,
2781           source: link.source,
2782           target: link.target,
2783           layer: link.layer,
2784           length: link.length,
2785           azimuthAZ: link.azimuthAZ.toLocaleString("en-US", {
2786             minimumFractionDigits: 4
2787           }),
2788           azimuthZA: link.azimuthZA.toLocaleString("en-US", {
2789             minimumFractionDigits: 4
2790           }),
2791         });
2792       });
2793
2794       $timeout(function () {
2795         vm.gridOptions.data = orderedData;
2796         vm.gridOptions.totalItems = tempData.length;
2797       });
2798     }
2799
2800     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
2801     var subscription = mwtnTopologyPhysicalPathData.events.subscribe("elementsChanged", function (data) {
2802       loadPage();
2803       // to unsubscribe call subscription.remove();
2804     });
2805
2806   }]);
2807
2808   mwtnTopologyApp.directive("mwtnTopologyLinksGrid", [function () {
2809     return {
2810       restrict: 'E',
2811       replace: false,
2812       controller: 'mwtnTopologyLinksGridController',
2813       controllerAs: 'vm',
2814       scope: {
2815
2816       },
2817       templateUrl: 'src/app/mwtnTopology/templates/linksGrid.tpl.html'
2818     };
2819   }]);
2820
2821   /********************************************* Ethernet ***********************************/
2822
2823   mwtnTopologyApp.controller('mwtnTopologyEthernetViewController', ['$scope', '$q', '$timeout', '$state', '$window', '$mwtnTopology', function ($scope, $q, $timeout, $state, $window, $mwtnTopology) {
2824     var vm = this;
2825     vm.status = {
2826       topologyIsOpen: false,
2827       portsOpen: false,
2828       ethConnectionsIsOpen: false
2829     }
2830
2831     $scope.$watchCollection(function () { return [vm.status.topologyIsOpen, vm.status.portsOpen, vm.status.ethConnectionsIsOpen] }, function (newVal, oldVal) {
2832       if (newVal[1] || newVal[2]) {
2833         $timeout(function () {
2834           $window.dispatchEvent(new Event("resize"));
2835         });
2836       }
2837     });
2838
2839   }]);
2840
2841   mwtnTopologyApp.directive('mwtnTopologyEthernetView', function () {
2842     return {
2843       restrict: 'E',
2844       controller: 'mwtnTopologyEthernetViewController',
2845       controllerAs: 'vm',
2846       bindToController: true,
2847       templateUrl: 'src/app/mwtnTopology/templates/ethernetView.tpl.html',
2848       scope: {
2849
2850       }
2851     };
2852   });
2853
2854   mwtnTopologyApp.factory("mwtnTopologyEthernetPathData", function () {
2855     var colors = {
2856       root: '#f54',
2857       port: '#377',
2858       device: '#252',
2859       site: '#525',
2860       edge: '#49a',
2861       white: '#eed',
2862       grey: '#555',
2863       selected: '#ff0'
2864     };
2865
2866     var styles = [
2867       {
2868         selector: 'node',
2869         css: {
2870           'content': 'data(label)',
2871           'text-valign': 'center',
2872           'text-halign': 'center',
2873           'background-color': '#eeeeee',
2874           'border-color': '#000000',
2875           'border-width': '1px',
2876           'color': '#000000'
2877         }
2878       },
2879       {
2880         selector: 'node[type = "label"]',
2881         css: {
2882           'content': 'data(label)',
2883           'border-width': '0px',
2884           'background-color': '#ffffff',
2885           'font-size': '50px',
2886           'text-valign': 'bottom',
2887           'text-halign': 'right',
2888
2889         }
2890       },
2891
2892       {
2893         selector: 'node[layer = "MWPS"]',
2894         css: {
2895           'content': 'data(label)',
2896           'text-valign': 'center',
2897           'text-halign': 'center',
2898           'background-color': '#316ac5',
2899           'border-color': '#000000',
2900           'border-width': '1px',
2901           'color': '#ffffff'
2902         }
2903       },
2904       {
2905         selector: 'node[layer = "ETH-TTP"]',
2906         css: {
2907           'content': 'data(label)',
2908           'text-valign': 'center',
2909           'text-halign': 'center',
2910           'background-color': '#008800',
2911           'border-color': '#004400',
2912           'border-width': '1px',
2913           'color': '#ffffff'
2914         }
2915       },
2916       {
2917         selector: '$node > node',
2918         css: {
2919           'shape': 'roundrectangle',
2920           'padding-top': '10px',
2921           'padding-left': '10px',
2922           'padding-bottom': '10px',
2923           'padding-right': '10px',
2924           'text-valign': 'top',
2925           'text-halign': 'center',
2926           'background-color': '#eeeeee',
2927           'color': '#444444',
2928           'border-color': '#888888'
2929         }
2930       },
2931       {
2932         selector: 'node[type = "site"]',
2933         css: {
2934           'shape': 'roundrectangle',
2935           'padding-top': '10px',
2936           'padding-left': '10px',
2937           'padding-bottom': '10px',
2938           'padding-right': '10px',
2939           'text-valign': 'center',
2940           'text-halign': 'center',
2941           'background-color': '#fefefe',
2942           'color': '#444444',
2943           'border-color': '#888888',
2944           'font-weight': 'bold'
2945         }
2946       },
2947       {
2948         selector: 'node[type = "device"]',
2949         css: {
2950           'background-color': '#eeeeee',
2951           'background-opacity': '0.1',
2952           'border-color': '#888888',
2953           'border-width': '2px',
2954           'color': '#444444'
2955         }
2956       },
2957       {
2958         selector: 'node[type = "device"][active = "true"]',
2959         css: {
2960           'background-color': '#316ac5',
2961           'background-opacity': '0.1',
2962           'border-color': '#316ac5',
2963           'border-width': '2px',
2964           'color': '#444444'
2965         }
2966       },
2967       {
2968         selector: 'node[type = "port"][active = "true"]',
2969         css: {
2970           'background-opacity': '1.0',
2971         }
2972       },
2973       {
2974         selector: 'node[path = "working"]',
2975         css: {
2976           'background-color': '#FFA500',
2977           'border-color': '#FFA500',
2978           'border-width': '2px',
2979           'color': '#444444'
2980         }
2981       },
2982       {
2983         selector: 'node[path = "protection"]',
2984         css: {
2985           'background-color': '#ffffff',
2986           'border-color': '#FFA500',
2987           'border-width': '2px',
2988           'color': '#444444'
2989         }
2990       },
2991       {
2992         selector: '$node > node[path = "working"]',
2993         css: {
2994           'background-color': '#FFA500',
2995           'border-color': '#FFA500',
2996           'border-width': '2px',
2997           'color': '#444444'
2998         }
2999       },
3000       {
3001         selector: '$node > node[path = "protection"]',
3002         css: {
3003           'border-color': '#FFA500',
3004           'border-width': '2px',
3005           'color': '#444444'
3006         }
3007       },
3008       {
3009         selector: 'node[active = "false"]',
3010         css: {
3011           'background-opacity': '0.3',
3012           'border-opacity': '0.5'
3013         }
3014       },
3015       {
3016         selector: 'edge',
3017         css: {
3018           'content': 'data(id)',
3019           'target-arrow-shape': 'triangle',
3020           'line-color': '#666666',
3021           'color': '#444444'
3022         }
3023       },
3024       {
3025         selector: 'edge[active = "false"]',
3026         css: {
3027           'line-color': '#cccccc',
3028           'text-opacity': '0.9'
3029         }
3030       },
3031       {
3032         selector: 'edge[layer = "ETC"]',
3033         css: {
3034           'content': 'data(id)',
3035           'target-arrow-shape': 'triangle',
3036           'width': '3px',
3037           'line-color': '#316ac5',
3038           'color': '#444444'
3039         }
3040       },
3041       {
3042         selector: 'edge[layer = "ETH"]',
3043         css: {
3044           'content': '',
3045           'target-arrow-shape': 'triangle',
3046           'width': '2px',
3047           'line-color': '#FFA500',
3048           'color': '#000000'
3049         }
3050       }, {
3051         selector: 'edge[layer = "ETC"][active = "false"]',
3052         css: {
3053           'line-color': '#C0D1EC',
3054           'text-opacity': '0.9'
3055         }
3056       },
3057
3058       {
3059         selector: 'edge[layer = "ETH"][path = "false"]',
3060         css: {
3061           'opacity': '0.0'
3062         }
3063       },
3064       {
3065         selector: 'edge[path = "working"]',
3066         css: {
3067           'line-color': '#FFA500',
3068           'width': '5px',
3069           'opacity': '1.0'
3070         }
3071       },
3072       {
3073         selector: 'edge[path = "protection"]',
3074         css: {
3075           'line-color': '#FFA500',
3076           'line-style': 'dashed',
3077           'width': '3px',
3078           'opacity': '1.0'
3079         }
3080       },
3081       {
3082         selector: ':selected',
3083         css: {
3084           'background-color': 'black',
3085           'line-color': 'black',
3086           'target-arrow-color': 'black',
3087           'source-arrow-color': 'black'
3088         }
3089       }
3090     ];
3091
3092     var elements = {
3093       nodes: [
3094         { data: { id: 'label', label : '' , type: 'label' }, position: { x: -259, y: -167 } },
3095
3096         { data: { id: 'north', label : 'north' , type: 'site', latitude: 50.734916, longitude: 7.137636 } },
3097         { data: { id: 'north-east', label : 'north-east' , type: 'site', latitude: 50.733028, longitude: 7.151086 } },
3098         { data: { id: 'north-west', label : 'north-west' , type: 'site', latitude: 50.730230, longitude: 7.126017 } },
3099         { data: { id: 'east', label : 'east' , type: 'site', latitude: 50.725672, longitude: 7.158488 } },
3100         { data: { id: 'west', label : 'west' , type: 'site', latitude: 50.721914, longitude: 7.120521 } },
3101         { data: { id: 'south-east', label : 'south-east' , type: 'site', latitude: 50.717158, longitude: 7.155506 } },
3102         { data: { id: 'south-west', label : 'south-west' , type: 'site', latitude: 50.714359, longitude: 7.130437 } },
3103         { data: { id: 'south', label : 'south' , type: 'site', latitude: 50.712472, longitude: 7.143887 } },
3104
3105         { data: { id: 'ADVA-Y', label : 'ADVA-Y' , parent : 'north-east', type: 'device', active: 'true' , latitude: 50.733028, longitude: 7.151086 } },
3106         { data: { id: 'ADVA-Z', label : 'ADVA-Z' , parent : 'south', type: 'device', active: 'true' , latitude: 50.712472, longitude: 7.143887 } },
3107         { data: { id: 'Aviat-A', label : 'Aviat-A' , parent : 'north-east', type: 'device', active: 'true' , latitude: 50.733028, longitude: 7.151086 } },
3108         { data: { id: 'Aviat-Z', label : 'Aviat-Z' , parent : 'east', type: 'device', active: 'true' , latitude: 50.725672, longitude: 7.158488 } },
3109         { data: { id: 'Ceragon-A', label : 'Ceragon-A' , parent : 'north-west', type: 'device', active: 'true' , latitude: 50.730230, longitude: 7.126017 } },
3110         { data: { id: 'Ceragon-Z', label : 'Ceragon-Z' , parent : 'west', type: 'device', active: 'true' , latitude: 50.721914, longitude: 7.120521 } },
3111         { data: { id: 'DragonWave-A', label : 'DragonWave-A' , parent : 'south-west', type: 'device', active: 'true' , latitude: 50.714359, longitude: 7.130437 } },
3112         { data: { id: 'DragonWave-Z', label : 'DragonWave-Z' , parent : 'south', type: 'device', active: 'true' , latitude: 50.712472, longitude: 7.143887 } },
3113         { data: { id: 'ELVA-1-A', label : 'ELVA-1-A' , parent : 'north', type: 'device', active: 'true' , latitude: 50.734916, longitude: 7.137636 } },
3114         { data: { id: 'ELVA-1-Z', label : 'ELVA-1-Z' , parent : 'south-west', type: 'device', active: 'true' , latitude: 50.714359, longitude: 7.130437 } },
3115         { data: { id: 'Ericsson-A', label : 'Ericsson-A' , parent : 'north-east', type: 'device', active: 'true' , latitude: 50.733028, longitude: 7.151086 } },
3116         { data: { id: 'Ericsson-Z', label : 'Ericsson-Z' , parent : 'east', type: 'device', active: 'true' , latitude: 50.725672, longitude: 7.158488 } },
3117         { data: { id: 'Fujitsu-A', label : 'Fujitsu-A' , parent : 'east', type: 'device', active: 'true' , latitude: 50.725672, longitude: 7.158488 } },
3118         { data: { id: 'Fujitsu-Z', label : 'Fujitsu-Z' , parent : 'south-east', type: 'device', active: 'true' , latitude: 50.717158, longitude: 7.155506 } },
3119         { data: { id: 'Huawei-A', label : 'Huawei-A' , parent : 'south-west', type: 'device', active: 'true' , latitude: 50.714359, longitude: 7.130437 } },
3120         { data: { id: 'Huawei-Z', label : 'Huawei-Z' , parent : 'south', type: 'device', active: 'true' , latitude: 50.712472, longitude: 7.143887 } },
3121         { data: { id: 'Intracom-A', label : 'Intracom-A' , parent : 'south', type: 'device', active: 'true' , latitude: 50.712472, longitude: 7.143887 } },
3122         { data: { id: 'Intracom-Z', label : 'Intracom-Z' , parent : 'south-east', type: 'device', active: 'true' , latitude: 50.717158, longitude: 7.155506 } },
3123         { data: { id: 'NEC-A', label : 'NEC-A' , parent : 'north', type: 'device', active: 'true' , latitude: 50.734916, longitude: 7.137636 } },
3124         { data: { id: 'NEC-Z', label : 'NEC-Z' , parent : 'north-east', type: 'device', active: 'true' , latitude: 50.733028, longitude: 7.151086 } },
3125         { data: { id: 'Nokia-A', label : 'Nokia-A' , parent : 'west', type: 'device', active: 'fasel' , latitude: 50.721914, longitude: 7.120521 } },
3126         { data: { id: 'Nokia-Z', label : 'Nokia-Z' , parent : 'south-west', type: 'device', active: 'true' , latitude: 50.714359, longitude: 7.130437 } },
3127         { data: { id: 'SIAE-A', label : 'SIAE-A' , parent : 'south', type: 'device', active: 'true' , latitude: 50.712472, longitude: 7.143887 } },
3128         { data: { id: 'SIAE-Z', label : 'SIAE-Z' , parent : 'south-east', type: 'device', active: 'true' , latitude: 50.717158, longitude: 7.155506 } },
3129         { data: { id: 'ZTE-A', label : 'ZTE-A' , parent : 'north-west', type: 'device', active: 'true' , latitude: 50.730230, longitude: 7.126017 } },
3130         { data: { id: 'ZTE-Z', label : 'ZTE-Z' , parent : 'north', type: 'device', active: 'true' , latitude: 50.734916, longitude: 7.137636 } },
3131
3132         { data: { id: 'Aviat-Z#5', label : '#5' , parent : 'Aviat-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1984, y: 390 } },
3133         { data: { id: 'Aviat-Z#6', label : '#6' , parent : 'Aviat-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1984, y: 321 } },
3134         { data: { id: 'Ericsson-Z#5', label : '#5' , parent : 'Ericsson-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1777, y: 393 } },
3135         { data: { id: 'Ericsson-Z#6', label : '#6' , parent : 'Ericsson-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1765, y: 325 } },
3136         { data: { id: 'Fujitsu-A#5', label : '#5' , parent : 'Fujitsu-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1859, y: 567 } },
3137         { data: { id: 'Fujitsu-A#6', label : '#6' , parent : 'Fujitsu-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1859, y: 647 } },
3138         { data: { id: 'ELVA-1-A#2', label : '#2' , parent : 'ELVA-1-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 895, y: 150 } },
3139         { data: { id: 'ELVA-1-A#6', label : '#6' , parent : 'ELVA-1-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 815, y: 150 } },
3140         { data: { id: 'NEC-A#3', label : '#3' , parent : 'NEC-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 955, y: -52 } },
3141         { data: { id: 'NEC-A#6', label : '#6' , parent : 'NEC-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 1035, y: -52 } },
3142         { data: { id: 'ZTE-Z#4', label : '#4' , parent : 'ZTE-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 707, y: -67 } },
3143         { data: { id: 'ZTE-Z#5', label : '#5' , parent : 'ZTE-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 747, y: -27 } },
3144         { data: { id: 'ZTE-Z#6', label : '#6' , parent : 'ZTE-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.734916, longitude: 7.137636 }, position: { x: 667, y: -27 } },
3145         // { data: { id: 'ADVA-Y#1', label : '#1' , parent : 'ADVA-Y' , type:'port', layer:'ETY', active:'true', latitude:50.733028, longitude:7.151086}, position: { x: 1392, y: 229 } },
3146         { data: { id: 'ADVA-Y#2', label : '#2' , parent : 'ADVA-Y' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: 172 } },
3147         { data: { id: 'ADVA-Y#3', label : '#3' , parent : 'ADVA-Y' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1392, y: 172 } },
3148         { data: { id: 'ADVA-Y#4', label : '#4' , parent : 'ADVA-Y' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: 229 } },
3149         { data: { id: 'Aviat-A#5', label : '#5' , parent : 'Aviat-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1654, y: -47 } },
3150         { data: { id: 'Aviat-A#6', label : '#6' , parent : 'Aviat-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1654, y: 22 } },
3151         { data: { id: 'Ericsson-A#4', label : '#4' , parent : 'Ericsson-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1605, y: 229 } },
3152         { data: { id: 'Ericsson-A#5', label : '#5' , parent : 'Ericsson-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1605, y: 172 } },
3153         { data: { id: 'Ericsson-A#6', label : '#6' , parent : 'Ericsson-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1664, y: 226 } },
3154         // { data: { id: 'NEC-Z#3', label : '#3' , parent : 'NEC-Z' , type:'port', layer:'ETY', active:'true', latitude:50.733028, longitude:7.151086}, position: { x: 1392, y: 16 } },
3155         { data: { id: 'NEC-Z#4', label : '#4' , parent : 'NEC-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: 16 } },
3156         { data: { id: 'NEC-Z#2', label : '#2' , parent : 'NEC-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: -41 } },
3157         { data: { id: 'NEC-Z#6', label : '#6' , parent : 'NEC-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1382, y: -23 } },
3158         { data: { id: 'Ceragon-A#5', label : '#5' , parent : 'Ceragon-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.730230, longitude: 7.126017 }, position: { x: 195, y: 312 } },
3159         { data: { id: 'Ceragon-A#6', label : '#6' , parent : 'Ceragon-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.730230, longitude: 7.126017 }, position: { x: 136, y: 366 } },
3160         { data: { id: 'ZTE-A#5', label : '#5' , parent : 'ZTE-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.730230, longitude: 7.126017 }, position: { x: 345, y: 108 } },
3161         { data: { id: 'ZTE-A#6', label : '#6' , parent : 'ZTE-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.730230, longitude: 7.126017 }, position: { x: 408, y: 99 } },
3162         { data: { id: 'ADVA-Z#1', label : '#1' , parent : 'ADVA-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1139, y: 1001 } },
3163         // { data: { id: 'ADVA-Z#2', label : '#2' , parent : 'ADVA-Z' , type:'port', layer:'ETY', active:'true', latitude:50.712472, longitude:7.143887}, position: { x: 1196, y: 944 } },
3164         // { data: { id: 'ADVA-Z#3', label : '#3' , parent : 'ADVA-Z' , type:'port', layer:'ETY', active:'true', latitude:50.712472, longitude:7.143887}, position: { x: 1139, y: 944 } },
3165         { data: { id: 'ADVA-Z#4', label : '#4' , parent : 'ADVA-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1196, y: 1001 } },
3166         { data: { id: 'DragonWave-Z#2', label : '#2' , parent : 'DragonWave-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1133, y: 1252 } },
3167         { data: { id: 'DragonWave-Z#6', label : '#6' , parent : 'DragonWave-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1053, y: 1252 } },
3168         { data: { id: 'Huawei-Z#3', label : '#3' , parent : 'Huawei-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1046, y: 1094 } },
3169         // { data: { id: 'Huawei-Z#4', label : '#4' , parent : 'Huawei-Z' , type:'port', layer:'ETY', active:'true', latitude:50.712472, longitude:7.143887}, position: { x: 989, y: 1094 } },
3170         { data: { id: 'Huawei-Z#5', label : '#5' , parent : 'Huawei-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1058, y: 1123 } },
3171         { data: { id: 'Huawei-Z#6', label : '#6' , parent : 'Huawei-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 978, y: 1123 } },
3172         { data: { id: 'Intracom-A#2', label : '#2' , parent : 'Intracom-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1203, y: 1252 } },
3173         { data: { id: 'Intracom-A#6', label : '#6' , parent : 'Intracom-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1283, y: 1252 } },
3174         { data: { id: 'SIAE-A#4', label : '#4' , parent : 'SIAE-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1289, y: 1094 } },
3175         { data: { id: 'SIAE-A#5', label : '#5' , parent : 'SIAE-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1278, y: 1123 } },
3176         { data: { id: 'SIAE-A#6', label : '#6' , parent : 'SIAE-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1346, y: 1094 } },
3177         { data: { id: 'Fujitsu-Z#5', label : '#5' , parent : 'Fujitsu-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1855, y: 821 } },
3178         { data: { id: 'Fujitsu-Z#6', label : '#6' , parent : 'Fujitsu-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1855, y: 741 } },
3179         { data: { id: 'Intracom-Z#5', label : '#5' , parent : 'Intracom-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1755, y: 998 } },
3180         { data: { id: 'Intracom-Z#6', label : '#6' , parent : 'Intracom-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1714, y: 1018 } },
3181         { data: { id: 'SIAE-Z#4', label : '#4' , parent : 'SIAE-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1590, y: 784 } },
3182         { data: { id: 'SIAE-Z#5', label : '#5' , parent : 'SIAE-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1647, y: 784 } },
3183         { data: { id: 'SIAE-Z#6', label : '#6' , parent : 'SIAE-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1588, y: 838 } },
3184         { data: { id: 'DragonWave-A#2', label : '#2' , parent : 'DragonWave-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 455, y: 1178 } },
3185         { data: { id: 'DragonWave-A#6', label : '#6' , parent : 'DragonWave-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 535, y: 1178 } },
3186         { data: { id: 'ELVA-1-Z#2', label : '#2' , parent : 'ELVA-1-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 455, y: 878 } },
3187         { data: { id: 'ELVA-1-Z#6', label : '#6' , parent : 'ELVA-1-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 535, y: 878 } },
3188         { data: { id: 'Huawei-A#5', label : '#5' , parent : 'Huawei-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 605, y: 1028 } },
3189         { data: { id: 'Huawei-A#6', label : '#6' , parent : 'Huawei-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 685, y: 1028 } },
3190         { data: { id: 'Nokia-Z33', label : '33' , parent : 'Nokia-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 385, y: 1028 } },
3191         { data: { id: 'Nokia-Z#6', label : '#6' , parent : 'Nokia-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.714359, longitude: 7.130437 }, position: { x: 305, y: 1028 } },
3192         { data: { id: 'Ceragon-Z#4', label : '#4' , parent : 'Ceragon-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.721914, longitude: 7.120521 }, position: { x: -159, y: 547 } },
3193         { data: { id: 'Ceragon-Z#5', label : '#5' , parent : 'Ceragon-Z' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.721914, longitude: 7.120521 }, position: { x: -159, y: 604 } },
3194         { data: { id: 'Ceragon-Z#6', label : '#6' , parent : 'Ceragon-Z' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.721914, longitude: 7.120521 }, position: { x: -130, y: 536 } },
3195         // { data: { id: 'Nokia-A13', label : '13' , parent : 'Nokia-A' , type:'port', layer:'ETY', active:'true', latitude:50.721914, longitude:7.120521}, position: { x: 40, y: 801 } },
3196         // { data: { id: 'Nokia-A34', label : '34' , parent : 'Nokia-A' , type:'port', layer:'ETY', active:'true', latitude:50.721914, longitude:7.120521}, position: { x: 28, y: 772 } },
3197         { data: { id: 'Nokia-A11', label : '11' , parent : 'Nokia-A' , type: 'port', layer: 'ETY', active: 'true', latitude: 50.721914, longitude: 7.120521 }, position: { x: -29, y: 772 } },
3198         { data: { id: 'Nokia-A#6', label : '#6' , parent : 'Nokia-A' , type: 'port', layer: 'ETC', active: 'true', latitude: 50.721914, longitude: 7.120521 }, position: { x: 28, y: 829 } },
3199
3200         { data: { id: 'Spirent#1', label : '#1' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service13' }, position: { x: 707, y: -167 } },
3201         { data: { id: 'Spirent#2', label : '#2' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service24' }, position: { x: -259, y: 547 } },
3202         { data: { id: 'Spirent#3', label : '#3' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service13' }, position: { x: 1292, y: 172 } },
3203         { data: { id: 'Spirent#4', label : '#4' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service24' }, position: { x: 1490, y: 784 } },
3204         { data: { id: 'Spirent#5', label : '#5' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service56' }, position: { x: 1754, y: -47 } },
3205         { data: { id: 'Spirent#6', label : '#6' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service56' }, position: { x: 455, y: 1278 } },
3206         { data: { id: 'Spirent#7', label : '#7' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service78' }, position: { x: 895, y: 250 } },
3207         { data: { id: 'Spirent#8', label : '#8' , parent : 'Spirent' , type: 'host', layer: 'ETH-TTP', active: 'true', service: 'service78' }, position: { x: 455, y: 778 } },
3208
3209       ],
3210       edges: [
3211
3212         { data: { id: '21', source: 'Aviat-A#6', target: 'Aviat-Z#6', label: '21', layer: 'ETC' , active: 'true' } },
3213         { data: { id: '31', source: 'Ceragon-A#6', target: 'Ceragon-Z#6', label: '31', layer: 'ETC' , active: 'true' } },
3214         { data: { id: '41', source: 'DragonWave-A#6', target: 'DragonWave-Z#6', label: '41', layer: 'ETC' , active: 'true' } },
3215         { data: { id: '121', source: 'ELVA-1-A#6', target: 'ELVA-1-Z#6', label: '121' , layer: 'ETC' , active: 'true' } },
3216         { data: { id: 'ERI1', source: 'Ericsson-A#6', target: 'Ericsson-Z#6', label: 'ERI1', layer: 'ETC' , active: 'true' } },
3217         { data: { id: '61', source: 'Fujitsu-A#6', target: 'Fujitsu-Z#6', label: '61', layer: 'ETC' , active: 'true' } },
3218         { data: { id: '71', source: 'Huawei-A#6', target: 'Huawei-Z#6', label: '71', layer: 'ETC' , active: 'true' } },
3219         { data: { id: '131', source: 'Intracom-A#6', target: 'Intracom-Z#6', label: '131', layer: 'ETC' , active: 'true' } },
3220         { data: { id: '81', source: 'NEC-A#6', target: 'NEC-Z#6', label: '81', layer: 'ETC' , active: 'true' } },
3221         { data: { id: '91', source: 'Nokia-A#6', target: 'Nokia-Z#6', label: '91', layer: 'ETC' , active: 'true' } },
3222         { data: { id: '101', source: 'SIAE-A#6', target: 'SIAE-Z#6', label: '101', layer: 'ETC' , active: 'true' } },
3223         { data: { id: '111', source: 'ZTE-A#6', target: 'ZTE-Z#6', label: '111', layer: 'ETC' , active: 'true' } },
3224
3225         // { data: { id: 'ETY01', source: 'ADVA-A#1', target: 'Nokia-A34', label: 'ADVA-A#1-Nokia-A34' , layer: 'ETY' , active: 'true' } },
3226         // { data: { id: 'ETY02', source: 'ADVA-A#2', target: 'ZTE-A#4', label: 'ADVA-A#2-ZTE-A#4' , layer: 'ETY' , active: 'false' } },
3227         // { data: { id: 'ETY03', source: 'ADVA-B#1', target: 'Nokia-A13', label: 'ADVA-B#1-Nokia-A13' , layer: 'ETY' , active: 'true' } },
3228         // { data: { id: 'ETY04', source: 'ADVA-B#2', target: 'ZTE-A#3', label: 'ADVA-B#2-ZTE-A#3' , layer: 'ETY' , active: 'true' } },
3229         // { data: { id: 'ETY05', source: 'ADVA-Y#1', target: 'Huawei-Z#4', label: 'ADVA-Y#1-Huawei-Z#4' , layer: 'ETY' , active: 'true' } },
3230         { data: { id: 'ETY06', source: 'ADVA-Y#2', target: 'NEC-Z#4', label: 'ADVA-Y#2-NEC-Z#4' , layer: 'ETY' , active: 'true' } },
3231         { data: { id: 'ETY07', source: 'ADVA-Y#3', target: 'Spirent#3', label: 'ADVA-Y#3-Spirent#3' , layer: 'ETY' , active: 'true' } },
3232         { data: { id: 'ETY08', source: 'ADVA-Y#4', target: 'Ericsson-A#4', label: 'ADVA-Y#4-Ericsson-A#4' , layer: 'ETY' , active: 'true' } },
3233         { data: { id: 'ETY09', source: 'ADVA-Z#1', target: 'Huawei-Z#3', label: 'ADVA-Z#1-Huawei-Z#3' , layer: 'ETY' , active: 'true' } },
3234         // { data: { id: 'ETY10', source: 'ADVA-Z#2', target: 'NEC-Z#3', label: 'ADVA-Z#2-NEC-Z#3' , layer: 'ETY' , active: 'true' } },
3235         { data: { id: 'ETY11', source: 'ADVA-Z#4', target: 'SIAE-A#4', label: 'ADVA-Z#4-SIAE-A#4' , layer: 'ETY' , active: 'true' } },
3236         { data: { id: 'ETY12', source: 'Aviat-A#5', target: 'Spirent#5', label: 'Aviat-A#5-Spirent#5' , layer: 'ETY' , active: 'true' } },
3237         { data: { id: 'ETY13', source: 'Aviat-Z#5', target: 'Fujitsu-A#5', label: 'Aviat-Z#5-Fujitsu-A#5' , layer: 'ETY' , active: 'true' } },
3238         { data: { id: 'ETY14', source: 'Ceragon-A#5', target: 'ZTE-A#5', label: 'Ceragon-A#5-ZTE-A#5' , layer: 'ETY' , active: 'true' } },
3239         { data: { id: 'ETY15', source: 'Ceragon-Z#4', target: 'Spirent#2', label: 'Ceragon-Z#4-Spirent#2' , layer: 'ETY' , active: 'true' } },
3240         { data: { id: 'ETY16', source: 'Ceragon-Z#5', target: 'Nokia-A11', label: 'Ceragon-Z#5-Nokia-A11' , layer: 'ETY' , active: 'true' } },
3241         { data: { id: 'ETY17', source: 'DragonWave-A#2', target: 'Spirent#6', label: 'DragonWave-A#2-Spirent#6' , layer: 'ETY' , active: 'true' } },
3242         { data: { id: 'ETY18', source: 'DragonWave-Z#2', target: 'Intracom-A#2', label: 'DragonWave-Z#2-Intracom-A#2' , layer: 'ETY' , active: 'true' } },
3243         { data: { id: 'ETY19', source: 'ELVA-1-A#2', target: 'Spirent#7', label: 'ELVA-1-A#2-Spirent#7' , layer: 'ETY' , active: 'true' } },
3244         { data: { id: 'ETY20', source: 'ELVA-1-Z#2', target: 'Spirent#8', label: 'ELVA-1-Z#2-Spirent#8' , layer: 'ETY' , active: 'true' } },
3245         { data: { id: 'ETY21', source: 'Ericsson-A#5', target: 'NEC-Z#2', label: 'Ericsson-A#5-NEC-Z#2' , layer: 'ETY' , active: 'true' } },
3246         { data: { id: 'ETY22', source: 'Ericsson-Z#5', target: 'SIAE-Z#5', label: 'Ericsson-Z#5-SIAE-Z#5' , layer: 'ETY' , active: 'true' } },
3247         { data: { id: 'ETY23', source: 'Fujitsu-Z#5', target: 'Intracom-Z#5', label: 'Fujitsu-Z#5-Intracom-Z#5' , layer: 'ETY' , active: 'true' } },
3248         { data: { id: 'ETY24', source: 'Huawei-A#5', target: 'Nokia-Z33', label: 'Huawei-A#5-Nokia-Z33' , layer: 'ETY' , active: 'true' } },
3249         { data: { id: 'ETY25', source: 'Huawei-Z#5', target: 'SIAE-A#5', label: 'Huawei-Z#5-SIAE-A#5' , layer: 'ETY' , active: 'true' } },
3250         { data: { id: 'ETY26', source: 'NEC-A#3', target: 'ZTE-Z#5', label: 'NEC-A#3-ZTE-Z#5' , layer: 'ETY' , active: 'true' } },
3251         { data: { id: 'ETY27', source: 'SIAE-Z#4', target: 'Spirent#4', label: 'SIAE-Z#4-Spirent#4' , layer: 'ETY' , active: 'true' } },
3252         { data: { id: 'ETY28', source: 'ZTE-Z#4', target: 'Spirent#1', label: 'Spirent#1-ZTE-Z#4' , layer: 'ETY' , active: 'true' } },
3253
3254         {  data:  {  id:  'ADVA-Y#2-ETH-13<->ADVA-Y#3-ETH-13',  source:  'ADVA-Y#2',  target:  'ADVA-Y#3',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'protection'  }  },
3255         {  data:  {  id:  'ADVA-Y#2-ETH-24<->ADVA-Y#4-ETH-24',  source:  'ADVA-Y#2',  target:  'ADVA-Y#4',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3256         {  data:  {  id:  'ADVA-Y#3-ETH-13<->ADVA-Y#4-ETH-13',  source:  'ADVA-Y#3',  target:  'ADVA-Y#4',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3257         {  data:  {  id:  'Aviat-A#5-ETH-56<->Aviat-A#6-ETH-56',  source:  'Aviat-A#5',  target:  'Aviat-A#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3258         {  data:  {  id:  'Aviat-Z#5-ETH-56<->Aviat-Z#6-ETH-56',  source:  'Aviat-Z#5',  target:  'Aviat-Z#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3259         {  data:  {  id:  'Ceragon-A#5-ETH-13<->Ceragon-A#6-ETH-13',  source:  'Ceragon-A#5',  target:  'Ceragon-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3260         {  data:  {  id:  'Ceragon-A#5-ETH-24<->Ceragon-A#6-ETH-24',  source:  'Ceragon-A#5',  target:  'Ceragon-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3261         {  data:  {  id:  'Ceragon-Z#4-ETH-24<->Ceragon-Z#5-ETH-24',  source:  'Ceragon-Z#4',  target:  'Ceragon-Z#5',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3262         {  data:  {  id:  'Ceragon-Z#4-ETH-24<->Ceragon-Z#6-ETH-24',  source:  'Ceragon-Z#4',  target:  'Ceragon-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3263         {  data:  {  id:  'Ceragon-Z#5-ETH-13<->Ceragon-Z#6-ETH-13',  source:  'Ceragon-Z#5',  target:  'Ceragon-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3264         {  data:  {  id:  'DragonWave-A#2-ETH-56<->DragonWave-A#6-ETH-56',  source:  'DragonWave-A#2',  target:  'DragonWave-A#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3265         {  data:  {  id:  'DragonWave-Z#2-ETH-56<->DragonWave-Z#6-ETH-56',  source:  'DragonWave-Z#2',  target:  'DragonWave-Z#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3266         {  data:  {  id:  'ELVA-1-A#2-ETH-78<->ELVA-1-A#6-ETH-78',  source:  'ELVA-1-A#2',  target:  'ELVA-1-A#6',  label:  'service78' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service78' ,  rule:  'working'  }  },
3267         {  data:  {  id:  'ELVA-1-Z#2-ETH-78<->ELVA-1-Z#6-ETH-78',  source:  'ELVA-1-Z#2',  target:  'ELVA-1-Z#6',  label:  'service78' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service78' ,  rule:  'working'  }  },
3268         {  data:  {  id:  'Ericsson-A#4-ETH-13<->Ericsson-A#6-ETH-13',  source:  'Ericsson-A#4',  target:  'Ericsson-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3269         {  data:  {  id:  'Ericsson-A#4-ETH-24<->Ericsson-A#6-ETH-24',  source:  'Ericsson-A#4',  target:  'Ericsson-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3270         {  data:  {  id:  'Ericsson-Z#5-ETH-13<->Ericsson-Z#6-ETH-13',  source:  'Ericsson-Z#5',  target:  'Ericsson-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3271         {  data:  {  id:  'Ericsson-Z#5-ETH-24<->Ericsson-Z#6-ETH-24',  source:  'Ericsson-Z#5',  target:  'Ericsson-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3272         {  data:  {  id:  'Fujitsu-A#5-ETH-56<->Fujitsu-A#6-ETH-56',  source:  'Fujitsu-A#5',  target:  'Fujitsu-A#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3273         {  data:  {  id:  'Fujitsu-Z#5-ETH-56<->Fujitsu-Z#6-ETH-56',  source:  'Fujitsu-Z#5',  target:  'Fujitsu-Z#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3274         {  data:  {  id:  'Huawei-A#5-ETH-13<->Huawei-A#6-ETH-13',  source:  'Huawei-A#5',  target:  'Huawei-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3275         {  data:  {  id:  'Huawei-A#5-ETH-24<->Huawei-A#6-ETH-24',  source:  'Huawei-A#5',  target:  'Huawei-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3276         {  data:  {  id:  'Huawei-Z#5-ETH-13<->Huawei-Z#6-ETH-13',  source:  'Huawei-Z#5',  target:  'Huawei-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3277         {  data:  {  id:  'Huawei-Z#5-ETH-24<->Huawei-Z#6-ETH-24',  source:  'Huawei-Z#5',  target:  'Huawei-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3278         {  data:  {  id:  'Intracom-A#2-ETH-56<->Intracom-A#6-ETH-56',  source:  'Intracom-A#2',  target:  'Intracom-A#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3279         {  data:  {  id:  'Intracom-Z#5-ETH-56<->Intracom-Z#6-ETH-56',  source:  'Intracom-Z#5',  target:  'Intracom-Z#6',  label:  'service56' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service56' ,  rule:  'working'  }  },
3280         {  data:  {  id:  'NEC-A#3-ETH-13<->NEC-A#6-ETH-13',  source:  'NEC-A#3',  target:  'NEC-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'protection'  }  },
3281         {  data:  {  id:  'NEC-A#3-ETH-24<->NEC-A#6-ETH-24',  source:  'NEC-A#3',  target:  'NEC-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3282         {  data:  {  id:  'NEC-Z#4-ETH-13<->NEC-Z#6-ETH-13',  source:  'NEC-Z#4',  target:  'NEC-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'protection'  }  },
3283         {  data:  {  id:  'NEC-Z#4-ETH-24<->NEC-Z#6-ETH-24',  source:  'NEC-Z#4',  target:  'NEC-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3284         {  data:  {  id:  'Nokia-A11-ETH-13<->Nokia-A#6-ETH-13',  source:  'Nokia-A11',  target:  'Nokia-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3285         {  data:  {  id:  'Nokia-A11-ETH-24<->Nokia-A#6-ETH-24',  source:  'Nokia-A11',  target:  'Nokia-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3286         {  data:  {  id:  'Nokia-Z33-ETH-13<->Nokia-Z#6-ETH-13',  source:  'Nokia-Z33',  target:  'Nokia-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3287         {  data:  {  id:  'Nokia-Z33-ETH-24<->Nokia-Z#6-ETH-24',  source:  'Nokia-Z33',  target:  'Nokia-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3288         {  data:  {  id:  'SIAE-A#5-ETH-13<->SIAE-A#6-ETH-13',  source:  'SIAE-A#5',  target:  'SIAE-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3289         {  data:  {  id:  'SIAE-A#5-ETH-24<->SIAE-A#6-ETH-24',  source:  'SIAE-A#5',  target:  'SIAE-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3290         {  data:  {  id:  'SIAE-Z#4-ETH-24<->SIAE-Z#6-ETH-24',  source:  'SIAE-Z#4',  target:  'SIAE-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'working'  }  },
3291         {  data:  {  id:  'SIAE-Z#4-ETH-24<->SIAE-Z#5-ETH-24',  source:  'SIAE-Z#4',  target:  'SIAE-Z#5',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3292         {  data:  {  id:  'SIAE-Z#5-ETH-13<->SIAE-Z#6-ETH-13',  source:  'SIAE-Z#5',  target:  'SIAE-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3293         {  data:  {  id:  'ZTE-A#5-ETH-13<->ZTE-A#6-ETH-13',  source:  'ZTE-A#5',  target:  'ZTE-A#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3294         {  data:  {  id:  'ZTE-A#5-ETH-24<->ZTE-A#6-ETH-24',  source:  'ZTE-A#5',  target:  'ZTE-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3295         {  data:  {  id:  'ZTE-A#5-ETH-24<->ZTE-A#6-ETH-24',  source:  'ZTE-A#5',  target:  'ZTE-A#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3296         {  data:  {  id:  'ZTE-Z#4-ETH-13<->ZTE-Z#6-ETH-13',  source:  'ZTE-Z#4',  target:  'ZTE-Z#6',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'working'  }  },
3297         {  data:  {  id:  'ZTE-Z#4-ETH-13<->ZTE-Z#5-ETH-13',  source:  'ZTE-Z#4',  target:  'ZTE-Z#5',  label:  'service13' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service13' ,  rule:  'protection'  }  },
3298         {  data:  {  id:  'ZTE-Z#5-ETH-24<->ZTE-Z#6-ETH-24',  source:  'ZTE-Z#5',  target:  'ZTE-Z#6',  label:  'service24' ,  layer:  'ETH' ,  active:  'true' ,  path:  'false'  ,  service:  'service24' ,  rule:  'protection'  }  },
3299       ]
3300     };
3301
3302     var events = eventsFabric();
3303
3304     var result = {
3305       colors: colors,
3306       elements: elements,
3307       styles: styles,
3308       events: events
3309     };
3310
3311     var someMethodChangingTheElements = function () {
3312       // @Martin: hier kannst Du die Elements ändern, anschließend mußt Du das Ereignis veröffentlichen
3313       //          das Ereigniss wird in der Directive aufgefangen und die Grig wird neu gezeichnet
3314
3315       // Hinweis: Die Reihenfolge muss so bleiben und du kannst NUR result.elements ändern.
3316
3317       events.publish("elementsChanged", {
3318         elements: result.elements
3319       });
3320     };
3321
3322     return result;
3323   });
3324
3325   mwtnTopologyApp.directive("mwtnTopologyEthernetPathGraph", ["mwtnTopologyEthernetPathData", '$mwtnCommons', function (mwtnTopologyEthernetPathData, $mwtnCommons) {
3326
3327     return {
3328       restrict: 'E',
3329       replace: true,
3330       template: '<div id="cy" style="height: 750px; width: 100%;"></div>',
3331       controller: function () {
3332
3333       },
3334       scope: {
3335
3336       },
3337       link: function (scope, element, attrs, ctrl) {
3338
3339         var cy = cytoscape({
3340           container: element[0],
3341
3342           boxSelectionEnabled: false,
3343           autounselectify: true,
3344
3345           style: mwtnTopologyEthernetPathData.styles,
3346           elements: mwtnTopologyEthernetPathData.elements,
3347           layout: {
3348             name: 'preset',
3349             padding: 5
3350           }
3351         });
3352
3353         // @ Martin: Hier wird das Ereignis aus dem Service aboniert.
3354         //           Es ist möglich mehrere Ereignisse zu definieren.
3355         mwtnTopologyEthernetPathData.events.subscribe("elementsChanged", function (data) {
3356
3357           // @Martin: cy aktualisiert sich mit Hilfe der Referenz auf die Elemente aus dem Service
3358           cy.json({
3359             elements: mwtnTopologyEthernetPathData.elements // oder data.elements
3360           });
3361
3362         });
3363
3364         // pathGraphData.events.subscribe("styleChanged", function () {
3365         //   cy.json({
3366         //      style: mwtnTopologyEthernetPathData.styles
3367         //   });
3368         // });
3369
3370         cy.viewport({
3371           zoom: 0.5,
3372           pan: { x: 150, y: 100 }
3373         });
3374
3375         var clearService = function () {
3376           var lable = cy.getElementById('label');
3377           lable.data('label', '');
3378
3379           var pathState = ['working', 'protection', 'hidden'];
3380           pathState.map(function (state) {
3381             var selector = "[path = '" + state + "']";
3382             cy.elements(selector).map(function (element) {
3383               element.data('path', 'false');
3384             });
3385           });
3386         };
3387
3388         var highlightService = function (service) {
3389           var lable = cy.getElementById('label');
3390           lable.data('label', service);
3391
3392           var selector = "[service = '" + service + "']";
3393           // start and end service node (host, traffic analyser)
3394           cy.nodes(selector).map(function (node) {
3395             // console.log(node.id());
3396             node.data('path', 'working');
3397           });
3398
3399           ['protection', 'working'].map(function (state) {
3400             selector = "[service = '" + service + "'][rule = '" + state + "']";
3401             cy.edges(selector).map(function (edge) {
3402               edge.connectedNodes().map(function (node) {
3403                 node.data('path', edge.data('rule'));
3404               });
3405             });
3406             return state;
3407           }).reverse().map(function (state) {
3408             selector = "[path = '" + state + "']";
3409             cy.nodes(selector).connectedEdges().filter(function (edge) {
3410               return edge.data('service') === service || edge.data('layer') !== "ETH";
3411             }).map(function (edge) {
3412               edge.data('path', state);
3413             });
3414           });
3415         };
3416         var filterActiveMountPoints = function (mountpoints) {
3417           return mountpoints.filter(function (mountpoint) {
3418             if (!mountpoint) return false;
3419             // console.warn(mountpoint['node-id'], mountpoint['netconf-node-topology:connection-status']);
3420             return mountpoint['netconf-node-topology:connection-status'] === 'connected';
3421           }).map(function (mountpoint) {
3422             return mountpoint['node-id'];
3423           });
3424         };
3425
3426         var setDevicesActive = function (nodeIds) {
3427           // console.warn(nodeIds);
3428           cy.nodes().filter(function (node) {
3429             node.data('active', 'false');
3430             return node.data('type') === 'device' && nodeIds.contains(node.data('id'));
3431           }).map(function (node) {
3432             node.data('active', 'true');
3433           });
3434         };
3435
3436         var setAllDevicesInactive = function () {
3437           cy.nodes().map(function (node) {
3438             node.data('active', 'false');
3439           });
3440         };
3441
3442         var setPortAndEdgedActive = function () {
3443           cy.edges().map(function (edge) {
3444             var active = 'true';
3445             edge.connectedNodes().map(function (port) {
3446               // console.log('  node', JSON.stringify(edge.data()));
3447               var parent = cy.getElementById(port.data('parent'));
3448               if (parent.data('active') === 'false') {
3449                 port.data('active', 'false');
3450                 edge.data('active', 'false');
3451               } else {
3452                 port.data('active', 'true');
3453               }
3454             });
3455           });
3456         };
3457
3458         var init = function () {
3459           var timerName = 'init ethernet';
3460           console.time(timerName);
3461           $mwtnCommons.getMountPoints().then(function (mountpoints) {
3462             var filtered = filterActiveMountPoints(mountpoints);
3463             setDevicesActive(filtered);
3464             setPortAndEdgedActive();
3465             console.timeEnd(timerName);
3466           }, function (error) {
3467             setAllDevicesInactive();
3468             setPortAndEdgedActive();
3469             console.timeEnd(timerName);
3470           });
3471
3472         };
3473         init();
3474
3475         cy.on('tap', function (event) {
3476           clearService();
3477           if (event.target !== cy) {
3478             if (event.target.data('service')) {
3479               highlightService(event.target.data('service'));
3480             }
3481           } else {
3482             init();
3483           }
3484         });
3485       }
3486     }
3487   }]);
3488
3489   mwtnTopologyApp.controller("mwtnTopologyPortsGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyEthernetPathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyEthernetPathData) {
3490     var vm = this;
3491
3492     // The page number to show in the grid.
3493     var paginationPage = 1;
3494     // The page size.
3495     var paginationPageSize = 100;
3496     // The grid column object with current sorting informations.
3497     var sortColumn = null;
3498     // The grid column object with current sorting informations.
3499     var gridFilters = [];
3500     // caches all sites at the current grid page
3501
3502     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
3503       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
3504       paginationPageSizes: [10, 25, 50, 100],
3505       paginationPageSize: paginationPageSize,
3506       useExternalPagination: true,
3507       useExternalFiltering: true,
3508       useExternalSorting: true,
3509       totalItems: 0,
3510       columnDefs: [{
3511         field: "id",
3512         type: "string",
3513         displayName: "Id"
3514       },
3515       {
3516         field: "layer",
3517         type: "string",
3518         displayName: "Layer"
3519       },
3520       {
3521         field: "active",
3522         type: "string",
3523         displayName: "Active"
3524       }
3525       ],
3526       data: [],
3527       onRegisterApi: function (gridApi) {
3528         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
3529         vm.gridApi = gridApi;
3530
3531         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
3532           // Save the current sort column for later use.
3533           sortColumn = (!sortColumns || sortColumns.length === 0) ?
3534             null :
3535             sortColumns[0];
3536           loadPage();
3537         });
3538
3539         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
3540           // Save the pagination informations for later use.
3541           paginationPage = newPage;
3542           paginationPageSize = newPageSize;
3543           loadPage();
3544         });
3545
3546         vm.gridApi.core.on.filterChanged($scope, function () {
3547           // Save the all filters for later use.
3548           var filters = [];
3549           this.grid.columns.forEach(function (col, ind) {
3550             if (col.filters[0] && col.filters[0].term) {
3551               filters.push({
3552                 field: col.field,
3553                 term: col.filters[0].term
3554               });
3555             }
3556           });
3557           gridFilters = filters;
3558           loadPage();
3559         });
3560
3561         loadPage();
3562       }
3563     });
3564
3565     /**
3566      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
3567      */
3568     function loadPage() {
3569
3570       // extract all ports        
3571       var ports = mwtnTopologyEthernetPathData.elements.nodes.filter(function (node, ind, arr) {
3572         return node && node.data && node.data.type === 'port';
3573       }).reduce(function (acc, cur, ind, arr) {
3574         if (cur.data) acc[cur.data.id] = cur.data;
3575         return acc;
3576       }, {});
3577
3578       // get all port ids
3579       var portIds = Object.keys(ports);
3580
3581       // apply the grid filters
3582       var tempData = portIds.filter(function (portId, ind, arr) {
3583         var port = ports[portId];
3584         return gridFilters.map(function (filter) {
3585           switch (filter.field) {
3586             case "active":
3587               return port[filter.field].toString().contains(filter.term);
3588             default:
3589               return port[filter.field].contains(filter.term);
3590           }
3591         }).and(true);
3592       }).map(function (portId) {
3593         var port = ports[portId];
3594         var orderBy;
3595
3596         if (!sortColumn || !sortColumn.sort.direction) {
3597           return {
3598             id: port.id
3599           }
3600         }
3601
3602         switch (sortColumn.field) {
3603           case "active":
3604             orderBy = port[sortColumn.field] ? 1 : 0;
3605             break;
3606           default:
3607             orderBy = port[sortColumn.field];
3608             break;
3609         }
3610
3611         return {
3612           id: port.id,
3613           orderBy: orderBy
3614         };
3615       });
3616
3617       if (sortColumn && sortColumn.sort.direction) {
3618         tempData.sort(function (left, right) {
3619           if (left === right || left.orderBy === right.orderBy) {
3620             return 0;
3621           }
3622           if (left.orderBy > right.orderBy) {
3623             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
3624           }
3625           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
3626         });
3627       }
3628
3629       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
3630       var currentPage = Math.min(maxPageNumber, paginationPage);
3631       var orderedPortsAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
3632
3633       portsAtCurrentPageCache = {};
3634       var orderedData = [];
3635
3636       orderedPortsAtCurrentPage.forEach(function (orderedPort) {
3637         var port = ports[orderedPort.id];
3638         portsAtCurrentPageCache[port.id] = port;
3639         orderedData.push({
3640           id: orderedPort.id,
3641           layer: port.layer,
3642           active: port.active
3643         });
3644       });
3645
3646       $timeout(function () {
3647         vm.gridOptions.data = orderedData;
3648         vm.gridOptions.totalItems = tempData.length;
3649       });
3650     }
3651
3652     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
3653     mwtnTopologyEthernetPathData.events.subscribe("elementsChanged", function (data) {
3654       loadPage();
3655     });
3656
3657   }]);
3658
3659   mwtnTopologyApp.directive("mwtnTopologyPortsGrid", [function () {
3660     return {
3661       restrict: 'E',
3662       replace: false,
3663       controller: 'mwtnTopologyPortsGridController',
3664       controllerAs: 'vm',
3665       scope: {
3666
3667       },
3668       templateUrl: 'src/app/mwtnTopology/templates/portsGrid.tpl.html'
3669     };
3670   }]);
3671
3672   mwtnTopologyApp.controller("mwtnTopologyEthConnectionsGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyEthernetPathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyEthernetPathData) {
3673     var vm = this;
3674
3675     // The page number to show in the grid.
3676     var paginationPage = 1;
3677     // The page size.
3678     var paginationPageSize = 100;
3679     // The grid column object with current sorting informations.
3680     var sortColumn = null;
3681     // The grid column object with current sorting informations.
3682     var gridFilters = [];
3683     // caches all sites at the current grid page
3684
3685     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
3686       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
3687       paginationPageSizes: [10, 25, 50, 100],
3688       paginationPageSize: paginationPageSize,
3689       useExternalPagination: true,
3690       useExternalFiltering: true,
3691       useExternalSorting: true,
3692       totalItems: 0,
3693       columnDefs: [{
3694         field: "id",
3695         type: "string",
3696         displayName: "Id",
3697         width: 400
3698       },
3699       {
3700         field: "source",
3701         type: "string",
3702         displayName: "PortA",
3703         width: 200
3704       },
3705       {
3706         field: "target",
3707         type: "string",
3708         displayName: "PortZ",
3709         width: 200
3710       },
3711       {
3712         field: "layer",
3713         type: "string",
3714         displayName: "Layer",
3715         width: 80
3716       },
3717       {
3718         field: "service",
3719         type: "string",
3720         displayName: "Service",
3721         width: 150
3722       },
3723       {
3724         field: "rule",
3725         type: "string",
3726         displayName: "Rule",
3727         width: 150
3728       }
3729       ],
3730       data: [],
3731       onRegisterApi: function (gridApi) {
3732         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
3733         vm.gridApi = gridApi;
3734
3735         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
3736           // Save the current sort column for later use.
3737           sortColumn = (!sortColumns || sortColumns.length === 0) ?
3738             null :
3739             sortColumns[0];
3740           loadPage();
3741         });
3742
3743         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
3744           // Save the pagination informations for later use.
3745           paginationPage = newPage;
3746           paginationPageSize = newPageSize;
3747           loadPage();
3748         });
3749
3750         vm.gridApi.core.on.filterChanged($scope, function () {
3751           // Save the all filters for later use.
3752           var filters = [];
3753           this.grid.columns.forEach(function (col, ind) {
3754             if (col.filters[0] && col.filters[0].term) {
3755               filters.push({
3756                 field: col.field,
3757                 term: col.filters[0].term
3758               });
3759             }
3760           });
3761           gridFilters = filters;
3762           loadPage();
3763         });
3764
3765         loadPage();
3766       }
3767     });
3768
3769     /**
3770      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
3771      */
3772     function loadPage() {
3773
3774       // extract all links        
3775       var links = mwtnTopologyEthernetPathData.elements.edges.filter(function (link, ind, arr) {
3776         // return true; // node && node.data && node.data.type === 'port'; 
3777         return link.data.layer === 'ETH';
3778       }).reduce(function (acc, cur, ind, arr) {
3779         if (cur.data) {
3780           acc[cur.data.id] = cur.data;
3781         }
3782         return acc;
3783       }, {});
3784
3785       // get all relevant link ids
3786       var linkIds = Object.keys(links);
3787
3788       // apply the grid filters
3789       var tempData = linkIds.filter(function (linkId, ind, arr) {
3790         var link = links[linkId];
3791         return gridFilters.map(function (filter) {
3792           switch (filter.field) {
3793             default:
3794               return link[filter.field].contains(filter.term);
3795           }
3796         }).and(true);
3797       }).map(function (linkId) {
3798         var link = links[linkId];
3799         var orderBy;
3800
3801         if (!sortColumn || !sortColumn.sort.direction) {
3802           return {
3803             id: link.id
3804           }
3805         }
3806
3807         switch (sortColumn.field) {
3808           default:
3809             orderBy = link[sortColumn.field];
3810             break;
3811         }
3812
3813         return {
3814           id: link.id,
3815           orderBy: orderBy
3816         };
3817       });
3818
3819       if (sortColumn && sortColumn.sort.direction) {
3820         tempData.sort(function (left, right) {
3821           if (left === right || left.orderBy === right.orderBy) {
3822             return 0;
3823           }
3824           if (left.orderBy > right.orderBy) {
3825             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
3826           }
3827           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
3828         });
3829       }
3830
3831       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
3832       var currentPage = Math.min(maxPageNumber, paginationPage);
3833       var orderedLinksAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
3834
3835       linksAtCurrentPageCache = {};
3836       var orderedData = [];
3837
3838       orderedLinksAtCurrentPage.forEach(function (orderedLink) {
3839         var link = links[orderedLink.id];
3840         linksAtCurrentPageCache[link.id] = link;
3841         orderedData.push({
3842           id: orderedLink.id,
3843           source: link.source,
3844           target: link.target,
3845           layer: link.layer,
3846           service: link.service,
3847           rule: link.rule
3848         });
3849       });
3850
3851       $timeout(function () {
3852         vm.gridOptions.data = orderedData;
3853         vm.gridOptions.totalItems = tempData.length;
3854       });
3855     }
3856
3857     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
3858     var subscription = mwtnTopologyEthernetPathData.events.subscribe("elementsChanged", function (data) {
3859       loadPage();
3860       // to unsubscribe call subscription.remove();
3861     });
3862
3863   }]);
3864
3865   mwtnTopologyApp.directive("mwtnTopologyEthConnectionsGrid", [function () {
3866     return {
3867       restrict: 'E',
3868       replace: false,
3869       controller: 'mwtnTopologyEthConnectionsGridController',
3870       controllerAs: 'vm',
3871       scope: {
3872
3873       },
3874       templateUrl: 'src/app/mwtnTopology/templates/ethConnectionsGrid.tpl.html'
3875     };
3876   }]);
3877
3878   /********************************************* IEEE 1588v2 (PTP) **************************/
3879
3880   mwtnTopologyApp.controller('mwtnTopologyIeee1588ViewController', ['$scope', '$q', '$timeout', '$state', '$window', '$mwtnTopology', function ($scope, $q, $timeout, $state, $window, $mwtnTopology) {
3881     var vm = this;
3882     vm.status = {
3883       topologyIsOpen: false,
3884       portsOpen: false,
3885       ethConnectionsIsOpen: false
3886     }
3887
3888     $scope.$watchCollection(function () { return [vm.status.topologyIsOpen, vm.status.portsOpen, vm.status.ethConnectionsIsOpen] }, function (newVal, oldVal) {
3889       if (newVal[1] || newVal[2]) {
3890         $timeout(function () {
3891           $window.dispatchEvent(new Event("resize"));
3892         });
3893       }
3894     });
3895   }]);
3896
3897   mwtnTopologyApp.directive('mwtnTopologyIeee1588View', function () {
3898     return {
3899       restrict: 'E',
3900       controller: 'mwtnTopologyIeee1588ViewController',
3901       controllerAs: 'vm',
3902       bindToController: true,
3903       templateUrl: 'src/app/mwtnTopology/templates/ieee1588View.tpl.html',
3904       scope: {
3905
3906       }
3907     };
3908   });
3909
3910   mwtnTopologyApp.factory("mwtnTopologyIeee1588PathData", function () {
3911     var colors = {
3912       root: '#f54',
3913       port: '#377',
3914       device: '#252',
3915       site: '#525',
3916       edge: '#49a',
3917       white: '#eed',
3918       grey: '#555',
3919       selected: '#ff0'
3920     };
3921
3922     var styles = [
3923       {
3924         selector: 'node',
3925         css: {
3926           'content': 'data(label)',
3927           'text-valign': 'center',
3928           'text-halign': 'center',
3929           'background-color': '#aaaaaa',
3930           'border-color': '#000000',
3931           'border-width': '1px',
3932           'color': '#ffffff'
3933         }
3934       },
3935       {
3936         selector: '$node > node',
3937         css: {
3938           'shape': 'roundrectangle',
3939           'padding-top': '10px',
3940           'padding-left': '10px',
3941           'padding-bottom': '10px',
3942           'padding-right': '10px',
3943           'text-valign': 'top',
3944           'text-halign': 'center',
3945           'background-color': '#eeeeee',
3946           'color': '#444444',
3947           'border-color': '#888888'
3948         }
3949       },
3950       {
3951         selector: 'node[type = "site"]',
3952         css: {
3953           'shape': 'roundrectangle',
3954           'padding-top': '10px',
3955           'padding-left': '10px',
3956           'padding-bottom': '10px',
3957           'padding-right': '10px',
3958           'text-valign': 'center',
3959           'text-halign': 'center',
3960           'background-color': '#fefefe',
3961           'color': '#444444',
3962           'border-color': '#888888',
3963           'font-weight': 'bold'
3964         }
3965       },
3966       {
3967         selector: 'node[type = "ptp-clock"][active = "true"]',
3968         css: {
3969           'background-color': '#316ac5',
3970           'background-opacity': '0.2',
3971           'border-color': '#316ac5',
3972           'border-opacity': '0.8',
3973           'border-width': '2px',
3974           'color': '#444444'
3975         }
3976       },
3977       {
3978         selector: 'node[type = "port"][active = "true"]',
3979         css: {
3980           'background-opacity': '1.0',
3981         }
3982       },
3983       {
3984         selector: 'node[active = "false"]',
3985         css: {
3986           'background-opacity': '0.3',
3987           'border-opacity': '0.5'
3988         }
3989       },
3990       {
3991         selector: 'node[path = "true"]',
3992         css: {
3993           'background-color': '#ff00ff',
3994           'background-opacity': '0.9',
3995           'border-color': '#880088',
3996         }
3997       },
3998       {
3999         selector: '$node > node[path = "true"]',
4000         css: {
4001           'background-color': '#ff00ff',
4002           'background-opacity': '0.3',
4003           'border-color': '#ff00ff',
4004           'border-opacity': '1.0',
4005           'border-width': '2px',
4006         }
4007       },
4008       {
4009         selector: 'edge',
4010         css: {
4011           'content': 'data(id)',
4012           'target-arrow-shape': 'triangle',
4013           'line-color': '#666666',
4014           'color': '#444444'
4015         }
4016       },
4017       {
4018         selector: 'edge[active = "false"]',
4019         css: {
4020           'line-color': '#cccccc',
4021           'text-opacity': '0.9'
4022         }
4023       },
4024       {
4025         selector: 'edge[path = "true"]',
4026         css: {
4027           'line-color': '#ff00ff',
4028           'width': '5px'
4029         }
4030       },
4031       {
4032         selector: ':selected',
4033         css: {
4034           'background-color': 'black',
4035           'line-color': 'black',
4036           'target-arrow-color': 'black',
4037           'source-arrow-color': 'black'
4038         }
4039       }];
4040
4041     var elements = {
4042       nodes: [
4043         { data: { id: 'north', label: 'north', type: 'site', latitude: 50.734916, longitude: 7.137636 } },
4044         { data: { id: 'north-east', label: 'north-east', type: 'site', latitude: 50.733028, longitude: 7.151086 } },
4045         { data: { id: 'north-west', label: 'north-west', type: 'site', latitude: 50.730230, longitude: 7.126017 } },
4046         { data: { id: 'east', label: 'east', type: 'site', latitude: 50.725672, longitude: 7.158488 } },
4047         { data: { id: 'west', label: 'west', type: 'site', latitude: 50.721914, longitude: 7.120521 } },
4048         { data: { id: 'south-east', label: 'south-east', type: 'site', latitude: 50.717158, longitude: 7.155506 } },
4049         { data: { id: 'south-west', label: 'south-west', type: 'site', latitude: 50.714359, longitude: 7.130437 } },
4050         { data: { id: 'south', label: 'south', type: 'site', latitude: 50.712472, longitude: 7.143887 } },
4051
4052         {  data:  {  id:  'ADVA-A',  label :  'ADVA-A' ,  parent :  'west', type: 'ptp-clock', base64: 'AIDq//6MFzA=', hex: '0x47 0x4D 0x30 0x30 0x30 0x30 0x30 0x31', active: 'true' , latitude: 50.721914, longitude: 7.120521, parentDs: '', path: 'false' }   },
4053         {  data:  {  id:  'ADVA-B',  label :  'ADVA-B' ,  parent :  'north-west', type: 'ptp-clock', base64: 'AIDq//6MGDA=', hex: '0x47 0x4D 0x30 0x30 0x30 0x30 0x30 0x32', active: 'true' , latitude: 50.730230, longitude: 7.126017, parentDs: '', path: 'false' }   },
4054         {  data:  {  id:  'ADVA-Y',  label :  'ADVA-Y' ,  parent :  'north-east', type: 'ptp-clock', base64: 'AIDqb0hQAAE=', hex: '0x53 0x4C 0x41 0x56 0x45 0x30 0x30 0x31', active: 'true' , latitude: 50.733028, longitude: 7.151086, parentDs: 'NEC-Z#4', path: 'false' }   },
4055         {  data:  {  id:  'ADVA-Z',  label :  'ADVA-Z' ,  parent :  'south', type: 'ptp-clock', base64: 'AIDqb0mAAAA=', hex: '0x53 0x4C 0x41 0x56 0x45 0x30 0x30 0x32', active: 'true' , latitude: 50.712472, longitude: 7.143887, parentDs: 'Huawei-Z#3', path: 'false' }   },
4056         {  data:  {  id:  'Ericsson-A',  label :  'Ericsson-A' ,  parent :  'north-east', type: 'ptp-clock', base64: 'BE4G//4jsio=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x31', active: 'true' , latitude: 50.733028, longitude: 7.151086, parentDs: 'NEC-Z#2', path: 'false' }   },
4057         {  data:  {  id:  'Ericsson-Z',  label :  'Ericsson-Z' ,  parent :  'east', type: 'ptp-clock', base64: 'BE4G//4jtBA=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x32', active: 'true' , latitude: 50.725672, longitude: 7.158488, parentDs: 'Ericsson-A#6', path: 'false' }   },
4058         {  data:  {  id:  'Huawei-A',  label :  'Huawei-A' ,  parent :  'south-west', type: 'ptp-clock', base64: 'ACWeIQAJq6A=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x33', active: 'true' , latitude: 50.714359, longitude: 7.130437, parentDs: 'Nokia-Z33', path: 'false' }   },
4059         {  data:  {  id:  'Huawei-Z',  label :  'Huawei-Z' ,  parent :  'south', type: 'ptp-clock', base64: 'ACWeIQAJAm8=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x34', active: 'true' , latitude: 50.712472, longitude: 7.143887, parentDs: 'Huawei-A#6', path: 'false' }   },
4060         {  data:  {  id:  'NEC-A',  label :  'NEC-A' ,  parent :  'north', type: 'ptp-clock', base64: 'jN+d//5XDuA=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x35', active: 'true' , latitude: 50.734916, longitude: 7.137636, parentDs: 'ZTE-Z#5', path: 'false' }   },
4061         {  data:  {  id:  'NEC-Z',  label :  'NEC-Z' ,  parent :  'north-east', type: 'ptp-clock', base64: 'jN+d//5XDwA=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x36', active: 'true' , latitude: 50.733028, longitude: 7.151086, parentDs: 'NEC-A#6', path: 'false' }   },
4062         {  data:  {  id:  'Nokia-A',  label :  'Nokia-A' ,  parent :  'west', type: 'ptp-clock', base64: 'ACGu//4Crac=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x37', active: 'true' , latitude: 50.721914, longitude: 7.120521, parentDs: 'ADVA-A#1', path: 'false' }   },
4063         {  data:  {  id:  'Nokia-Z',  label :  'Nokia-Z' ,  parent :  'south-west', type: 'ptp-clock', base64: 'ACGu//4CrZY=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x38', active: 'true' , latitude: 50.714359, longitude: 7.130437, parentDs: 'Nokia-A#6', path: 'false' }   },
4064         {  data:  {  id:  'SIAE-A',  label :  'SIAE-A' ,  parent :  'south', type: 'ptp-clock', base64: 'ALCs//4R6K8=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x30 0x39', active: 'true' , latitude: 50.712472, longitude: 7.143887, parentDs: 'Huawei-Z#5', path: 'false' }   },
4065         {  data:  {  id:  'SIAE-Z',  label :  'SIAE-Z' ,  parent :  'south-east', type: 'ptp-clock', base64: 'ALCs//4RucQ=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x31 0x30', active: 'true' , latitude: 50.717158, longitude: 7.155506, parentDs: 'SIAE-A#6', path: 'false' }   },
4066         {  data:  {  id:  'ZTE-A',  label :  'ZTE-A' ,  parent :  'north-west', type: 'ptp-clock', base64: 'DBJi//7Zdpo=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x31 0x31', active: 'true' , latitude: 50.730230, longitude: 7.126017, parentDs: 'ADVA-B#2', path: 'false' }   },
4067         {  data:  {  id:  'ZTE-Z',  label :  'ZTE-Z' ,  parent :  'north', type: 'ptp-clock', base64: 'DBJi//7ZdqQ=', hex: '0x42 0x43 0x30 0x30 0x30 0x30 0x31 0x32', active: 'true' , latitude: 50.734916, longitude: 7.137636, parentDs: 'ZTE-A#6', path: 'false' }   },
4068
4069         { data: { id: 'ADVA-A#1', label: '#1', parent: 'ADVA-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.721914, longitude: 7.120521 }, position: { x: 124, y: 564 } },
4070         { data: { id: 'ADVA-A#2', label: '#2', parent: 'ADVA-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.721914, longitude: 7.120521 }, position: { x: 124, y: 507 } },
4071         { data: { id: 'ADVA-B#1', label: '#1', parent: 'ADVA-B', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.730230, longitude: 7.126017 }, position: { x: 380, y: 381 } },
4072         { data: { id: 'ADVA-B#2', label: '#2', parent: 'ADVA-B', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.730230, longitude: 7.126017 }, position: { x: 380, y: 301 } },
4073         { data: { id: 'ADVA-Y#1', label: '#1', parent: 'ADVA-Y', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1392, y: 229 } },
4074         { data: { id: 'ADVA-Y#2', label: '#2', parent: 'ADVA-Y', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: 172 } },
4075         { data: { id: 'ADVA-Z#1', label: '#1', parent: 'ADVA-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1139, y: 1001 } },
4076         { data: { id: 'ADVA-Z#2', label: '#2', parent: 'ADVA-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1196, y: 944 } },
4077         { data: { id: 'Ericsson-A#5', label: '#5', parent: 'Ericsson-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1605, y: 172 } },
4078         { data: { id: 'Ericsson-A#6', label: '#6', parent: 'Ericsson-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1664, y: 226 } },
4079         { data: { id: 'Ericsson-Z#6', label: '#6', parent: 'Ericsson-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.725672, longitude: 7.158488 }, position: { x: 1765, y: 325 } },
4080         { data: { id: 'Huawei-A#5', label: '#5', parent: 'Huawei-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.714359, longitude: 7.130437 }, position: { x: 605, y: 1028 } },
4081         { data: { id: 'Huawei-A#6', label: '#6', parent: 'Huawei-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.714359, longitude: 7.130437 }, position: { x: 685, y: 1028 } },
4082         { data: { id: 'Huawei-Z#3', label: '#3', parent: 'Huawei-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1046, y: 1094 } },
4083         { data: { id: 'Huawei-Z#4', label: '#4', parent: 'Huawei-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 989, y: 1094 } },
4084         { data: { id: 'Huawei-Z#5', label: '#5', parent: 'Huawei-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1058, y: 1123 } },
4085         { data: { id: 'Huawei-Z#6', label: '#6', parent: 'Huawei-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 978, y: 1123 } },
4086         { data: { id: 'NEC-A#3', label: '#3', parent: 'NEC-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.734916, longitude: 7.137636 }, position: { x: 955, y: -52 } },
4087         { data: { id: 'NEC-A#6', label: '#6', parent: 'NEC-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.734916, longitude: 7.137636 }, position: { x: 1035, y: -52 } },
4088         { data: { id: 'NEC-Z#2', label: '#2', parent: 'NEC-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: -41 } },
4089         { data: { id: 'NEC-Z#3', label: '#3', parent: 'NEC-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1392, y: 16 } },
4090         { data: { id: 'NEC-Z#4', label: '#4', parent: 'NEC-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1449, y: 16 } },
4091         { data: { id: 'NEC-Z#6', label: '#6', parent: 'NEC-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.733028, longitude: 7.151086 }, position: { x: 1382, y: -23 } },
4092         { data: { id: 'Nokia-A13', label: '13', parent: 'Nokia-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.721914, longitude: 7.120521 }, position: { x: 40, y: 801 } },
4093         { data: { id: 'Nokia-A34', label: '34', parent: 'Nokia-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.721914, longitude: 7.120521 }, position: { x: 28, y: 772 } },
4094         { data: { id: 'Nokia-A#6', label: '#6', parent: 'Nokia-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.721914, longitude: 7.120521 }, position: { x: 28, y: 829 } },
4095         { data: { id: 'Nokia-Z33', label: '33', parent: 'Nokia-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.714359, longitude: 7.130437 }, position: { x: 385, y: 1028 } },
4096         { data: { id: 'Nokia-Z#6', label: '#6', parent: 'Nokia-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.714359, longitude: 7.130437 }, position: { x: 305, y: 1028 } },
4097         { data: { id: 'SIAE-A#5', label: '#5', parent: 'SIAE-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1278, y: 1123 } },
4098         { data: { id: 'SIAE-A#6', label: '#6', parent: 'SIAE-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.712472, longitude: 7.143887 }, position: { x: 1346, y: 1094 } },
4099         { data: { id: 'SIAE-Z#6', label: '#6', parent: 'SIAE-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.717158, longitude: 7.155506 }, position: { x: 1588, y: 838 } },
4100         { data: { id: 'ZTE-A#3', label: '#3', parent: 'ZTE-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.730230, longitude: 7.126017 }, position: { x: 380, y: 168 } },
4101         { data: { id: 'ZTE-A#4', label: '#4', parent: 'ZTE-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.730230, longitude: 7.126017 }, position: { x: 345, y: 148 } },
4102         { data: { id: 'ZTE-A#6', label: '#6', parent: 'ZTE-A', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.730230, longitude: 7.126017 }, position: { x: 408, y: 99 } },
4103         { data: { id: 'ZTE-Z#5', label: '#5', parent: 'ZTE-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.734916, longitude: 7.137636 }, position: { x: 747, y: -27 } },
4104         { data: { id: 'ZTE-Z#6', label: '#6', parent: 'ZTE-Z', type: 'port', layer: 'PTP', active: 'true', path: 'false', latitude: 50.734916, longitude: 7.137636 }, position: { x: 667, y: -27 } },
4105
4106       ],
4107       edges: [
4108         { data: { id: 'ERI1', source: 'Ericsson-A#6', target: 'Ericsson-Z#6', label: 'ERI1', layer: 'PTP', active: 'true' } },
4109         { data: { id: '71', source: 'Huawei-A#6', target: 'Huawei-Z#6', label: '71', layer: 'PTP', active: 'true' } },
4110         { data: { id: '81', source: 'NEC-A#6', target: 'NEC-Z#6', label: '81', layer: 'PTP', active: 'true' } },
4111         { data: { id: '91', source: 'Nokia-A#6', target: 'Nokia-Z#6', label: '91', layer: 'PTP', active: 'true' } },
4112         { data: { id: '101', source: 'SIAE-A#6', target: 'SIAE-Z#6', label: '101', layer: 'PTP', active: 'true' } },
4113         { data: { id: '111', source: 'ZTE-A#6', target: 'ZTE-Z#6', label: '111', layer: 'PTP', active: 'true' } },
4114
4115         { data: { id: 'ETY01', source: 'ADVA-A#1', target: 'Nokia-A34', label: 'ADVA-A#1-Nokia-A34', layer: 'PTP', active: 'true' } },
4116         { data: { id: 'ETY02', source: 'ADVA-A#2', target: 'ZTE-A#4', label: 'ADVA-A#2-ZTE-A#4', layer: 'PTP', active: 'false' } },
4117         { data: { id: 'ETY03', source: 'ADVA-B#1', target: 'Nokia-A13', label: 'ADVA-B#1-Nokia-A13', layer: 'PTP', active: 'true' } },
4118         { data: { id: 'ETY04', source: 'ADVA-B#2', target: 'ZTE-A#3', label: 'ADVA-B#2-ZTE-A#3', layer: 'PTP', active: 'true' } },
4119         { data: { id: 'ETY05', source: 'ADVA-Y#1', target: 'Huawei-Z#4', label: 'ADVA-Y#1-Huawei-Z#4', layer: 'PTP', active: 'true' } },
4120         { data: { id: 'ETY06', source: 'ADVA-Y#2', target: 'NEC-Z#4', label: 'ADVA-Y#2-NEC-Z#4', layer: 'PTP', active: 'true' } },
4121         { data: { id: 'ETY09', source: 'ADVA-Z#1', target: 'Huawei-Z#3', label: 'ADVA-Z#1-Huawei-Z#3', layer: 'PTP', active: 'true' } },
4122         { data: { id: 'ETY10', source: 'ADVA-Z#2', target: 'NEC-Z#3', label: 'ADVA-Z#2-NEC-Z#3', layer: 'PTP', active: 'true' } },
4123         { data: { id: 'ETY21', source: 'Ericsson-A#5', target: 'NEC-Z#2', label: 'Ericsson-A#5-NEC-Z#2', layer: 'PTP', active: 'true' } },
4124         { data: { id: 'ETY24', source: 'Huawei-A#5', target: 'Nokia-Z33', label: 'Huawei-A#5-Nokia-Z33', layer: 'PTP', active: 'true' } },
4125         { data: { id: 'ETY25', source: 'Huawei-Z#5', target: 'SIAE-A#5', label: 'Huawei-Z#5-SIAE-A#5', layer: 'PTP', active: 'true' } },
4126         { data: { id: 'ETY26', source: 'NEC-A#3', target: 'ZTE-Z#5', label: 'NEC-A#3-ZTE-Z#5', layer: 'PTP', active: 'true' } },
4127
4128       ]
4129     };
4130
4131     var events = eventsFabric();
4132
4133     var result = {
4134       colors: colors,
4135       elements: elements,
4136       styles: styles,
4137       events: events
4138     };
4139
4140     var someMethodChangingTheElements = function () {
4141       // @Martin: hier kannst Du die Elements ändern, anschließend mußt Du das Ereignis veröffentlichen
4142       //          das Ereigniss wird in der Directive aufgefangen und die Grig wird neu gezeichnet
4143
4144       // Hinweis: Die Reihenfolge muss so bleiben und du kannst NUR result.elements ändern.
4145
4146       events.publish("elementsChanged", {
4147         elements: result.elements
4148       });
4149     };
4150
4151     return result;
4152   });
4153
4154   mwtnTopologyApp.directive("mwtnTopologyIeee1588PathGraph", ["mwtnTopologyIeee1588PathData", "$mwtnPtp", function (mwtnTopologyIeee1588PathData, $mwtnPtp) {
4155
4156     return {
4157       restrict: 'E',
4158       replace: true,
4159       template: '<div style="height: 750px; width: 100%;"></div>',
4160       controller: function () {
4161
4162       },
4163       scope: {
4164
4165       },
4166       link: function (scope, element, attrs, ctrl) {
4167
4168         var cy = cytoscape({
4169           container: element[0],
4170
4171           boxSelectionEnabled: false,
4172           autounselectify: true,
4173
4174           style: mwtnTopologyIeee1588PathData.styles,
4175           elements: mwtnTopologyIeee1588PathData.elements,
4176           layout: {
4177             name: 'preset',
4178             padding: 5
4179           }
4180         });
4181
4182         // @ Martin: Hier wird das Ereignis aus dem Service aboniert.
4183         //           Es ist möglich mehrere Ereignisse zu definieren.
4184         mwtnTopologyIeee1588PathData.events.subscribe("elementsChanged", function (data) {
4185
4186           // @Martin: cy aktualisiert sich mit Hilfe der Referenz auf die Elemente aus dem Service
4187           cy.json({
4188             elements: mwtnTopologyIeee1588PathData.elements // oder data.elements
4189           });
4190         });
4191
4192         // pathGraphData.events.subscribe("styleChanged", function () {
4193         //   cy.json({
4194         //      style: mwtnTopologyIeee1588PathData.styles
4195         //   });
4196         // });
4197
4198         cy.viewport({
4199           zoom: 0.5,
4200           pan: { x: 150, y: 100 }
4201         });
4202
4203         var clearPtpPath = function () {
4204           var selector = "[path = 'true']";
4205           cy.elements(selector).map(function (element) {
4206             element.data('path', 'false');
4207           });
4208         };
4209
4210         var highlightPtpMaster = function (id) {
4211           var selector = "[id = '" + id + "']";
4212           cy.nodes(selector).map(function (node) {
4213             if (node.data('parentDs') && node.data('parentDs') != '') {
4214               // console.warn('parentDs', node.data('parentDs'));
4215               var parentNode = node.data('parentDs').slice(0, -2);
4216               if (parentNode !== id) {
4217                 highlightPtpMaster(parentNode);
4218
4219                 // highlight edge
4220                 selector = "[id = '" + node.data('parentDs') + "']";
4221                 cy.nodes(selector).connectedEdges().map(function (edge) {
4222                   edge.data('path', 'true');
4223                   edge.connectedNodes().map(function (node) {
4224                     node.data('path', 'true');
4225                   });
4226                 });
4227               }
4228             }
4229           });
4230         };
4231
4232         var setAllDevicesInactive = function () {
4233           cy.nodes().map(function (node) {
4234             node.data('active', 'false');
4235           });
4236         };
4237
4238         var setPortAndEdgedActive = function () {
4239           cy.edges().map(function (edge) {
4240             var active = 'true';
4241             edge.connectedNodes().map(function (port) {
4242               // console.log('  node', JSON.stringify(edge.data()));
4243               var parent = cy.getElementById(port.data('parent'));
4244               if (parent.data('active') === 'false') {
4245                 port.data('active', 'false');
4246                 edge.data('active', 'false');
4247               } else {
4248                 port.data('active', 'true');
4249               }
4250             });
4251           });
4252         };
4253
4254         var init = function () {
4255           setAllDevicesInactive();
4256           $mwtnPtp.getPtpClocks().then(function (clocks) {
4257             // setDevicesActive(Object.keys(clocks));
4258             var hex = true;
4259             // update clock ids first
4260             Object.keys(clocks).map(function (key) {
4261               var clock = clocks[key];
4262               var graphClock = cy.getElementById(key);
4263               graphClock.data('active', 'true')
4264               graphClock.data('base64', clock.getIdentity());
4265               graphClock.data('hex', clock.getIdentity(hex));
4266             });
4267             // update rest
4268             Object.keys(clocks).map(function (key) {
4269               var clock = clocks[key];
4270               var graphClock = cy.getElementById(key);
4271               var graphParentDs = nodeId(clock.getParent().slice(0, -2));
4272               graphClock.data('parentDs', graphParentDs + clock.getParent().slice(-2));
4273               graphClock.data('grandMaster', nodeId(clock.getGrandMaster()));
4274
4275               // clock.getPtpPorts().map(function(port){
4276               //   // console.warn(port.getId(), port.getNumber(), port.getState(), port.isSlave(), port.isMaster(), port.getLogicalTerminationPointReference());
4277               //   var portKey = [key, port.getNumber() < 10 ? '#' : '', port.getNumber()].join('');
4278               //   var graphPort = cy.getElementById(portKey);
4279               //   // console.warn(JSON.stringify(graphPort.data()));
4280               //   if (graphPort === undefined) {
4281               //     console.error('PtpPort not found in graph:' , portKey);
4282               //   } else {
4283               //     console.info('PtpPort found in graph:' , portKey);
4284               //   }
4285
4286               // });
4287             });
4288             setPortAndEdgedActive();
4289           }, function (error) {
4290             setPortAndEdgedActive();
4291             console.error(JSON.stringify(error));
4292           });
4293         };
4294         // init();
4295
4296         var getNodeId = function (base64) {
4297           if (base64 === undefined || base64 === '') return '';
4298
4299           var selector = "[type = 'ptp-clock']";
4300           var result = cy.nodes(selector).filter(function (graphClock) {
4301             // console.error(base64, graphClock.data('base64'), graphClock.data('base64') === base64);
4302             return graphClock.data('base64') === base64;
4303           });
4304           if (result.length === 0) {
4305             console.warn('Clock', base64, 'not found!');
4306             return '';
4307           } else {
4308             return result[0].id();
4309           }
4310         };
4311
4312         var getParentClock = function (nodeId) {
4313           $mwtnPtp.getParent(nodeId).then(function (parentPortIdentity) {
4314             var parentNode = getNodeId(parentPortIdentity['clock-identity']);
4315             console.log(JSON.stringify(parentPortIdentity), parentNode);
4316             if (parentNode) {
4317               getParentClock(parentNode);
4318             }
4319           });
4320         };
4321
4322         cy.on('tap', function (event) {
4323           console.log('tap');
4324           clearPtpPath();
4325           if (event.target !== cy) {
4326             if (event.target.data('type') === 'ptp-clock') {
4327               getParentClock(event.target.id());
4328               //   highlightPtpMaster(event.target.id());
4329             } else if (event.target.data('type') === 'port') {
4330               var parent = cy.getElementById(event.target.data('parent'));
4331               // highlightPtpMaster(parent.id());
4332               getParentClock(event.target.id());
4333             }
4334           } else {
4335             // init();
4336           }
4337         });
4338
4339       }
4340     }
4341   }]);
4342
4343   mwtnTopologyApp.controller("mwtnTopologyClocksGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyIeee1588PathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyIeee1588PathData) {
4344     var vm = this;
4345
4346     // The page number to show in the grid.
4347     var paginationPage = 1;
4348     // The page size.
4349     var paginationPageSize = 100;
4350     // The grid column object with current sorting informations.
4351     var sortColumn = null;
4352     // The grid column object with current sorting informations.
4353     var gridFilters = [];
4354     // caches all sites at the current grid page
4355
4356     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
4357       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
4358       paginationPageSizes: [10, 25, 50, 100],
4359       paginationPageSize: paginationPageSize,
4360       useExternalPagination: true,
4361       useExternalFiltering: true,
4362       useExternalSorting: true,
4363       totalItems: 0,
4364       columnDefs: [{
4365         field: "id",
4366         type: "string",
4367         displayName: "Node id",
4368         width: 120
4369       },
4370       {
4371         field: "hex",
4372         type: "string",
4373         displayName: "Clock identity in hex",
4374         width: 300
4375       }, {
4376         field: "base64",
4377         type: "string",
4378         displayName: "... in base64",
4379         width: 150
4380       },
4381       {
4382         field: "parentDs",
4383         type: "string",
4384         displayName: "parentDs",
4385         width: 150
4386       },
4387       {
4388         field: "grandMaster",
4389         type: "string",
4390         displayName: "grandmaster",
4391         width: 300
4392       },
4393       {
4394         field: "active",
4395         type: "string",
4396         displayName: "Active",
4397         width: 80
4398       }
4399       ],
4400       data: [],
4401       onRegisterApi: function (gridApi) {
4402         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
4403         vm.gridApi = gridApi;
4404
4405         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
4406           // Save the current sort column for later use.
4407           sortColumn = (!sortColumns || sortColumns.length === 0) ?
4408             null :
4409             sortColumns[0];
4410           loadPage();
4411         });
4412
4413         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
4414           // Save the pagination informations for later use.
4415           paginationPage = newPage;
4416           paginationPageSize = newPageSize;
4417           loadPage();
4418         });
4419
4420         vm.gridApi.core.on.filterChanged($scope, function () {
4421           // Save the all filters for later use.
4422           var filters = [];
4423           this.grid.columns.forEach(function (col, ind) {
4424             if (col.filters[0] && col.filters[0].term) {
4425               filters.push({
4426                 field: col.field,
4427                 term: col.filters[0].term
4428               });
4429             }
4430           });
4431           gridFilters = filters;
4432           loadPage();
4433         });
4434
4435         loadPage();
4436       }
4437     });
4438
4439     /**
4440      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
4441      */
4442     function loadPage() {
4443
4444       // extract all ports        
4445       var ports = mwtnTopologyIeee1588PathData.elements.nodes.filter(function (node, ind, arr) {
4446         return node && node.data && node.data.type === 'ptp-clock';
4447       }).reduce(function (acc, cur, ind, arr) {
4448         if (cur.data) acc[cur.data.id] = cur.data;
4449         return acc;
4450       }, {});
4451
4452       // get all port ids
4453       var portIds = Object.keys(ports);
4454
4455       // apply the grid filters
4456       var tempData = portIds.filter(function (portId, ind, arr) {
4457         var port = ports[portId];
4458         return gridFilters.map(function (filter) {
4459           switch (filter.field) {
4460             case "active":
4461               return port[filter.field].toString().contains(filter.term);
4462             default:
4463               return port[filter.field].contains(filter.term);
4464           }
4465         }).and(true);
4466       }).map(function (portId) {
4467         var port = ports[portId];
4468         var orderBy;
4469
4470         if (!sortColumn || !sortColumn.sort.direction) {
4471           return {
4472             id: port.id
4473           }
4474         }
4475
4476         switch (sortColumn.field) {
4477           case "active":
4478             orderBy = port[sortColumn.field] ? 1 : 0;
4479             break;
4480           default:
4481             orderBy = port[sortColumn.field];
4482             break;
4483         }
4484
4485         return {
4486           id: port.id,
4487           orderBy: orderBy
4488         };
4489       });
4490
4491       if (sortColumn && sortColumn.sort.direction) {
4492         tempData.sort(function (left, right) {
4493           if (left === right || left.orderBy === right.orderBy) {
4494             return 0;
4495           }
4496           if (left.orderBy > right.orderBy) {
4497             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
4498           }
4499           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
4500         });
4501       }
4502
4503       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
4504       var currentPage = Math.min(maxPageNumber, paginationPage);
4505       var orderedPortsAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
4506
4507       portsAtCurrentPageCache = {};
4508       var orderedData = [];
4509
4510       orderedPortsAtCurrentPage.forEach(function (orderedPort) {
4511         var port = ports[orderedPort.id]; // TODO [sko] varible port should be renamed to clock
4512         portsAtCurrentPageCache[port.id] = port;
4513         orderedData.push({
4514           id: orderedPort.id,
4515           layer: port.layer,
4516           active: port.active,
4517           hex: port.hex,
4518           base64: port.base64,
4519           parentDs: port.parentDs,
4520           grandMaster: port.grandMaster
4521         });
4522       });
4523
4524       $timeout(function () {
4525         vm.gridOptions.data = orderedData;
4526         vm.gridOptions.totalItems = tempData.length;
4527       });
4528     }
4529
4530     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
4531     mwtnTopologyIeee1588PathData.events.subscribe("elementsChanged", function (data) {
4532       loadPage();
4533     });
4534
4535   }]);
4536
4537   mwtnTopologyApp.directive("mwtnTopologyClocksGrid", [function () {
4538     return {
4539       restrict: 'E',
4540       replace: false,
4541       controller: 'mwtnTopologyClocksGridController',
4542       controllerAs: 'vm',
4543       scope: {
4544
4545       },
4546       templateUrl: 'src/app/mwtnTopology/templates/clocksGrid.tpl.html'
4547     };
4548   }]);
4549
4550   mwtnTopologyApp.controller("mwtnTopologyPtpLinksGridController", ['$scope', '$timeout', '$state', '$mwtnCommons', '$mwtnTopology', 'uiGridConstants', 'mwtnTopologyIeee1588PathData', function ($scope, $timeout, $state, $mwtnCommons, $mwtnTopology, uiGridConstants, mwtnTopologyIeee1588PathData) {
4551     var vm = this;
4552
4553     // The page number to show in the grid.
4554     var paginationPage = 1;
4555     // The page size.
4556     var paginationPageSize = 100;
4557     // The grid column object with current sorting informations.
4558     var sortColumn = null;
4559     // The grid column object with current sorting informations.
4560     var gridFilters = [];
4561     // caches all sites at the current grid page
4562
4563     vm.gridOptions = Object.assign({}, $mwtnCommons.gridOptions, {
4564       showGridFooter: false, // disable the grid footer because the pagination component sets its own.
4565       paginationPageSizes: [10, 25, 50, 100],
4566       paginationPageSize: paginationPageSize,
4567       useExternalPagination: true,
4568       useExternalFiltering: true,
4569       useExternalSorting: true,
4570       totalItems: 0,
4571       columnDefs: [{
4572         field: "id",
4573         type: "string",
4574         displayName: "Id"
4575       },
4576       {
4577         field: "source",
4578         type: "string",
4579         displayName: "PortA"
4580       },
4581       {
4582         field: "target",
4583         type: "string",
4584         displayName: "PortZ"
4585       },
4586       {
4587         field: "active",
4588         type: "string",
4589         displayName: "active"
4590       }
4591       ],
4592       data: [],
4593       onRegisterApi: function (gridApi) {
4594         // http://ui-grid.info/docs/#/api/ui.grid.core.api:PublicApi
4595         vm.gridApi = gridApi;
4596
4597         vm.gridApi.core.on.sortChanged($scope, function (grid, sortColumns) {
4598           // Save the current sort column for later use.
4599           sortColumn = (!sortColumns || sortColumns.length === 0) ?
4600             null :
4601             sortColumns[0];
4602           loadPage();
4603         });
4604
4605         vm.gridApi.pagination.on.paginationChanged($scope, function (newPage, newPageSize) {
4606           // Save the pagination informations for later use.
4607           paginationPage = newPage;
4608           paginationPageSize = newPageSize;
4609           loadPage();
4610         });
4611
4612         vm.gridApi.core.on.filterChanged($scope, function () {
4613           // Save the all filters for later use.
4614           var filters = [];
4615           this.grid.columns.forEach(function (col, ind) {
4616             if (col.filters[0] && col.filters[0].term) {
4617               filters.push({
4618                 field: col.field,
4619                 term: col.filters[0].term
4620               });
4621             }
4622           });
4623           gridFilters = filters;
4624           loadPage();
4625         });
4626
4627         loadPage();
4628       }
4629     });
4630
4631     /**
4632      * Calculates the page content of the grid and sets the values to the gridOprions.data object.
4633      */
4634     function loadPage() {
4635
4636       // extract all links        
4637       var links = mwtnTopologyIeee1588PathData.elements.edges.filter(function (node, ind, arr) {
4638         return true; // node && node.data && node.data.type === 'port';
4639       }).reduce(function (acc, cur, ind, arr) {
4640         if (cur.data) {
4641           acc[cur.data.id] = cur.data;
4642         }
4643         return acc;
4644       }, {});
4645
4646       // get all link ids
4647       var linkIds = Object.keys(links);
4648
4649       // apply the grid filters
4650       var tempData = linkIds.filter(function (linkId, ind, arr) {
4651         var link = links[linkId];
4652         return gridFilters.map(function (filter) {
4653           switch (filter.field) {
4654             default:
4655               return link[filter.field].contains(filter.term);
4656           }
4657         }).and(true);
4658       }).map(function (linkId) {
4659         var link = links[linkId];
4660         var orderBy;
4661
4662         if (!sortColumn || !sortColumn.sort.direction) {
4663           return {
4664             id: link.id
4665           }
4666         }
4667
4668         switch (sortColumn.field) {
4669           case "active":
4670             orderBy = link[sortColumn.field] ? 1 : 0;
4671           default:
4672             orderBy = link[sortColumn.field];
4673             break;
4674         }
4675
4676         return {
4677           id: link.id,
4678           orderBy: orderBy
4679         };
4680       });
4681
4682       if (sortColumn && sortColumn.sort.direction) {
4683         tempData.sort(function (left, right) {
4684           if (left === right || left.orderBy === right.orderBy) {
4685             return 0;
4686           }
4687           if (left.orderBy > right.orderBy) {
4688             return (sortColumn.sort.direction === uiGridConstants.ASC) ? 1 : -1;
4689           }
4690           return (sortColumn.sort.direction === uiGridConstants.ASC) ? -1 : 1;
4691         });
4692       }
4693
4694       var maxPageNumber = Math.max(1, Math.ceil(tempData.length / paginationPageSize));
4695       var currentPage = Math.min(maxPageNumber, paginationPage);
4696       var orderedLinksAtCurrentPage = tempData.slice((currentPage - 1) * paginationPageSize, currentPage * paginationPageSize);
4697
4698       linksAtCurrentPageCache = {};
4699       var orderedData = [];
4700
4701       orderedLinksAtCurrentPage.forEach(function (orderedLink) {
4702         var link = links[orderedLink.id];
4703         linksAtCurrentPageCache[link.id] = link;
4704         orderedData.push({
4705           id: orderedLink.id,
4706           source: link.source,
4707           target: link.target,
4708           active: link.active
4709         });
4710       });
4711
4712       $timeout(function () {
4713         vm.gridOptions.data = orderedData;
4714         vm.gridOptions.totalItems = tempData.length;
4715       });
4716     }
4717
4718     // subscribe to the elementsChanged event to reload the grid page if the data in the service has chenged      
4719     var subscription = mwtnTopologyIeee1588PathData.events.subscribe("elementsChanged", function (data) {
4720       loadPage();
4721       // to unsubscribe call subscription.remove();
4722     });
4723
4724   }]);
4725
4726   mwtnTopologyApp.directive("mwtnTopologyPtpLinksGrid", [function () {
4727     return {
4728       restrict: 'E',
4729       replace: false,
4730       controller: 'mwtnTopologyPtpLinksGridController',
4731       controllerAs: 'vm',
4732       scope: {
4733
4734       },
4735       templateUrl: 'src/app/mwtnTopology/templates/ptpLinksGrid.tpl.html'
4736     };
4737   }]);
4738
4739   mwtnTopologyApp.filter('coordinateFilter', coordinateFilter);
4740
4741   function coordinateFilter($sce) {
4742
4743     return function (coordinate, conversion, type, places) {
4744
4745       // The filter will be running as we type values into the input boxes, which returns undefined
4746       // and brings up an error in the console. Here wait until the coordinate is defined
4747       if (coordinate != undefined) {
4748
4749         // Check for user input that is a positive or negative number with the option
4750         // that it is a float. Match only the numbers and not the white space or other characters
4751         var pattern = /[-+]?[0-9]*\.?[0-9]+/g
4752         var match = String(coordinate).match(pattern);
4753
4754         if (conversion === "toDD" && match && coordinateIsValid(match, type)) {
4755           // If the match array only has one item, the user has provided decimal degrees
4756           // and we can just return what the user typed in
4757           if (match.length === 1) {
4758             return parseFloat(match);
4759           }
4760
4761           // If the match array has a length of three then we know degrees, minutes, and seconds
4762           // were provided so we can convert it to decimal degrees
4763           if (match.length === 3) {
4764             return toDecimalDegrees(match);
4765           }
4766         }
4767
4768         else if (conversion === 'toDMS' && match && coordinateIsValid(match, type)) {
4769           // When converting from decimal degrees to degrees, minutes and seconds, if
4770           // the match array has one item we know the user has input decimal degrees
4771           // so we can convert it to degrees, minutes and seconds
4772           if (match.length === 1) {
4773             return toDegreesMinutesSeconds(match, type);
4774           }
4775
4776           // To properly format the converted coordinates we will need to add in HTML entities
4777           // which means we'll need to bind the returned string as HTML and thus we need
4778           // to use $sce (Strict Contextual Escaping) to say that we trust what is being bound as HTML
4779           if (match.length === 3) {
4780             return $sce.trustAsHtml(match[0] + '&deg; ' + match[1] + '&prime; ' + match[2] + '&Prime; ');
4781           }
4782         }
4783
4784         // Output a notice that the coordinates are invalid if they are
4785         else if (!coordinateIsValid(match, type)) {
4786           return "Invalid Coordinate!";
4787         }
4788
4789         function toDecimalDegrees(coord) {
4790           // Setup for all parts of the DMS coordinate and the necessary math to convert
4791           // from DMS to DD
4792           var degrees = parseInt(coord[0]);
4793           var minutes = parseInt(coord[1]) / 60;
4794           var seconds = parseInt(coord[2]) / 3600;
4795
4796           // When the degrees value is negative, the math is a bit different
4797           // than when the value is positive. This checks whether the value is below zero
4798           // and does subtraction instead of addition if it is. 
4799           if (degrees < 0) {
4800             var calculated = degrees - minutes - seconds;
4801             return calculated.toFixed(places || 4);
4802           }
4803           else {
4804             var calculated = degrees + minutes + seconds
4805             return calculated.toFixed(places || 4);
4806           }
4807         }
4808
4809         // This function converts from DD to DMS. Math.abs is used a lot because
4810         // for the minutes and seconds, negative values aren't valid 
4811         function toDegreesMinutesSeconds(decimal_degrees, type) {
4812
4813           var dd = decimal_degrees[0];
4814           var direction = 'E';
4815
4816           if (type === 'lat') {
4817             if (dd < 0) {
4818               direction = 'S';
4819             } else {
4820               direction = 'N'
4821             }
4822           } else {
4823             if (dd < 0) {
4824               direction = 'W';
4825             } else {
4826               direction = 'E'
4827             }
4828           }
4829
4830           dd = Math.abs(dd);
4831           var degrees = Math.floor(dd);
4832           var frac = dd - degrees; // get fractional part
4833           var min = Math.floor(frac * 60);
4834           var sec = frac * 3600 - min * 60;
4835
4836           var formated = [degrees, '° ', ("0" + min).slice(-2), '\' ', ("0" + sec.toFixed(4)).slice(-7), '\" ', direction];
4837           return formated.join('');
4838
4839           //           var degrees = coordinate[0].split('.')[0];
4840           //           var minutes = Math.abs(Math.floor(60 * (Math.abs(coordinate[0]) - Math.abs(degrees))));
4841           //           var seconds = 3600 * (Math.abs(coordinate[0]) - Math.abs(degrees) - Math.abs(minutes) / 60).toFixed(2);
4842
4843           //            return $sce.trustAsHtml(degrees + '° ' + minutes + '\' ' + seconds + '\" ');
4844
4845         }
4846
4847         // This function checks whether the coordinate value the user enters is valid or not. 
4848         // If the coordinate doesn't pass one of these rules, the function will return false
4849         // which will then alert the user that the coordinate is invalid.
4850         function coordinateIsValid(coordinate, type) {
4851           if (coordinate) {
4852
4853             // The degree values of latitude coordinates have a range between -90 and 90
4854             if (coordinate[0] && type === 'lat') {
4855               if (!(-90 <= +(coordinate[0]) <= 90)) return false;
4856             }
4857             // The degree values longitude coordinates have a range between -180 and 180
4858             else if (coordinate[0] && type === 'lon') {
4859               if (!(-180 <= +(coordinate[0]) <= 180)) return false;
4860             }
4861             // Minutes and seconds can only be between 0 and 60
4862             if (coordinate[1]) {
4863               if (!(0 <= +(coordinate[1]) <= 60)) return false;
4864             }
4865             if (coordinate[2]) {
4866               if (!(0 <= +(coordinate[2]) <= 60)) return false;
4867             }
4868           }
4869
4870           // If the coordinate made it through all the rules above, the function
4871           // returns true because the coordinate is good
4872           return true;
4873         }
4874       }
4875     }
4876   }
4877 });