source: src/main/webapp/newsession.xhtml@ 27

Last change on this file since 27 was 27, checked in by bart, 4 years ago

New protocols Learn and APPLearn. Fixed memory leak.

File size: 22.7 KB
Line 
1<?xml version="1.0" encoding="UTF-8"?>
2<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3<head>
4<title>Profiles and Domains list</title>
5<link rel="stylesheet" type="text/css" href="style.css" />
6</head>
7<body onload="init()">
8 <h1>Session</h1>
9
10 On this page you can configure the settings for running a new session
11 and start the session.
12
13 <br /> Protocol:
14 <select id="selectedprotocol" onchange="selectProtocol()">
15 <option value="SAOP">SAOP ( Stacked Alternating Offers
16 Protocol )</option>
17 <option value="MOPAC">MOPAC (Multiple Offers Partial
18 Consensus)</option>
19 <option value="MOPAC2">MOPAC2 (MOPAC but with values for each vote)</option>
20 <option value="AMOP">AMOP (Alternating Multiple Offers
21 Protocol)</option>
22 <option value="SHAOP">SHAOP (Stacked Human Alternating Offers
23 Protocol)</option>
24 <option value="Learn">Learn</option>
25
26 </select>
27
28 <div id="votingevaluator">
29 <br /> Voting Evaluator: <select id="selectedevaluator">
30 <option value="LargestAgreement">Largest Agreement</option>
31 <option value="LargestAgreementsLoop">Largest Agreements and Repeat</option>
32 </select>
33 </div>
34 <div id="votingevaluator2">
35 <br /> Voting Evaluator: <select id="selectedevaluator2">
36 <option value="LargestAgreementWithValue">Largest Agreement with Values</option>
37 </select>
38 </div>
39
40
41 <br /> Deadline:
42 <input type="number" id="deadlinevalue" name="deadline" min="1"
43 max="10000" value="10" />
44 <select id="deadlinetype">
45 <option value="ROUNDS">rounds</option>
46 <option value="TIME">seconds</option>
47 </select>
48
49 <div id="profserverselector">
50 <br /> Domain/Profile Server:
51 <input type="url" name="url" id="profilesserverurl"
52 value="localhost:8080/profilesserver-1.6.0"
53 pattern=".*:[0-9]+/profilesserver.*" size="30"
54 onchange="connectDomain()"> </input>
55 <br /> Domain:
56 <select id="domainselection" onchange="selectDomain()">
57 <!-- <option>Waiting for profiles server</option> -->
58 </select>
59 </div>
60
61 <br />
62 <br />
63
64 <div id="box" class="box">
65 <br /> <b>Participants</b> <br /> Parties Server: <input type="url"
66 name="url" id="partiesserverurl"
67 value="localhost:8080/partiesserver-1.6.0"
68 pattern=".*:[0-9]+/partiesserver.*" size="30"
69 onchange="connectParties()"> </input> <br /> <br /> <b>Party
70 settings</b> <br /> Party : <select id="partyselection">
71 </select> <br />
72
73 <div id="profileselector">
74 Profile: <select id="profileselection"></select> Filter: <input
75 type="text" id="filter" value="" maxlength="40" /> <br />
76 </div>
77 Parameters: {
78 <textarea id="parameters" rows="2" cols="70"
79 onchange="updateParameters()" value="" />
80 } <br /> <br />
81
82 <div id="cobsetting">
83 <input type="checkbox" id="advancedCobSettings"
84 onchange="advancedCobSet()"></input> Advanced COB settings<br />
85 <div id="advancedsettings" style="display: none">
86 <b>COB party settings</b> <br /> Party : <select
87 id="cobpartyselection">
88 </select> <br /> Profile: <select id="cobprofileselection"></select> Filter:
89 <input type="text" id="cobfilter" value="" maxlength="40" /> <br />
90 <!-- -->
91 Parameters: {
92 <textarea id="cobparameters" rows="2" cols="70"
93 onchange="updateCobParameters()" value="" />
94 } <br /> <br />
95 </div>
96 </div>
97
98 <button onclick="addParty()">Add</button>
99
100
101 <br /> <br /> <b>Selected Profiles, Parties for the session</b>
102 <table id="selectedpartiestable" width="100%">
103 <colgroup>
104 <col />
105 <col />
106 <col />
107 <col />
108 <col />
109 <col />
110 </colgroup>
111 <thead>
112 <tr>
113 <th align="center">Party</th>
114 <th align="center">Parameters</th>
115 <th align="center">Profile</th>
116
117 <th align="center">COB Party</th>
118 <th align="center">COB Parameters</th>
119 <th align="center">COB Profile</th>
120 </tr>
121 </thead>
122 <tbody id="partiesList">
123 </tbody>
124 </table>
125
126 </div>
127
128 <form>
129 <input id="startbutton" type="button" value="Start Session"
130 onclick="start()" />
131 </form>
132
133 <div id="started" style="visibility: hidden">
134 Your session started. Waiting for the results. <br />
135 </div>
136 <div id="results" style="visibility: hidden">
137 Session completed. <a href="" id="logref">view the log file</a> <br />
138 <a href="" id="plotref">render a utilities plot</a>.
139 </div>
140
141</body>
142
143<script type="application/javascript">
144
145
146
147
148
149
150
151
152
153
154
155 // FIXME quick and dirty code. No clean MVC
156
157
158 <![CDATA[
159 //"use strict";
160
161 var domainwebsocket = null;
162 var partieswebsocket=null;
163 // current setting of parameters
164 var parameters = {};
165
166 // currently known domains (and profiles) as coming from domainwebsocket.
167 // keys are domain names, values are list of profile names
168 var knowndomains={};
169
170
171 /**
172 List of created participants for the session. Each participant is a dictionary.
173 Each dict element contains keys
174 "party" and "profile", both containing a String containing
175 a valid IRI to the party resp. the profile to use.
176 The party should contain a IRI that gives a new instance of the required party.
177 The profile should contain an IRI that gives the profile contents.
178 */
179 var partyprofiles=[];
180
181 var cobpartyprofiles=[];
182
183 /** from http://fitzgeraldnick.com/2010/08/10/settimeout-patterns.html */
184
185 function getAdvancedCobSettings() {
186 return document.getElementById('advancedCobSettings').checked;
187 }
188
189 function advancedCobSet() {
190 document.getElementById('advancedsettings').style.display=(getAdvancedCobSettings()?'':'none');
191 }
192
193 function async (fn) {
194 setTimeout(fn, 1000);
195 }
196
197 function sometimeWhen (test, then) {
198 async(function () {
199 if ( test() ) {
200 then();
201 } else {
202 async(arguments.callee);
203 }
204 });
205 }
206
207 /**
208 Called when user changes the protocol */
209 function selectProtocol() {
210 var visible=getSelectedProtocol() == "SHAOP";
211
212 document.getElementById("cobsetting").style.display=(visible ? 'block': 'none');
213 var tbl = document.getElementById('selectedpartiestable');
214 tbl.getElementsByTagName('col')[3].style.visibility=(visible?'':'collapse');
215 tbl.getElementsByTagName('col')[4].style.visibility=(visible?'':'collapse');
216 tbl.getElementsByTagName('col')[5].style.visibility=(visible?'':'collapse');
217
218 var evaluatorvisible=getSelectedProtocol() == "MOPAC";
219 document.getElementById("votingevaluator").style.display=(evaluatorvisible ? 'block': 'none');
220
221 var evaluator2visible=getSelectedProtocol() == "MOPAC2";
222 document.getElementById("votingevaluator2").style.display=(evaluator2visible ? 'block': 'none');
223
224
225 var profileselectorvisible=!isUsingFakeProfile();
226 document.getElementById("profserverselector").style.display=(profileselectorvisible ? 'block': 'none');
227 document.getElementById("profileselector").style.display=(profileselectorvisible ? 'block': 'none');
228 if (getSelectedProtocol() == "Learn") {
229 params = {"persistentstate":createUUID(), "negotiationdata":[ createUUID() ] };
230 str=JSON.stringify(params);
231 // remove outer brackets from the text in the box
232 document.getElementById("parameters").value=str.substring(1, str.length-1);
233 updateParameters();
234 }
235 }
236
237 /**
238 @return true iff we need to use a fake profile.
239 */
240 function isUsingFakeProfile() {
241 return getSelectedProtocol() == "Learn";
242 }
243
244 function isUsingCobParty() {
245 return getSelectedProtocol() == "SHAOP";
246 }
247
248 /**
249 Refresh known domains using given profilesserver URL.
250 Called when user enters URL for domain server.
251 */
252 function connectDomain() {
253 if (domainwebsocket!=null) {
254 domainwebsocket.close();
255 domainwebsocket=null;
256 }
257 var url=new URL("http:"+document.getElementById("profilesserverurl").value);
258 // insert the liststream to the path
259 var target = "ws://"+url.host+url.pathname+"/websocket/liststream"+window.location.search+url.hash;
260 if ('WebSocket' in window) {
261 domainwebsocket = new WebSocket(target);
262 } else if ('MozWebSocket' in window) {
263 domainwebsocket = new MozWebSocket(target);
264 } else {
265 alert('WebSocket is not supported by this browser. Please use a newer browser');
266 return;
267 }
268 domainwebsocket.onopen = function () {
269 // whatever.
270 };
271 domainwebsocket.onmessage = function (event) {
272 updateDomainComboBox(JSON.parse(event.data));
273 };
274 domainwebsocket.onclose = function (event) {
275 alert('Info: Server closed connection. Code: ' + event.code +
276 (event.reason == "" ? "" : ", Reason: " + event.reason));
277 domainwebsocket=null;
278 updateDomainComboBox({});
279 };
280 }
281
282 /**
283 Sets a new knowndomains value and Updates the contents of the domain selector combobox.
284 @param the known domains, a map of the form {"jobs":["jobs1","jobs2"]}
285 where the keys are the names of the available domains nd the values a list of the available profiles in that domain.
286
287 */
288 function updateDomainComboBox(newdomains) {
289 knowndomains=newdomains
290 var combobox = document.getElementById("domainselection");
291 combobox.options.length=0;
292 for (var domain in knowndomains) {
293 var option = document.createElement('option');
294 option.text = option.value = domain;
295 combobox.add(option, 0);
296 }
297 selectDomain();
298 }
299 /**
300 Refresh known parties using given partiesserver URL.
301 Called when user enters URL for parties server.
302 */
303 function connectParties() {
304 if (partieswebsocket!=null) {
305 partieswebsocket.close();
306 partieswebsocket=null;
307 }
308 var url=document.getElementById("partiesserverurl").value;
309 var target = "ws://"+url+"/available";
310 if ('WebSocket' in window) {
311 partieswebsocket = new WebSocket(target);
312 } else if ('MozWebSocket' in window) {
313 partieswebsocket = new MozWebSocket(target);
314 } else {
315 alert('WebSocket is not supported by this browser. Please use a newer browser');
316 return;
317 }
318 partieswebsocket.onopen = function () {
319 // whatever.
320 };
321 partieswebsocket.onmessage = function (event) {
322 updateParties(JSON.parse(event.data));
323 };
324 partieswebsocket.onclose = function (event) {
325 alert('Info: Server closed connection. Code: ' + event.code +
326 (event.reason == "" ? "" : ", Reason: " + event.reason));
327 partieswebsocket=null;
328 updateParties({});
329 };
330 }
331
332
333 /**
334 refresh table: copy all parties elements in there.
335 Typically parties is something like
336 [{"uri":"http:130.161.180.1:8080/partiesserver/run/randomparty-1.6.0",
337 "capabilities":{"protocols":["SAOP"]},
338 "description":"places random bids until it can accept an offer with utility >0.6",
339 "id":"randomparty-1.6.0",
340 "partyClass":"geniusweb.exampleparties.randomparty.RandomParty"},
341 ...]
342 */
343 function updateParties(parties) {
344 updatePartiesCombobox(parties, document.getElementById("partyselection"),['SAOP','AMOP','SHAOP','MOPAC', 'MOPAC2']);
345 updatePartiesCombobox(parties, document.getElementById("cobpartyselection"),['COB']);
346 }
347
348 /**
349 @param parties a list of parties r(as received from the server)
350 @param combobox a combobox element to put the party URLs in
351 @param behaviours a list of behaviours for the parties. Only these are put in the combobox.
352 */
353 function updatePartiesCombobox(parties, combobox, behaviours) {
354 combobox.options.length=0;
355 for (var p in parties) {
356 var party = parties[p];
357 if (intersect(party.capabilities.behaviours, behaviours).length==0)
358 continue;
359 var option = document.createElement('option');
360 option.text = option.value = party.uri;
361 combobox.add(option, 0);
362 }
363
364 }
365
366 /**
367 @param a a list
368 @param b another list
369 @return intersection of a and b
370 */
371 function intersect(a, b) {
372 var t;
373 if (b.length > a.length) t = b, b = a, a = t; // indexOf to loop over shorter
374 return a.filter(function (e) {
375 return b.indexOf(e) > -1;
376 });
377 }
378
379
380 /**
381 updates parameters field to match the given text.
382 */
383 function updateParameters() {
384 var text="{"+document.getElementById("parameters").value+"}";
385 try {
386 parameters=JSON.parse(text);
387 } catch(e) {
388 alert("Parameters can not be parsed. "+e);
389 return;
390 }
391 }
392
393 /**
394 Called when the selected domain changes. Assumes knowndomains has been set.
395 Updates the available profiles in the profile combobox.
396 @param selection the name of the selected domain.
397 */
398 function selectDomain() {
399 // determined current selection
400 var domaincombobox = document.getElementById("domainselection");
401 if (domaincombobox.options.length==0) return; // fixme clean profiles options?
402 var domain = domaincombobox.options[domaincombobox.selectedIndex].value;
403
404 updateProfileComboBox(document.getElementById("profileselection"), knowndomains[domain]);
405 updateProfileComboBox(document.getElementById("cobprofileselection"), knowndomains[domain]);
406 }
407
408 function updateProfileComboBox(profilecombo, options) {
409 profilecombo.options.length=0;
410 for (var profile in options) {
411 var option = document.createElement('option');
412 option.text = option.value = options[profile];
413 profilecombo.add(option, 0);
414 }
415
416 }
417
418 /**
419 Called when user clicks "Add"
420 */
421 function addParty() {
422 addNormalParty();
423 if (isUsingCobParty()) {
424 addCobParty();
425 }
426 updatePartyProfileTable(); // what, MVC?
427 }
428
429 function addNormalParty() {
430 var partycombo = document.getElementById("partyselection");
431 var profilecombo = document.getElementById("profileselection");
432 var filteroptions = document.getElementById("filter").value;
433
434 var selectedprofile;
435 if (isUsingFakeProfile()) {
436 selectedprofile="not://valid";
437 } else {
438 if (partycombo.options.length==0) {
439 alert("Please set partier server and select a party");
440 return;
441 }
442 if (profilecombo.options.length==0) {
443 alert("Please set domain/profile server and select a domain and a profile");
444 return;
445 }
446 selectedprofile=profilecombo.options[profilecombo.selectedIndex].value +filteroptions;
447 }
448
449 if (filteroptions!="") {
450 filteroptions="?"+filteroptions;
451 }
452 var newpartyprof = {};
453 newpartyprof["party"]={"partyref":partycombo.options[partycombo.selectedIndex].value ,
454 "parameters":parameters };
455 newpartyprof["profile"]=selectedprofile;
456
457 partyprofiles.push(newpartyprof)
458 }
459
460
461 function addCobParty() {
462 // we assume a sensible default has been loaded into the combo and set,
463 // regardless it being invisible
464 var partycombo = document.getElementById("cobpartyselection");
465 if (partycombo.options.length==0) {
466 alert("Please set cpb partier server and select a party");
467 return;
468 }
469
470 var newpartyprof = {};
471 if (getAdvancedCobSettings()) {
472 var profilecombo = document.getElementById("cobprofileselection");
473 var filteroptions = document.getElementById("cobfilter").value;
474
475 if (profilecombo.options.length==0) {
476 alert("Please set a cob profile");
477 return;
478 }
479 if (filteroptions!="") {
480 filteroptions="?"+filteroptions;
481 }
482 newpartyprof["party"]={"partyref":partycombo.options[partycombo.selectedIndex].value ,
483 "parameters":parameters };
484 newpartyprof["profile"]=profilecombo.options[profilecombo.selectedIndex].value +filteroptions;
485 } else {
486 var profilecombo = document.getElementById("profileselection");
487 newpartyprof["party"]={"partyref":partycombo.options[partycombo.selectedIndex].value ,
488 "parameters":{} };
489 newpartyprof["profile"]=profilecombo.options[profilecombo.selectedIndex].value;
490 }
491 cobpartyprofiles.push(newpartyprof)
492 }
493
494
495 /** updates the party and profiles table, to match the #partyprofiles list. */
496 function updatePartyProfileTable() {
497 var table = document.getElementById("partiesList");
498 table.innerHTML = ""; // clear table
499 for ( var pp in partyprofiles) {
500 var row = table.insertRow(-1);
501 var cell1 = row.insertCell(-1);
502 var cell2 = row.insertCell(-1);
503 var cell3 = row.insertCell(-1);
504
505 cell1.innerHTML = partyprofiles[pp]["party"]["partyref"];
506 // help browser breaking too large strings
507 cell2.innerHTML = JSON.stringify(partyprofiles[pp]["party"]["parameters"]).replace(/,/g,", ");
508 cell2.setAttribute("style","overflow-wrap: anywhere;");
509 cell3.innerHTML = partyprofiles[pp]["profile"];
510
511 if (isUsingCobParty()) {
512 var cell4 = row.insertCell(-1);
513 var cell5 = row.insertCell(-1);
514 var cell6 = row.insertCell(-1);
515 cell4.innerHTML = cobpartyprofiles[pp]["party"]["partyref"];
516 cell5.innerHTML = JSON.stringify(cobpartyprofiles[pp]["party"]["parameters"]).replace(/,/g,", ");
517 cell5.setAttribute("style","overflow-wrap: anywhere;");
518 cell6.innerHTML = cobpartyprofiles[pp]["profile"];
519
520 }
521
522 }
523
524 }
525
526 var x=1;
527 /**
528 start the session as currently set on this page.
529 We need to send a SessionSettings object to the server, which typically looks like this
530 but is protocol dependent (currently we do SAOP)
531
532 {"SAOPSettings":
533 {"participants":[
534 {"party":{"partyref":"http://party1","parameters":{}},"profile":"ws://profile1"},
535 {"party":{"partyref",}"http://party2","parameters":{}},"profile":"ws://profile2"}],
536 "deadline":{"deadlinetime":{"durationms":100}}}}
537
538 participants are already in the global partyprofiles dictionary
539 */
540 function start() {
541 var minParties = getSelectedProtocol() == "Learn" ? 1: 2;
542 if (Object.keys(partyprofiles).length < minParties) {
543 alert("At least "+minParties+" parties are needed to run a "+getSelectedProtocol()+" session.");
544 return;
545 }
546
547 // see https://www.w3schools.com/xml/dom_httprequest.asp
548 document.getElementById("startbutton").disabled=true;
549 document.getElementById("started").setAttribute("style","");
550
551 var xmlHttp = new XMLHttpRequest();
552 xmlHttp.onreadystatechange = function() {
553 if (this.readyState == 4) {
554 if (this.status == 200) {
555 var logurl="log/"+this.responseText+".json";
556 document.getElementById("logref").href=logurl;
557 document.getElementById("plotref").href="plotlog.xhtml"+
558 combineQuery("?id="+this.responseText,window.location.search);
559
560 sometimeWhen(function() { return urlExists(logurl) },
561 function() { document.getElementById("results").setAttribute("style",""); });
562
563 }
564 else
565 alert("request failed:"+this.statusText);
566 }
567 }
568 xmlHttp.open("POST", "run", true);
569 xmlHttp.send(makeRequest());
570
571 }
572
573
574
575 /**
576 @param query1 a query string like &p=q (part of URL)
577 @param query2 another query string
578 @return the combined querystring of query1 and query2
579 */
580 function combineQuery(query1, query2) {
581 if (query1=="") return query2;
582 if (query2=="") return query1;
583 return query1+"&"+query2.substring(1);
584 }
585
586
587 /**
588 @return true iff the URL exists.
589 */
590 function urlExists(urlToFile) {
591 // Warning. Synchronous XMLHttpRequest on the main thread is
592 // deprecated because of its detrimental effects to the end user’s
593 // experience. For more help http://xhr.spec.whatwg.org/
594 var xhr = new XMLHttpRequest();
595 xhr.open('HEAD', urlToFile, false);
596 xhr.send();
597
598 return (xhr.status == "200")
599 }
600
601 function getSelectedProtocol() {
602 var protocolcombo = document.getElementById("selectedprotocol");
603 return protocolcombo.options[protocolcombo.selectedIndex].value;
604 }
605 /**
606 @return a json request package for the run server
607 */
608 function makeRequest() {
609 switch(getSelectedProtocol()) {
610 case "SHAOP":
611 return makeShaopRequest();
612 case"SAOP":
613 case "AMOP":
614 case "Learn":
615 return makeStdRequest(getSelectedProtocol()+"Settings");
616 case "MOPAC":
617 return makeMopacRequest();
618 case "MOPAC2":
619 return makeMopac2Request();
620 }
621 }
622
623 /**
624 @return a SAOP/AMOP request
625 The header contains 'SAOSettings' or 'AMOPSettings'
626 */
627 function makeStdRequest(header) {
628 return JSON.stringify({[header]: standardHeader() });
629 }
630
631 /**
632 * @ereturn MOPAC request. Almost standard but extra 'votingEvaluator'.
633 */
634 function makeMopacRequest() {
635 var extended = standardHeader();
636 var combo = document.getElementById("selectedevaluator");
637 var evaluator = combo.options[combo.selectedIndex].value;
638 extended['votingevaluator'] = { [evaluator]: {} };
639 return JSON.stringify({"MOPACSettings": extended });
640 }
641
642 /**
643 * @ereturn MOPAC2 request. Almost standard but extra 'votingEvaluator'.
644 */
645 function makeMopac2Request() {
646 var extended = standardHeader();
647 var combo = document.getElementById("selectedevaluator2");
648 var evaluator = combo.options[combo.selectedIndex].value;
649 extended['votingevaluator'] = { [evaluator]: {} };
650 return JSON.stringify({"MOPAC2Settings": extended });
651 }
652
653 /**
654 @return standard header with participants and deadline
655 */
656 function standardHeader() {
657 var deadline={};
658 var value = document.getElementById("deadlinevalue").value;
659 var dtypecombo = document.getElementById("deadlinetype");
660 if (dtypecombo.options[dtypecombo.selectedIndex].value=="TIME") {
661 deadline["deadlinetime"] = { "durationms": 1000*value};
662 } else {
663 // ROUNDS
664 deadline["deadlinerounds"] = {"rounds": value, "durationms":10000};
665 }
666 var parties=[];
667 for (let i=0; i< partyprofiles.length; i++) {
668 parties.push( {"TeamInfo":{"parties": [ partyprofiles[i] ] }} );
669 }
670 return { "participants": parties, "deadline":deadline }
671 }
672
673
674
675 /**
676 @return a SHAOP request
677 */
678 function makeShaopRequest() {
679 var deadline={};
680 var value = document.getElementById("deadlinevalue").value;
681 var dtypecombo = document.getElementById("deadlinetype");
682 if (dtypecombo.options[dtypecombo.selectedIndex].value=="TIME") {
683 deadline["deadlinetime"] = { "durationms": 1000*value};
684 } else {
685 // ROUNDS
686 deadline["deadlinerounds"] = {"rounds": value, "durationms":10000};
687 }
688
689 var parties=[];
690 for (let i=0; i< partyprofiles.length; i++) {
691 var team=[ partyprofiles[i] , cobpartyprofiles[i] ];
692 parties.push( {"TeamInfo":{"parties": team }} );
693 }
694 return JSON.stringify({"SHAOPSettings": { "participants": parties, "deadline":deadline }});
695 }
696
697 /**
698 @return a random UUID.
699 Note − This should not be used in production as GUID or UUID
700 generated by Math.Random() may not be unique.
701 */
702 function createUUID() {
703 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
704 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
705 return v.toString(16);
706 });
707 }
708
709
710 /**
711 Initialize the page after html is loaded.
712 */
713 function init() {
714 selectProtocol();
715 document.getElementById("partiesserverurl").value =window.location.hostname+":8080/partiesserver-1.6.0"
716 document.getElementById("profilesserverurl").value =window.location.hostname+":8080/profilesserver-1.6.0"
717 connectDomain();
718 connectParties();
719
720 }
721 ]]>
722
723
724
725
726
727
728
729
730
731</script>
732
733</html>
Note: See TracBrowser for help on using the repository browser.