Committed by
Gerrit Code Review
Topo2 Fixed device positions
Change-Id: I10e61981000b427ff1ebf6ae0c35bfb2cdbb9c4b
Showing
9 changed files
with
161 additions
and
35 deletions
... | @@ -65,3 +65,12 @@ layout-add l3 r3 l2 | ... | @@ -65,3 +65,12 @@ layout-add l3 r3 l2 |
65 | 65 | ||
66 | layouts | 66 | layouts |
67 | EOF | 67 | EOF |
68 | + | ||
69 | + | ||
70 | +onos ${host} <<-EOF | ||
71 | + | ||
72 | +log:set DEBUG org.onosproject.ui.impl.topo.Topo2ViewMessageHandler | ||
73 | +log:set DEBUG org.onosproject.ui.impl.topo.Topo2Jsonifier | ||
74 | +log:set DEBUG org.onosproject.ui.impl.UiWebSocket | ||
75 | +log:list | ||
76 | +EOF | ... | ... |
... | @@ -36,6 +36,7 @@ import org.onosproject.net.intent.IntentService; | ... | @@ -36,6 +36,7 @@ import org.onosproject.net.intent.IntentService; |
36 | import org.onosproject.net.link.LinkService; | 36 | import org.onosproject.net.link.LinkService; |
37 | import org.onosproject.net.statistic.StatisticService; | 37 | import org.onosproject.net.statistic.StatisticService; |
38 | import org.onosproject.net.topology.TopologyService; | 38 | import org.onosproject.net.topology.TopologyService; |
39 | +import org.onosproject.ui.JsonUtils; | ||
39 | import org.onosproject.ui.model.topo.UiClusterMember; | 40 | import org.onosproject.ui.model.topo.UiClusterMember; |
40 | import org.onosproject.ui.model.topo.UiDevice; | 41 | import org.onosproject.ui.model.topo.UiDevice; |
41 | import org.onosproject.ui.model.topo.UiHost; | 42 | import org.onosproject.ui.model.topo.UiHost; |
... | @@ -482,4 +483,18 @@ class Topo2Jsonifier { | ... | @@ -482,4 +483,18 @@ class Topo2Jsonifier { |
482 | 483 | ||
483 | return splitList; | 484 | return splitList; |
484 | } | 485 | } |
486 | + | ||
487 | + /** | ||
488 | + * Stores the memento for an element. | ||
489 | + * This method assumes the payload has an id String, memento ObjectNode | ||
490 | + * | ||
491 | + * @param payload event payload | ||
492 | + */ | ||
493 | + void updateMeta(ObjectNode payload) { | ||
494 | + | ||
495 | + String id = JsonUtils.string(payload, "id"); | ||
496 | + metaUi.put(id, JsonUtils.node(payload, "memento")); | ||
497 | + | ||
498 | + log.debug("Storing metadata for {}", id); | ||
499 | + } | ||
485 | } | 500 | } | ... | ... |
... | @@ -60,6 +60,7 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { | ... | @@ -60,6 +60,7 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { |
60 | private static final String START = "topo2Start"; | 60 | private static final String START = "topo2Start"; |
61 | private static final String NAV_REGION = "topo2navRegion"; | 61 | private static final String NAV_REGION = "topo2navRegion"; |
62 | private static final String STOP = "topo2Stop"; | 62 | private static final String STOP = "topo2Stop"; |
63 | + private static final String UPDATE_META2 = "updateMeta2"; | ||
63 | 64 | ||
64 | // === Outbound event identifiers | 65 | // === Outbound event identifiers |
65 | private static final String ALL_INSTANCES = "topo2AllInstances"; | 66 | private static final String ALL_INSTANCES = "topo2AllInstances"; |
... | @@ -87,7 +88,8 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { | ... | @@ -87,7 +88,8 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { |
87 | return ImmutableSet.of( | 88 | return ImmutableSet.of( |
88 | new Topo2Start(), | 89 | new Topo2Start(), |
89 | new Topo2NavRegion(), | 90 | new Topo2NavRegion(), |
90 | - new Topo2Stop() | 91 | + new Topo2Stop(), |
92 | + new Topo2UpdateMeta() | ||
91 | ); | 93 | ); |
92 | } | 94 | } |
93 | 95 | ||
... | @@ -196,4 +198,15 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { | ... | @@ -196,4 +198,15 @@ public class Topo2ViewMessageHandler extends UiMessageHandler { |
196 | } | 198 | } |
197 | } | 199 | } |
198 | 200 | ||
201 | + private final class Topo2UpdateMeta extends RequestHandler { | ||
202 | + private Topo2UpdateMeta() { | ||
203 | + super(UPDATE_META2); | ||
204 | + } | ||
205 | + | ||
206 | + @Override | ||
207 | + public void process(long sid, ObjectNode payload) { | ||
208 | + t2json.updateMeta(payload); | ||
209 | + } | ||
210 | + } | ||
211 | + | ||
199 | } | 212 | } | ... | ... |
... | @@ -25,7 +25,7 @@ | ... | @@ -25,7 +25,7 @@ |
25 | 25 | ||
26 | // references to injected services | 26 | // references to injected services |
27 | var $scope, $log, fs, mast, ks, zs, | 27 | var $scope, $log, fs, mast, ks, zs, |
28 | - gs, sus, ps, t2es, t2fs, t2is, t2bcs, t2kcs, t2ms; | 28 | + gs, sus, ps, t2es, t2fs, t2is, t2bcs, t2kcs, t2ms, t2mcs; |
29 | 29 | ||
30 | // DOM elements | 30 | // DOM elements |
31 | var ovtopo2, svg, defs, zoomLayer, forceG; | 31 | var ovtopo2, svg, defs, zoomLayer, forceG; |
... | @@ -88,12 +88,13 @@ | ... | @@ -88,12 +88,13 @@ |
88 | 'WebSocketService', 'PrefsService', 'ThemeService', | 88 | 'WebSocketService', 'PrefsService', 'ThemeService', |
89 | 'Topo2EventService', 'Topo2ForceService', 'Topo2InstanceService', | 89 | 'Topo2EventService', 'Topo2ForceService', 'Topo2InstanceService', |
90 | 'Topo2BreadcrumbService', 'Topo2KeyCommandService', 'Topo2MapService', | 90 | 'Topo2BreadcrumbService', 'Topo2KeyCommandService', 'Topo2MapService', |
91 | + 'Topo2MapConfigService', | ||
91 | 92 | ||
92 | function (_$scope_, _$log_, _$loc_, | 93 | function (_$scope_, _$log_, _$loc_, |
93 | _fs_, _mast_, _ks_, _zs_, | 94 | _fs_, _mast_, _ks_, _zs_, |
94 | _gs_, _ms_, _sus_, _flash_, | 95 | _gs_, _ms_, _sus_, _flash_, |
95 | _wss_, _ps_, _th_, | 96 | _wss_, _ps_, _th_, |
96 | - _t2es_, _t2fs_, _t2is_, _t2bcs_, _t2kcs_, _t2ms_) { | 97 | + _t2es_, _t2fs_, _t2is_, _t2bcs_, _t2kcs_, _t2ms_, _t2mcs_) { |
97 | 98 | ||
98 | var params = _$loc_.search(), | 99 | var params = _$loc_.search(), |
99 | dim, | 100 | dim, |
... | @@ -127,6 +128,7 @@ | ... | @@ -127,6 +128,7 @@ |
127 | t2bcs = _t2bcs_; | 128 | t2bcs = _t2bcs_; |
128 | t2kcs = _t2kcs_; | 129 | t2kcs = _t2kcs_; |
129 | t2ms = _t2ms_; | 130 | t2ms = _t2ms_; |
131 | + t2mcs = _t2mcs_; | ||
130 | 132 | ||
131 | // capture selected intent parameters (if they are set in the | 133 | // capture selected intent parameters (if they are set in the |
132 | // query string) so that the traffic overlay can highlight | 134 | // query string) so that the traffic overlay can highlight |
... | @@ -177,25 +179,25 @@ | ... | @@ -177,25 +179,25 @@ |
177 | function (proj) { | 179 | function (proj) { |
178 | var z = ps.getPrefs('topo_zoom', { tx: 0, ty: 0, sc: 1 }); | 180 | var z = ps.getPrefs('topo_zoom', { tx: 0, ty: 0, sc: 1 }); |
179 | zoomer.panZoom([z.tx, z.ty], z.sc); | 181 | zoomer.panZoom([z.tx, z.ty], z.sc); |
182 | + | ||
183 | + t2mcs.projection(proj); | ||
180 | $log.debug('** Zoom restored:', z); | 184 | $log.debug('** Zoom restored:', z); |
181 | $log.debug('** We installed the projection:', proj); | 185 | $log.debug('** We installed the projection:', proj); |
186 | + | ||
187 | + // Now the map has load and we have a projection we can | ||
188 | + // get the info from the server | ||
189 | + t2es.start(); | ||
190 | + | ||
182 | } | 191 | } |
183 | ); | 192 | ); |
184 | 193 | ||
185 | // initialize the force layout, ready to render the topology | 194 | // initialize the force layout, ready to render the topology |
186 | forceG = zoomLayer.append('g').attr('id', 'topo-force'); | 195 | forceG = zoomLayer.append('g').attr('id', 'topo-force'); |
196 | + | ||
187 | t2fs.init(svg, forceG, uplink, dim); | 197 | t2fs.init(svg, forceG, uplink, dim); |
188 | t2bcs.init(); | 198 | t2bcs.init(); |
189 | t2kcs.init(t2fs); | 199 | t2kcs.init(t2fs); |
190 | 200 | ||
191 | - | ||
192 | - // =-=-=-=-=-=-=-=- | ||
193 | - // TODO: in future, we will load background map data | ||
194 | - // asynchronously (hence the promise) and then chain off | ||
195 | - // there to send the topo2start event to the server. | ||
196 | - // For now, we'll send the event inline... | ||
197 | - t2es.start(); | ||
198 | - | ||
199 | t2is.initInst({ showMastership: t2fs.showMastership }); | 201 | t2is.initInst({ showMastership: t2fs.showMastership }); |
200 | 202 | ||
201 | // === ORIGINAL CODE === | 203 | // === ORIGINAL CODE === | ... | ... |
... | @@ -22,7 +22,7 @@ | ... | @@ -22,7 +22,7 @@ |
22 | (function () { | 22 | (function () { |
23 | 'use strict'; | 23 | 'use strict'; |
24 | 24 | ||
25 | - var $log, sus, t2rs, t2d3, t2vs, t2ss; | 25 | + var $log, wss, sus, t2rs, t2d3, t2vs, t2ss; |
26 | 26 | ||
27 | var uplink, linkG, linkLabelG, nodeG; | 27 | var uplink, linkG, linkLabelG, nodeG; |
28 | var link, node; | 28 | var link, node; |
... | @@ -142,7 +142,7 @@ | ... | @@ -142,7 +142,7 @@ |
142 | // once we've finished moving, pin the node in position | 142 | // once we've finished moving, pin the node in position |
143 | d.fixed = true; | 143 | d.fixed = true; |
144 | d3.select(this).classed('fixed', true); | 144 | d3.select(this).classed('fixed', true); |
145 | - // TODO: sendUpdateMeta(d); | 145 | + sendUpdateMeta(d); |
146 | t2ss.clickConsumed(true); | 146 | t2ss.clickConsumed(true); |
147 | } | 147 | } |
148 | 148 | ||
... | @@ -153,6 +153,31 @@ | ... | @@ -153,6 +153,31 @@ |
153 | return !nodeLock && !zoomingOrPanning(ev); | 153 | return !nodeLock && !zoomingOrPanning(ev); |
154 | } | 154 | } |
155 | 155 | ||
156 | + function sendUpdateMeta(d, clearPos) { | ||
157 | + var metaUi = {}, | ||
158 | + ll; | ||
159 | + | ||
160 | + // if we are not clearing the position data (unpinning), | ||
161 | + // attach the x, y, (and equivalent longitude, latitude)... | ||
162 | + if (!clearPos) { | ||
163 | + ll = d.lngLatFromCoord([d.x, d.y]); | ||
164 | + metaUi = { | ||
165 | + x: d.x, | ||
166 | + y: d.y, | ||
167 | + equivLoc: { | ||
168 | + lng: ll[0], | ||
169 | + lat: ll[1] | ||
170 | + } | ||
171 | + }; | ||
172 | + } | ||
173 | + d.metaUi = metaUi; | ||
174 | + wss.sendEvent('updateMeta2', { | ||
175 | + id: d.get('id'), | ||
176 | + class: d.get('class'), | ||
177 | + memento: metaUi | ||
178 | + }); | ||
179 | + } | ||
180 | + | ||
156 | // predicate that indicates when clicking is active | 181 | // predicate that indicates when clicking is active |
157 | function clickEnabled() { | 182 | function clickEnabled() { |
158 | return true; | 183 | return true; |
... | @@ -396,12 +421,13 @@ | ... | @@ -396,12 +421,13 @@ |
396 | angular.module('ovTopo2') | 421 | angular.module('ovTopo2') |
397 | .factory('Topo2LayoutService', | 422 | .factory('Topo2LayoutService', |
398 | [ | 423 | [ |
399 | - '$log', 'SvgUtilService', 'Topo2RegionService', | 424 | + '$log', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService', |
400 | 'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService', | 425 | 'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService', |
401 | 426 | ||
402 | - function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) { | 427 | + function (_$log_, _wss_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) { |
403 | 428 | ||
404 | $log = _$log_; | 429 | $log = _$log_; |
430 | + wss = _wss_; | ||
405 | t2rs = _t2rs_; | 431 | t2rs = _t2rs_; |
406 | t2d3 = _t2d3_; | 432 | t2d3 = _t2d3_; |
407 | t2vs = _t2vs_; | 433 | t2vs = _t2vs_; | ... | ... |
... | @@ -55,7 +55,7 @@ | ... | @@ -55,7 +55,7 @@ |
55 | 55 | ||
56 | if (mapFilePath === '*countries') { | 56 | if (mapFilePath === '*countries') { |
57 | cfilter = countryFilters[mapId] || countryFilters.uk; | 57 | cfilter = countryFilters[mapId] || countryFilters.uk; |
58 | - loadMap = ms.loadMapRegionInto | 58 | + loadMap = ms.loadMapRegionInto; |
59 | } | 59 | } |
60 | 60 | ||
61 | promise = loadMap(mapG, mapFilePath, mapId, { | 61 | promise = loadMap(mapG, mapFilePath, mapId, { | ... | ... |
1 | +/* | ||
2 | + * Copyright 2016-present Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/* | ||
18 | + ONOS GUI -- Topology Layout Module. | ||
19 | + Module that contains the d3.force.layout logic | ||
20 | + */ | ||
21 | + | ||
22 | +(function () { | ||
23 | + 'use strict'; | ||
24 | + | ||
25 | + // Injected refs | ||
26 | + var $log; | ||
27 | + | ||
28 | + // Internal State | ||
29 | + var proj; | ||
30 | + | ||
31 | + function projection(x) { | ||
32 | + if (x) { | ||
33 | + proj = x; | ||
34 | + $log.debug("Set the projection"); | ||
35 | + } | ||
36 | + return proj; | ||
37 | + } | ||
38 | + | ||
39 | + angular.module('ovTopo2') | ||
40 | + .factory('Topo2MapConfigService', | ||
41 | + ['$log', | ||
42 | + function (_$log_) { | ||
43 | + | ||
44 | + $log = _$log_; | ||
45 | + | ||
46 | + return { | ||
47 | + projection: projection | ||
48 | + }; | ||
49 | + } | ||
50 | + ] | ||
51 | + ); | ||
52 | +})(); |
... | @@ -22,7 +22,7 @@ | ... | @@ -22,7 +22,7 @@ |
22 | (function () { | 22 | (function () { |
23 | 'use strict'; | 23 | 'use strict'; |
24 | 24 | ||
25 | - var randomService, ps, sus, is, ts; | 25 | + var randomService, ps, sus, is, ts, t2mcs; |
26 | var fn; | 26 | var fn; |
27 | 27 | ||
28 | // Internal state; | 28 | // Internal state; |
... | @@ -55,8 +55,7 @@ | ... | @@ -55,8 +55,7 @@ |
55 | } | 55 | } |
56 | 56 | ||
57 | function positionNode(node, forUpdate) { | 57 | function positionNode(node, forUpdate) { |
58 | - | 58 | + var meta = node.get('metaUi'), |
59 | - var meta = node.metaUi, | ||
60 | x = meta && meta.x, | 59 | x = meta && meta.x, |
61 | y = meta && meta.y, | 60 | y = meta && meta.y, |
62 | dim = [800, 600], | 61 | dim = [800, 600], |
... | @@ -64,7 +63,6 @@ | ... | @@ -64,7 +63,6 @@ |
64 | 63 | ||
65 | // If the device contains explicit LONG/LAT data, use that to position | 64 | // If the device contains explicit LONG/LAT data, use that to position |
66 | if (setLongLat(node)) { | 65 | if (setLongLat(node)) { |
67 | - // Indicate we want to update cached meta data... | ||
68 | return true; | 66 | return true; |
69 | } | 67 | } |
70 | 68 | ||
... | @@ -103,13 +101,16 @@ | ... | @@ -103,13 +101,16 @@ |
103 | } | 101 | } |
104 | 102 | ||
105 | function getDevice(cp) { | 103 | function getDevice(cp) { |
106 | - // console.log(cp); | ||
107 | - // var d = lu[cp.device]; | ||
108 | - // return d || rand(); | ||
109 | return rand(); | 104 | return rand(); |
110 | } | 105 | } |
111 | 106 | ||
112 | xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand(); | 107 | xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand(); |
108 | + | ||
109 | + if (node.class === 'sub-region') { | ||
110 | + xy = rand(); | ||
111 | + node.x = node.px = xy.x; | ||
112 | + node.y = node.py = xy.y; | ||
113 | + } | ||
113 | angular.extend(node, xy); | 114 | angular.extend(node, xy); |
114 | } | 115 | } |
115 | 116 | ||
... | @@ -118,7 +119,7 @@ | ... | @@ -118,7 +119,7 @@ |
118 | coord; | 119 | coord; |
119 | 120 | ||
120 | if (loc && loc.type === 'lnglat') { | 121 | if (loc && loc.type === 'lnglat') { |
121 | - coord = [0, 0]; | 122 | + coord = coordFromLngLat(loc); |
122 | node.fixed = true; | 123 | node.fixed = true; |
123 | node.px = node.x = coord[0]; | 124 | node.px = node.x = coord[0]; |
124 | node.py = node.y = coord[1]; | 125 | node.py = node.y = coord[1]; |
... | @@ -126,11 +127,16 @@ | ... | @@ -126,11 +127,16 @@ |
126 | } | 127 | } |
127 | } | 128 | } |
128 | 129 | ||
130 | + function coordFromLngLat(loc) { | ||
131 | + var p = t2mcs.projection(); | ||
132 | + return p ? p.invert([loc.lng, loc.lat]) : [0, 0]; | ||
133 | + } | ||
134 | + | ||
129 | angular.module('ovTopo2') | 135 | angular.module('ovTopo2') |
130 | .factory('Topo2NodeModel', | 136 | .factory('Topo2NodeModel', |
131 | ['Topo2Model', 'FnService', 'RandomService', 'Topo2PrefsService', | 137 | ['Topo2Model', 'FnService', 'RandomService', 'Topo2PrefsService', |
132 | - 'SvgUtilService', 'IconService', 'ThemeService', | 138 | + 'SvgUtilService', 'IconService', 'ThemeService', 'Topo2MapConfigService', |
133 | - function (Model, _fn_, _RandomService_, _ps_, _sus_, _is_, _ts_) { | 139 | + function (Model, _fn_, _RandomService_, _ps_, _sus_, _is_, _ts_, _t2mcs_) { |
134 | 140 | ||
135 | randomService = _RandomService_; | 141 | randomService = _RandomService_; |
136 | ts = _ts_; | 142 | ts = _ts_; |
... | @@ -138,11 +144,18 @@ | ... | @@ -138,11 +144,18 @@ |
138 | ps = _ps_; | 144 | ps = _ps_; |
139 | sus = _sus_; | 145 | sus = _sus_; |
140 | is = _is_; | 146 | is = _is_; |
147 | + t2mcs = _t2mcs_; | ||
141 | 148 | ||
142 | return Model.extend({ | 149 | return Model.extend({ |
143 | initialize: function () { | 150 | initialize: function () { |
144 | this.node = this.createNode(); | 151 | this.node = this.createNode(); |
145 | }, | 152 | }, |
153 | + createNode: function () { | ||
154 | + this.set('class', this.nodeType); | ||
155 | + this.set('svgClass', this.svgClassName()); | ||
156 | + positionNode(this); | ||
157 | + return this; | ||
158 | + }, | ||
146 | setUpEvents: function () { | 159 | setUpEvents: function () { |
147 | var _this = this; | 160 | var _this = this; |
148 | angular.forEach(this.events, function (handler, key) { | 161 | angular.forEach(this.events, function (handler, key) { |
... | @@ -221,6 +234,10 @@ | ... | @@ -221,6 +234,10 @@ |
221 | } | 234 | } |
222 | ); | 235 | ); |
223 | }, | 236 | }, |
237 | + lngLatFromCoord: function (coord) { | ||
238 | + var p = t2mcs.projection(); | ||
239 | + return p ? p.invert(coord) : [0, 0]; | ||
240 | + }, | ||
224 | update: function () { | 241 | update: function () { |
225 | this.updateLabel(); | 242 | this.updateLabel(); |
226 | }, | 243 | }, |
... | @@ -236,15 +253,6 @@ | ... | @@ -236,15 +253,6 @@ |
236 | .transition() | 253 | .transition() |
237 | .attr(this.labelBox(devIconDim, labelWidth)); | 254 | .attr(this.labelBox(devIconDim, labelWidth)); |
238 | }, | 255 | }, |
239 | - createNode: function () { | ||
240 | - var node = angular.extend({}, this.attributes); | ||
241 | - | ||
242 | - // Augment as needed... | ||
243 | - node.class = this.nodeType; | ||
244 | - node.svgClass = this.svgClassName(); | ||
245 | - positionNode(node); | ||
246 | - return node; | ||
247 | - }, | ||
248 | onEnter: function (el) { | 256 | onEnter: function (el) { |
249 | this.el = d3.select(el); | 257 | this.el = d3.select(el); |
250 | this.render(); | 258 | this.render(); | ... | ... |
... | @@ -141,6 +141,7 @@ | ... | @@ -141,6 +141,7 @@ |
141 | <script src="app/view/topo2/topo2Link.js"></script> | 141 | <script src="app/view/topo2/topo2Link.js"></script> |
142 | <script src="app/view/topo2/topo2Map.js"></script> | 142 | <script src="app/view/topo2/topo2Map.js"></script> |
143 | <script src="app/view/topo2/topo2MapCountryFilters.js"></script> | 143 | <script src="app/view/topo2/topo2MapCountryFilters.js"></script> |
144 | + <script src="app/view/topo2/topo2MapConfig.js"></script> | ||
144 | <script src="app/view/topo2/topo2MapDialog.js"></script> | 145 | <script src="app/view/topo2/topo2MapDialog.js"></script> |
145 | <script src="app/view/topo2/topo2Model.js"></script> | 146 | <script src="app/view/topo2/topo2Model.js"></script> |
146 | <script src="app/view/topo2/topo2NodeModel.js"></script> | 147 | <script src="app/view/topo2/topo2NodeModel.js"></script> | ... | ... |
-
Please register or login to post a comment