From c63fb6ed6fc59756fadbc571ee137eb5528fbec1 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 6 Oct 2020 23:56:27 +0200 Subject: [PATCH 01/24] Add image to report and to teleport player --- front/dist/resources/objects/report_flag.png | Bin 0 -> 447 bytes .../dist/resources/objects/teleportation.png | Bin 0 -> 555 bytes front/src/Phaser/Entity/Character.ts | 29 ++++++++++++++++++ front/src/Phaser/Entity/body_character.ts | 27 ++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 19 ++---------- 5 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 front/dist/resources/objects/report_flag.png create mode 100644 front/dist/resources/objects/teleportation.png diff --git a/front/dist/resources/objects/report_flag.png b/front/dist/resources/objects/report_flag.png new file mode 100644 index 0000000000000000000000000000000000000000..57b99126dd322fc798db9765fa10dfdd2195eb9d GIT binary patch literal 447 zcmV;w0YLtVP)amQ`_F&}{9*Y0jS*xDGAQ$%bc^@RcU8sb$MET8c=wBoVZrmC^I5P- z^0EG4uo8QKQ}d6%EDTN8RZm`G*pkJB*BUGU z%O7@zm*06A_I!N3`1}7)FF?wOHb9=|1DMtpc*(%bq*x}wsCMYZ|64n;dg1L)ZidZ| zwHU5`5N9~_Qjy`yZ#EQpJF)vLIif}@_!z}Cu)5&ezki>efBSj+$~PV`|L6x<3^o7$ z|7K8Tu*k=1!1KRv&Mf`7&Y2jmksJ$y+yFDK;29sE{FY$(#;2pl?%qnY0c?z%{6_44 zQ#53Z;{_GDc_p<(#5Bas)AU)rdYKtnQR5H|Fe)=x=14GThN@Yp*s=1WhUt&je;ICF pxVH5f!?l$U|DRiop__q$0Ra6sqZ;yv3L^jj002ovPDHLkV1hgX*C7A^ literal 0 HcmV?d00001 diff --git a/front/dist/resources/objects/teleportation.png b/front/dist/resources/objects/teleportation.png new file mode 100644 index 0000000000000000000000000000000000000000..e13826f9ae2b2c095008601ef503ee1161ce4e49 GIT binary patch literal 555 zcmV+`0@VG9P)JF|xzXlxY<5lU$r8cgr3H$_G8wvQlU zuPXF`3iYb`4ql6R(S)iG5L!Gm7(B$(cC*>@$R^DuO`7~+mibPq$GTGnViUC+Z0 z1EMJ6aDRX2)6voE#IFw$e4$4TV;(dhVY-YdgQq0}9Q1GyO93t0Is13}E~bbn&v~5M zCi6x_H--~ivYhv+wJf?qaCXuCKByDpeV#7bv^+_r;PNL>{PYy1IhU`F!Ivw)nx1PV_FY_RmC!;Bvt}z|Z4isTv6GwttLm)4*J%3aX++XdW%b34`H68HV zhw5dPZmbdd0{2HhSm7 zht2v*e!WOOSMw=K0io($;Nj8=Cg#Rc-!R7yZ>SWoSzg=TaK7B!ZS-9zSSYV<10Svd tDy5Z2_e>8>o3Y-cUMW;*<#Epd{sUbnu8Sxqu5|za002ovPDHLkV1f$k^0xp0 literal 0 HcmV?d00001 diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 6edb0e36..74a4d99d 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -46,6 +46,8 @@ export abstract class Character extends Container { public PlayerValue: string; public sprites: Map; private lastDirection: string = PlayerAnimationNames.WalkDown; + private report: Sprite; + private teleportation: Sprite; constructor(scene: Phaser.Scene, x: number, @@ -62,6 +64,15 @@ export abstract class Character extends Container { for (const texture of textures) { const sprite = new Sprite(scene, 0, 0, texture, frame); + sprite.setInteractive({useHandCursor: true}); + sprite.on('pointerover', () => { + this.report.visible = true; + this.teleportation.visible = true; + }); + sprite.on('pointerup', () => { + this.report.visible = true; + this.teleportation.visible = true; + }); this.add(sprite); this.getPlayerAnimations(texture).forEach(d => { this.scene.anims.create({ @@ -76,6 +87,24 @@ export abstract class Character extends Container { this.sprites.set(texture, sprite); } + this.report = new Sprite(scene, 20, -10, 'report_flag', 3); + this.report.setInteractive(); + this.report.visible = false; + this.report.on('pointerup', () => { + this.report.visible = false; + this.teleportation.visible = false; + }); + this.add(this.report); + + this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); + this.teleportation.setInteractive(); + this.teleportation.visible = false; + this.teleportation.on('pointerup', () => { + this.report.visible = false; + this.teleportation.visible = false; + }); + this.add(this.teleportation); + this.PlayerValue = name; this.playerName = new BitmapText(scene, x, y - 25, 'main_font', name, 8); this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999); diff --git a/front/src/Phaser/Entity/body_character.ts b/front/src/Phaser/Entity/body_character.ts index 3d9d5a5f..50f1eea6 100644 --- a/front/src/Phaser/Entity/body_character.ts +++ b/front/src/Phaser/Entity/body_character.ts @@ -1,4 +1,5 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "./Character"; export interface BodyResourceDescriptionInterface { name: string, @@ -310,3 +311,29 @@ export const loadAllLayers = (load: LoaderPlugin) => { } } } + +export const OBJECTS: Array = [ + {name:'report_flag', img:'resources/objects/report_flag.png'}, + {name:'layout_modes', img:'resources/objects/layout_modes.png'}, + {name:'teleportation', img:'resources/objects/teleportation.png'}, +]; + +export const loadObject = (load: LoaderPlugin) => { + for (let j = 0; j < OBJECTS.length; j++) { + load.spritesheet( + OBJECTS[j].name, + OBJECTS[j].img, + {frameWidth: 32, frameHeight: 32} + ) + } +} + +export const loadPlayerCharacters = (load: LoaderPlugin) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { + load.spritesheet( + playerResource.name, + playerResource.img, + {frameWidth: 32, frameHeight: 32} + ); + }); +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 608d920b..ed98d9b9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -16,7 +16,6 @@ import { ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; -import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; @@ -25,7 +24,7 @@ import {RemotePlayer} from "../Entity/RemotePlayer"; import {Queue} from 'queue-typescript'; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {loadAllLayers} from "../Entity/body_character"; +import {loadAllLayers, loadObject, loadPlayerCharacters} from "../Entity/body_character"; import {CenterListener, layoutManager, LayoutMode} from "../../WebRtc/LayoutManager"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; @@ -189,21 +188,9 @@ export class GameScene extends Phaser.Scene implements CenterListener { } //add player png - PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { - this.load.spritesheet( - playerResource.name, - playerResource.img, - {frameWidth: 32, frameHeight: 32} - ); - }); - - this.load.spritesheet( - 'layout_modes', - 'resources/objects/layout_modes.png', - {frameWidth: 32, frameHeight: 32} - ); - + loadPlayerCharacters(this.load); loadAllLayers(this.load); + loadObject(this.load); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); From aeced0c648fdcbd85949035a9e3185b38d5092c3 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Mon, 12 Oct 2020 11:22:41 +0200 Subject: [PATCH 02/24] create message to report --- back/src/Controller/IoSocketController.ts | 29 +++++++++++++++++++---- front/src/Connexion/RoomConnection.ts | 15 ++++++++++-- front/src/Phaser/Entity/Character.ts | 24 +++---------------- front/src/Phaser/Entity/RemotePlayer.ts | 21 ++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 9 +++++++ messages/messages.proto | 6 +++++ 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index e9f97ea6..b8d0d69a 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -42,7 +42,7 @@ import { SilentMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage + WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; @@ -50,6 +50,7 @@ import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" import {parse} from "query-string"; import {cpuTracker} from "../Services/CpuTracker"; +import axios from "axios"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -267,11 +268,13 @@ export class IoSocketController { } else if (message.hasItemeventmessage()) { this.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - this.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage) + this.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - this.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage) + this.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); } else if (message.hasPlayglobalmessage()) { - this.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage) + this.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); + } else if (message.hasReportplayermessage()){ + this.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); } /* Ok is false if backpressure was built up, wait for drain */ @@ -547,6 +550,24 @@ export class IoSocketController { } } + private handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { + try { + let reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); + if(!reportedSocket){ + throw 'reported socket user not found'; + } + //TODO report user on admin application + axios.post('/report', { + reportedUserId: reportPlayerMessage.getReporteduserid(), + reportedUserComment: reportPlayerMessage.getReportcomment(), + reporterUserId: client.userUuid, + }); + } catch (e) { + console.error('An error occurred on "handleReportMessage"'); + console.error(e); + } + } + emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { //send only at user const client = this.sockets.get(data.getReceiverid()); diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index a27bd323..282d830e 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -21,7 +21,8 @@ import { WebRtcDisconnectMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage + WebRtcStartMessage, + ReportPlayerMessage } from "../Messages/generated/messages_pb" import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; @@ -401,7 +402,6 @@ export class RoomConnection implements RoomConnection { } callback(event); }); - } public getUserId(): number|null { @@ -478,4 +478,15 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } + + public emitReportPlayerMessage(reportedUserId: number, reportComment: string ): void { + const reportPlayerMessage = new ReportPlayerMessage(); + reportPlayerMessage.setReporteduserid(reportedUserId); + reportPlayerMessage.setReportcomment(reportComment); + + const clientToServerMessage = new ClientToServerMessage(); + clientToServerMessage.setReportplayermessage(reportPlayerMessage); + + this.socket.send(clientToServerMessage.serializeBinary().buffer); + } } diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 74a4d99d..4e899ec2 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -46,8 +46,7 @@ export abstract class Character extends Container { public PlayerValue: string; public sprites: Map; private lastDirection: string = PlayerAnimationNames.WalkDown; - private report: Sprite; - private teleportation: Sprite; + //private teleportation: Sprite; constructor(scene: Phaser.Scene, x: number, @@ -65,14 +64,6 @@ export abstract class Character extends Container { for (const texture of textures) { const sprite = new Sprite(scene, 0, 0, texture, frame); sprite.setInteractive({useHandCursor: true}); - sprite.on('pointerover', () => { - this.report.visible = true; - this.teleportation.visible = true; - }); - sprite.on('pointerup', () => { - this.report.visible = true; - this.teleportation.visible = true; - }); this.add(sprite); this.getPlayerAnimations(texture).forEach(d => { this.scene.anims.create({ @@ -87,23 +78,14 @@ export abstract class Character extends Container { this.sprites.set(texture, sprite); } - this.report = new Sprite(scene, 20, -10, 'report_flag', 3); - this.report.setInteractive(); - this.report.visible = false; - this.report.on('pointerup', () => { - this.report.visible = false; - this.teleportation.visible = false; - }); - this.add(this.report); - - this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); + /*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); this.teleportation.setInteractive(); this.teleportation.visible = false; this.teleportation.on('pointerup', () => { this.report.visible = false; this.teleportation.visible = false; }); - this.add(this.teleportation); + this.add(this.teleportation);*/ this.PlayerValue = name; this.playerName = new BitmapText(scene, x, y - 25, 'main_font', name, 8); diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index ba0a74d2..f0d0baa5 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -1,12 +1,14 @@ import {GameScene} from "../Game/GameScene"; import {PointInterface} from "../../Connexion/ConnexionModels"; import {Character} from "../Entity/Character"; +import {Sprite} from "./Sprite"; /** * Class representing the sprite of a remote player (a player that plays on another computer) */ export class RemotePlayer extends Character { userId: number; + private report: Sprite; constructor( userId: number, @@ -23,6 +25,25 @@ export class RemotePlayer extends Character { //set data this.userId = userId; + this.report = new Sprite(Scene, 20, -10, 'report_flag', 3); + this.report.setInteractive(); + this.report.visible = false; + this.report.on('pointerup', () => { + //this.scene.events.emit('reportUser', {reportedUserId: userId, reportComment: comment}); + this.scene.events.emit('reportUser', {reportedUserId: this.userId, reportComment: 'test'}); + this.report.visible = false; + }); + this.add(this.report); + + this.sprites.forEach((sprite: Sprite) => { + sprite.on('pointerover', () => { + this.report.visible = true; + }); + sprite.on('pointerup', () => { + this.report.visible = true; + }); + }) + //the current player model should be push away by other players to prevent conflict //this.setImmovable(false); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 0dc89a7b..ca936e65 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -601,6 +601,9 @@ export class GameScene extends ResizableScene implements CenterListener { this.connection.setSilent(true); } }); + + //lisen event to report user + this.onReportUser(); } private switchLayoutMode(): void { @@ -1181,4 +1184,10 @@ export class GameScene extends ResizableScene implements CenterListener { public onCenterChange(): void { this.updateCameraOffset(); } + + public onReportUser(){ + this.events.on('reportUser', (message: {reportedUserId: number, reportComment: string}) => { + this.connection.emitReportPlayerMessage(message.reportedUserId, message.reportComment); + }); + } } diff --git a/messages/messages.proto b/messages/messages.proto index 87cf2231..2d9b970a 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -54,6 +54,11 @@ message WebRtcSignalToServerMessage { string signal = 2; } +message ReportPlayerMessage { + int32 reportedUserId = 1; + string reportComment = 2; +} + message ClientToServerMessage { oneof message { JoinRoomMessage joinRoomMessage = 1; @@ -66,6 +71,7 @@ message ClientToServerMessage { WebRtcSignalToServerMessage webRtcScreenSharingSignalToServerMessage = 8; PlayGlobalMessage playGlobalMessage = 9; StopGlobalMessage stopGlobalMessage = 10; + ReportPlayerMessage reportPlayerMessage = 11; } } From 3c2f134e4ed92bc3a3c50ac783407a47fbbe97ff Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 09:37:38 +0200 Subject: [PATCH 03/24] Finish report in admin --- back/src/Controller/IoSocketController.ts | 29 +++++++++++++++++------ back/src/Enum/EnvironmentVariable.ts | 4 ++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index b8d0d69a..ddf123ce 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -2,7 +2,14 @@ import * as http from "http"; import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.." import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import Jwt, {JsonWebTokenError} from "jsonwebtoken"; -import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." +import { + SECRET_KEY, + MINIMUM_DISTANCE, + GROUP_RADIUS, + ALLOW_ARTILLERY, + ADMIN_API_URL, + ADMIN_API_TOKEN +} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import {World} from "../Model/World"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; @@ -50,7 +57,7 @@ import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" import {parse} from "query-string"; import {cpuTracker} from "../Services/CpuTracker"; -import axios from "axios"; +import Axios from "axios"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -553,14 +560,22 @@ export class IoSocketController { private handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { try { let reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); - if(!reportedSocket){ + if (!reportedSocket) { throw 'reported socket user not found'; } //TODO report user on admin application - axios.post('/report', { - reportedUserId: reportPlayerMessage.getReporteduserid(), - reportedUserComment: reportPlayerMessage.getReportcomment(), - reporterUserId: client.userUuid, + console.log('ADMIN_API_URL', ADMIN_API_URL); + console.log('ADMIN_API_TOKEN', ADMIN_API_TOKEN); + Axios.post(`${ADMIN_API_URL}/aoi/report`, { + reportedUserUuid: client.userUuid, + reportedUserComment: reportPlayerMessage.getReportcomment(), + reporterUserUuid: client.userUuid, + }, + { + headers: {"Authorization": `${ADMIN_API_TOKEN}`} + }).catch((err) => { + console.error(err); + throw err; }); } catch (e) { console.error('An error occurred on "handleReportMessage"'); diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index b69ba00c..61ab4cc9 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -3,8 +3,8 @@ const URL_ROOM_STARTED = "/Floor0/floor0.json"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || null; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || null; +const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin'; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; export { From 65406f844eeb39704cebfddaabf9394b2dfce2b8 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 11:39:07 +0200 Subject: [PATCH 04/24] Teleport notification - Create end point to permit the teleport notification. The map url will be /teleport/token_user - Create message teleport - Create receive feature in phaser to teleport TODO Teleport player on black room. --- back/src/App.ts | 3 ++ back/src/Controller/BaseController.ts | 10 +++++- back/src/Controller/IoSocketController.ts | 41 +++++++++++++---------- back/src/Controller/ReportController.ts | 38 +++++++++++++++++++++ front/src/Connexion/ConnexionModels.ts | 2 ++ front/src/Connexion/RoomConnection.ts | 11 +++++- front/src/Phaser/Game/GameScene.ts | 5 +++ messages/messages.proto | 5 +++ 8 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 back/src/Controller/ReportController.ts diff --git a/back/src/App.ts b/back/src/App.ts index 6430251a..2cd668d2 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -6,6 +6,7 @@ import {PrometheusController} from "./Controller/PrometheusController"; import {FileController} from "./Controller/FileController"; import {DebugController} from "./Controller/DebugController"; import {App as uwsApp} from "./Server/sifrr.server"; +import {ReportController} from "./Controller/ReportController"; class App { public app: uwsApp; @@ -15,6 +16,7 @@ class App { public mapController: MapController; public prometheusController: PrometheusController; private debugController: DebugController; + private reportController: ReportController; constructor() { this.app = new uwsApp(); @@ -24,6 +26,7 @@ class App { this.authenticateController = new AuthenticateController(this.app); this.fileController = new FileController(this.app); this.mapController = new MapController(this.app); + this.reportController = new ReportController(this.app, this.ioSocketController); this.prometheusController = new PrometheusController(this.app, this.ioSocketController); this.debugController = new DebugController(this.app, this.ioSocketController); } diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 93c17ab4..2757519d 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,4 +1,5 @@ -import {HttpResponse} from "uWebSockets.js"; +import {HttpRequest, HttpResponse} from "uWebSockets.js"; +import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; export class BaseController { @@ -7,4 +8,11 @@ export class BaseController { res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.writeHeader('access-control-allow-origin', '*'); } + + protected checkAdminToken(req: HttpRequest): void { + //TODO + /*if(req.getHeader('Authorization') !== ADMIN_API_TOKEN){ + throw 'Error token api'; + }*/ + } } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index ddf123ce..f277cb4b 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -49,7 +49,7 @@ import { SilentMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage + WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage, TeleportMessageMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; @@ -108,22 +108,6 @@ export class IoSocketController { return true; } - /** - * - * @param token - */ -/* searchClientByToken(token: string): ExSocketInterface | null { - const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[]; - for (let i = 0; i < clients.length; i++) { - const client = clients[i]; - if (client.token !== token) { - continue - } - return client; - } - return null; - }*/ - private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> { //console.log(socket.handshake.query.token); @@ -921,4 +905,27 @@ export class IoSocketController { public getWorlds(): Map { return this.Worlds; } + + /** + * + * @param token + */ + searchClientByUuid(uuid: string): ExSocketInterface | null { + for(let socket of this.sockets.values()){ + if(socket.userUuid === uuid){ + return socket; + } + } + return null; + } + + public teleport(userUuid: string) { + let user = this.searchClientByUuid(userUuid); + if(!user){ + throw 'User not found'; + } + const teleportMessageMessage = new TeleportMessageMessage(); + teleportMessageMessage.setMap(`/teleport/${user.userUuid}`); + user.send(teleportMessageMessage.serializeBinary().buffer, true); + } } diff --git a/back/src/Controller/ReportController.ts b/back/src/Controller/ReportController.ts new file mode 100644 index 00000000..d39e9816 --- /dev/null +++ b/back/src/Controller/ReportController.ts @@ -0,0 +1,38 @@ +import {BaseController} from "./BaseController"; +import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; +import {IoSocketController} from "./IoSocketController"; + +export class ReportController extends BaseController { + + constructor(private App: TemplatedApp, private ioSocketController: IoSocketController) { + super(); + this.teleport(); + } + + teleport(){ + this.App.options("/teleport", (res: HttpResponse, req: HttpRequest) => { + this.checkAdminToken(req); + this.addCorsHeaders(res); + res.end(); + }); + + this.App.post("/teleport", (res: HttpResponse, req: HttpRequest) => { + (async () => { + try { + this.checkAdminToken(req); + this.addCorsHeaders(res); + + res.onAborted(() => { + console.warn('Login request was aborted'); + }) + const param = await res.json(); + this.ioSocketController.teleport(param.userUuid); + res.writeStatus("200 OK").end(); + } catch (e) { + console.log("An error happened", e) + res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); + } + })(); + }); + } +} \ No newline at end of file diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 4ec76198..30d14c79 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -24,6 +24,8 @@ export enum EventMessage{ PLAY_GLOBAL_MESSAGE = "play-global-message", STOP_GLOBAL_MESSAGE = "stop-global-message", + + TELEPORT = "teleport", } export interface PointInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 282d830e..5ce724aa 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -22,7 +22,8 @@ import { WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, WebRtcStartMessage, - ReportPlayerMessage + ReportPlayerMessage, + TeleportMessageMessage } from "../Messages/generated/messages_pb" import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; @@ -129,6 +130,8 @@ export class RoomConnection implements RoomConnection { this.dispatch(EventMessage.PLAY_GLOBAL_MESSAGE, message.getPlayglobalmessage()); } else if (message.hasStopglobalmessage()) { this.dispatch(EventMessage.STOP_GLOBAL_MESSAGE, message.getStopglobalmessage()); + } else if (message.hasTeleportmessagemessage()) { + this.dispatch(EventMessage.TELEPORT, message.getTeleportmessagemessage()); } else { throw new Error('Unknown message received'); } @@ -466,6 +469,12 @@ export class RoomConnection implements RoomConnection { }); } + public receiveTeleportMessage(callback: (messageId: string) => void) { + return this.onMessage(EventMessage.TELEPORT, (message: TeleportMessageMessage) => { + callback(message.getMap()); + }); + } + public emitGlobalMessage(message: PlayGlobalMessageInterface){ console.log('emitGlobalMessage', message); const playGlobalMessage = new PlayGlobalMessage(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ca936e65..3cc9ef57 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -269,6 +269,11 @@ export class GameScene extends ResizableScene implements CenterListener { item.fire(message.event, message.state, message.parameters); })); + connection.receiveTeleportMessage((map: string) => { + //TODO + console.log('receiveTeleportMessage', map); + }) + // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection); this.GlobalMessageManager = new GlobalMessageManager(this.connection); diff --git a/messages/messages.proto b/messages/messages.proto index 2d9b970a..bfd1958c 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -172,6 +172,10 @@ message WebRtcSignalToClientMessage { string signal = 2; } +message TeleportMessageMessage{ + string map = 1; +} + message ServerToClientMessage { oneof message { BatchMessage batchMessage = 1; @@ -184,5 +188,6 @@ message ServerToClientMessage { WebRtcDisconnectMessage webRtcDisconnectMessage = 8; PlayGlobalMessage playGlobalMessage = 9; StopGlobalMessage stopGlobalMessage = 10; + TeleportMessageMessage teleportMessageMessage = 11; } } From dbaf44e8146536662c4a3e587ce418d7d8931506 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 19:56:42 +0200 Subject: [PATCH 05/24] Change report flag - Add icon on video - Permit to have a modal with comment --- back/src/Controller/IoSocketController.ts | 9 +-- front/dist/resources/logos/close.svg | 1 + front/dist/resources/logos/report.svg | 1 + front/dist/resources/objects/report_flag.png | Bin 447 -> 0 bytes front/dist/resources/style/style.css | 78 ++++++++++++++++++- front/src/Phaser/Entity/RemotePlayer.ts | 23 ------ front/src/WebRtc/MediaManager.ts | 68 ++++++++++++++-- front/src/WebRtc/SimplePeer.ts | 12 ++- front/src/index.ts | 9 ++- 9 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 front/dist/resources/logos/close.svg create mode 100644 front/dist/resources/logos/report.svg delete mode 100644 front/dist/resources/objects/report_flag.png diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index f277cb4b..2ef8b422 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -543,13 +543,11 @@ export class IoSocketController { private handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { try { - let reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); + const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); if (!reportedSocket) { throw 'reported socket user not found'; } //TODO report user on admin application - console.log('ADMIN_API_URL', ADMIN_API_URL); - console.log('ADMIN_API_TOKEN', ADMIN_API_TOKEN); Axios.post(`${ADMIN_API_URL}/aoi/report`, { reportedUserUuid: client.userUuid, reportedUserComment: reportPlayerMessage.getReportcomment(), @@ -558,7 +556,6 @@ export class IoSocketController { { headers: {"Authorization": `${ADMIN_API_TOKEN}`} }).catch((err) => { - console.error(err); throw err; }); } catch (e) { @@ -911,7 +908,7 @@ export class IoSocketController { * @param token */ searchClientByUuid(uuid: string): ExSocketInterface | null { - for(let socket of this.sockets.values()){ + for(const socket of this.sockets.values()){ if(socket.userUuid === uuid){ return socket; } @@ -920,7 +917,7 @@ export class IoSocketController { } public teleport(userUuid: string) { - let user = this.searchClientByUuid(userUuid); + const user = this.searchClientByUuid(userUuid); if(!user){ throw 'User not found'; } diff --git a/front/dist/resources/logos/close.svg b/front/dist/resources/logos/close.svg new file mode 100644 index 00000000..6acd8b49 --- /dev/null +++ b/front/dist/resources/logos/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/report.svg b/front/dist/resources/logos/report.svg new file mode 100644 index 00000000..1cb3b068 --- /dev/null +++ b/front/dist/resources/logos/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/objects/report_flag.png b/front/dist/resources/objects/report_flag.png deleted file mode 100644 index 57b99126dd322fc798db9765fa10dfdd2195eb9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 447 zcmV;w0YLtVP)amQ`_F&}{9*Y0jS*xDGAQ$%bc^@RcU8sb$MET8c=wBoVZrmC^I5P- z^0EG4uo8QKQ}d6%EDTN8RZm`G*pkJB*BUGU z%O7@zm*06A_I!N3`1}7)FF?wOHb9=|1DMtpc*(%bq*x}wsCMYZ|64n;dg1L)ZidZ| zwHU5`5N9~_Qjy`yZ#EQpJF)vLIif}@_!z}Cu)5&ezki>efBSj+$~PV`|L6x<3^o7$ z|7K8Tu*k=1!1KRv&Mf`7&Y2jmksJ$y+yFDK;29sE{FY$(#;2pl?%qnY0c?z%{6_44 zQ#53Z;{_GDc_p<(#5Bas)AU)rdYKtnQR5H|Fe)=x=14GThN@Yp*s=1WhUt&je;ICF pxVH5f!?l$U|DRiop__q$0Ra6sqZ;yv3L^jj002ovPDHLkV1hgX*C7A^ diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index e22795a9..7e8d7521 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -56,6 +56,12 @@ body .message-info.warning{ padding: 10px; z-index: 2; } + +.video-container img.report{ + right: 5px; + left: auto; +} + .video-container video{ height: 100%; } @@ -567,4 +573,74 @@ body { .main-container .audio-playing p{ color: white; margin-left: 10px; -} \ No newline at end of file +} + +/*REPORT input*/ +div.modal-report-user{ + position: absolute; + width: 800px; + height: 600px; + left: calc(50% - 400px); + top: calc(50% - 100px); + background-color: #000000ad; +} + +.modal-report-user textarea{ + position: absolute; + width: 400px; + height: 200px; + z-index: 999; + left: calc(50% - 200px); + top: calc(50% - 100px); + background-color: #00000052; + color: white; +} + +.modal-report-user img{ + position: absolute; + height: 50px; + width: 50px; + z-index: 999; + left: calc(50% - 25px); + top: calc(50% - 250px); +} + +.modal-report-user img#cancel-report-user{ + position: absolute; + z-index: 999; + right: 0; + left: auto; + top: 0; + cursor: pointer; + width: 15px; + height: 15px; +} + +.modal-report-user button{ + position: absolute; + top: calc(50% + 200px); + left: calc(50% - 50px); + width: 100px; + border: 1px solid black; + background-color: #00000000; + color: #ffda01; + border-radius: 10px; + padding: 10px 30px; + transition: all .2s ease; +} +.modal-report-user button:hover{ + cursor: pointer; + background-color: #ffda01; + color: black; + border: 1px solid black; + transform: scale(1.1); +} + +.modal-report-user p{ + font-size: 30px; + color: white; + position: absolute; + top: 90px; + width: 100%; + text-align: center; +} diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index f0d0baa5..08f657d4 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -8,7 +8,6 @@ import {Sprite} from "./Sprite"; */ export class RemotePlayer extends Character { userId: number; - private report: Sprite; constructor( userId: number, @@ -24,28 +23,6 @@ export class RemotePlayer extends Character { //set data this.userId = userId; - - this.report = new Sprite(Scene, 20, -10, 'report_flag', 3); - this.report.setInteractive(); - this.report.visible = false; - this.report.on('pointerup', () => { - //this.scene.events.emit('reportUser', {reportedUserId: userId, reportComment: comment}); - this.scene.events.emit('reportUser', {reportedUserId: this.userId, reportComment: 'test'}); - this.report.visible = false; - }); - this.add(this.report); - - this.sprites.forEach((sprite: Sprite) => { - sprite.on('pointerover', () => { - this.report.visible = true; - }); - sprite.on('pointerup', () => { - this.report.visible = true; - }); - }) - - //the current player model should be push away by other players to prevent conflict - //this.setImmovable(false); } updatePosition(position: PointInterface): void { diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 6d8e5c3d..1dd58c11 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -10,6 +10,7 @@ const videoConstraint: boolean|MediaTrackConstraints = { export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; +export type ReportCallback = (message: string) => void; // TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only) // TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!) @@ -36,7 +37,6 @@ export class MediaManager { private cinemaBtn: HTMLDivElement; private monitorBtn: HTMLDivElement; - constructor() { this.myCamVideo = this.getElementByIdOrFail('myCamVideo'); @@ -91,17 +91,14 @@ export class MediaManager { } public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { - this.updatedLocalStreamCallBacks.add(callback); } public onStartScreenSharing(callback: StartScreenSharingCallback): void { - this.startScreenSharingCallBacks.add(callback); } public onStopScreenSharing(callback: StopScreenSharingCallback): void { - this.stopScreenSharingCallBacks.add(callback); } @@ -342,8 +339,10 @@ export class MediaManager { /** * * @param userId + * @param reportCallBack + * @param userName */ - addActiveVideo(userId: string, userName: string = ""){ + addActiveVideo(userId: string, reportCallBack: ReportCallback, userName: string = ""){ this.webrtcInAudio.play(); userName = userName.toUpperCase(); @@ -355,12 +354,19 @@ export class MediaManager { ${userName} + `; layoutManager.add(DivImportance.Normal, userId, html); + const reportBtn = this.getElementByIdOrFail(`report-${userId}`); + reportBtn.addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + this.showReportModal(userId, userName, reportCallBack); + }); + this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } @@ -542,6 +548,58 @@ export class MediaManager { return elem as T; } + private showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ + //create report text area + const mainContainer = this.getElementByIdOrFail('main-container'); + + const divReport = document.createElement('div'); + divReport.classList.add('modal-report-user'); + + const inputHidden = document.createElement('input'); + inputHidden.id = 'input-report-user'; + inputHidden.type = 'hidden'; + inputHidden.value = userId; + divReport.appendChild(inputHidden); + + const titleMessage = document.createElement('p'); + titleMessage.innerText = 'Vous souhaitez report : ' + userName; + divReport.appendChild(titleMessage); + + const imgReportUser = document.createElement('img'); + imgReportUser.id = 'img-report-user'; + imgReportUser.src = 'resources/logos/report.svg'; + divReport.appendChild(imgReportUser); + + const textareaUser = document.createElement('textarea'); + textareaUser.id = 'textarea-report-user'; + textareaUser.placeholder = 'Laissez un commentaire pour confirmer le report de la personne'; + divReport.appendChild(textareaUser); + + const buttonReport = document.createElement('button'); + buttonReport.id = 'button-save-report-user'; + buttonReport.innerText = 'Report'; + buttonReport.addEventListener('click', () => { + if(!textareaUser.value){ + textareaUser.style.border = '1px solid red' + return; + } + reportCallBack(textareaUser.value); + divReport.remove(); + }); + divReport.appendChild(buttonReport); + + const buttonCancel = document.createElement('img'); + buttonCancel.id = 'cancel-report-user'; + buttonCancel.src = 'resources/logos/close.svg'; + buttonCancel.addEventListener('click', () => { + divReport.remove(); + }); + divReport.appendChild(buttonCancel); + + mainContainer.appendChild(divReport); + } + + } export const mediaManager = new MediaManager(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 890a4313..20d10f04 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -47,6 +47,7 @@ export class SimplePeer { this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this); + mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback); mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback); mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback); @@ -145,7 +146,9 @@ export class SimplePeer { } mediaManager.removeActiveVideo("" + user.userId); - mediaManager.addActiveVideo("" + user.userId, name); + mediaManager.addActiveVideo("" + user.userId, (comment: string) => { + this.reportUser(user.userId, comment); + }, name); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); // When a connection is established to a video stream, and if a screen sharing is taking place, @@ -363,6 +366,13 @@ export class SimplePeer { } } + /** + * Triggered locally when clicking on the report button + */ + public reportUser(userId: number, message: string) { + this.Connection.emitReportPlayerMessage(userId, message) + } + private sendLocalScreenSharingStreamToUser(userId: number): void { // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) if (this.PeerScreenSharingConnectionArray.has(userId)) { diff --git a/front/src/index.ts b/front/src/index.ts index 177c56c0..0e8c0180 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -31,7 +31,14 @@ const config: GameConfig = { width: width / RESOLUTION, height: height / RESOLUTION, parent: "game", - scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], + scene: [ + LoginScene, + SelectCharacterScene, + EnableCameraScene, + ReconnectingScene, + FourOFourScene, + CustomizeScene + ], zoom: RESOLUTION, physics: { default: "arcade", From 3e74e178fb8ae7d784e05f118dcdb497358180fd Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 21:22:39 +0200 Subject: [PATCH 06/24] Fix CI --- back/src/Controller/IoSocketController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 35ae2c92..9febeacb 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,5 +1,5 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import {MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." +import {MINIMUM_DISTANCE, GROUP_RADIUS, ADMIN_API_URL, ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import {GameRoom} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; From 436cde7033a50abfa42d8a3e71614a5afaf2ca92 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 21:54:08 +0200 Subject: [PATCH 07/24] Fixe style media manager and text modale --- front/dist/resources/style/style.css | 23 ++++++++++++++++------- front/src/WebRtc/MediaManager.ts | 10 ++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 7e8d7521..6ee8f090 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -581,7 +581,7 @@ div.modal-report-user{ width: 800px; height: 600px; left: calc(50% - 400px); - top: calc(50% - 100px); + top: 100px; background-color: #000000ad; } @@ -591,8 +591,8 @@ div.modal-report-user{ height: 200px; z-index: 999; left: calc(50% - 200px); - top: calc(50% - 100px); - background-color: #00000052; + top: 200px; + background-color: #000000; color: white; } @@ -602,7 +602,7 @@ div.modal-report-user{ width: 50px; z-index: 999; left: calc(50% - 25px); - top: calc(50% - 250px); + top: 10px; } .modal-report-user img#cancel-report-user{ @@ -618,7 +618,7 @@ div.modal-report-user{ .modal-report-user button{ position: absolute; - top: calc(50% + 200px); + top: 450px; left: calc(50% - 50px); width: 100px; border: 1px solid black; @@ -636,11 +636,20 @@ div.modal-report-user{ transform: scale(1.1); } -.modal-report-user p{ +.modal-report-user p#title-report-user{ font-size: 30px; color: white; position: absolute; + top: 30px; + width: 100%; + text-align: center; +} + +.modal-report-user p#body-report-user{ + font-size: 24px; + color: white; + position: absolute; top: 90px; width: 100%; - text-align: center; + text-align: left; } diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 1dd58c11..dfa6c694 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -562,9 +562,15 @@ export class MediaManager { divReport.appendChild(inputHidden); const titleMessage = document.createElement('p'); - titleMessage.innerText = 'Vous souhaitez report : ' + userName; + titleMessage.id = 'title-report-user'; + titleMessage.innerText = 'Open a report'; divReport.appendChild(titleMessage); + const bodyMessage = document.createElement('p'); + bodyMessage.id = 'body-report-user'; + bodyMessage.innerText = `You are about to open a report regarding an offensive conduct from user ${userName.toUpperCase()}. Please explain to us how you think ${userName.toUpperCase()} breached the code of conduct.`; + divReport.appendChild(bodyMessage); + const imgReportUser = document.createElement('img'); imgReportUser.id = 'img-report-user'; imgReportUser.src = 'resources/logos/report.svg'; @@ -572,7 +578,7 @@ export class MediaManager { const textareaUser = document.createElement('textarea'); textareaUser.id = 'textarea-report-user'; - textareaUser.placeholder = 'Laissez un commentaire pour confirmer le report de la personne'; + textareaUser.placeholder = 'Write ...'; divReport.appendChild(textareaUser); const buttonReport = document.createElement('button'); From f05f4a7f5b01babba1c322d86c51bb6c312e86d2 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 13 Oct 2020 22:19:32 +0200 Subject: [PATCH 08/24] Fix url --- back/src/Controller/IoSocketController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 9febeacb..84bdd6b8 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -514,7 +514,7 @@ export class IoSocketController { throw 'reported socket user not found'; } //TODO report user on admin application - Axios.post(`${ADMIN_API_URL}/aoi/report`, { + Axios.post(`${ADMIN_API_URL}/api/report`, { reportedUserUuid: client.userUuid, reportedUserComment: reportPlayerMessage.getReportcomment(), reporterUserUuid: client.userUuid, From a6a51caa93675fbb6df03bbaa16218264c6a4593 Mon Sep 17 00:00:00 2001 From: arp Date: Wed, 14 Oct 2020 16:00:25 +0200 Subject: [PATCH 09/24] now fetch the tags from the admin into the nodejs back --- back/src/Controller/IoSocketController.ts | 211 +++++++++++----------- back/src/Model/GameRoom.ts | 37 +++- back/src/Model/RoomIdentifier.ts | 49 ++--- back/src/Services/AdminApi.ts | 36 ++-- back/src/Services/ArrayHelper.ts | 3 + back/tests/ArrayHelperTest.ts | 14 ++ back/tests/RoomIdentifierTest.ts | 19 ++ back/tests/WorldTest.ts | 6 +- 8 files changed, 226 insertions(+), 149 deletions(-) create mode 100644 back/src/Services/ArrayHelper.ts create mode 100644 back/tests/ArrayHelperTest.ts create mode 100644 back/tests/RoomIdentifierTest.ts diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 6b112e9e..79e4ae59 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,6 +1,6 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import {MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {GameRoom} from "../Model/GameRoom"; +import {GameRoom, GameRoomPolicyTypes} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; @@ -41,7 +41,7 @@ import {cpuTracker} from "../Services/CpuTracker"; import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi} from "../Services/AdminApi"; -import {RoomIdentifier} from "../Model/RoomIdentifier"; +import {PositionInterface} from "../Model/PositionInterface"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -113,11 +113,9 @@ export class IoSocketController { const websocketExtensions = req.getHeader('sec-websocket-extensions'); const roomId = query.roomId; - //todo: better validation: /\/_\/.*\/.*/ or /\/@\/.*\/.*\/.*/ if (typeof roomId !== 'string') { throw new Error('Undefined room ID: '); } - const roomIdentifier = new RoomIdentifier(roomId); const token = query.token; const x = Number(query.x); @@ -143,17 +141,20 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); - console.log('uuid', userUuid); let memberTags: string[] = []; - if (roomIdentifier.anonymous === false) { - const grants = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomIdentifier); - if (!grants.granted) { + const room = await this.getOrCreateRoom(roomId); + if (!room.anonymous && room.policyType !== GameRoomPolicyTypes.ANONYMUS_POLICY) { + try { + const userData = await adminApi.fetchMemberDataByUuid(userUuid); + memberTags = userData.tags; + if (room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) { + throw new Error('No correct tags') + } + console.log('access granted for user '+userUuid+' and room '+roomId); + } catch (e) { console.log('access not granted for user '+userUuid+' and room '+roomId); throw new Error('Client cannot acces this ressource.') - } else { - memberTags = grants.memberTags; - console.log('access granted for user '+userUuid+' and room '+roomId); } } @@ -172,6 +173,7 @@ export class IoSocketController { roomId, name, characterLayers, + tags: memberTags, position: { x: x, y: y, @@ -183,8 +185,7 @@ export class IoSocketController { right, bottom, left - }, - tags: memberTags + } }, /* Spell these correctly */ websocketKey, @@ -219,9 +220,9 @@ export class IoSocketController { client.disconnecting = false; client.name = ws.name; + client.tags = ws.tags; client.characterLayers = ws.characterLayers; client.roomId = ws.roomId; - client.tags = ws.tags; this.sockets.set(client.userId, client); @@ -230,7 +231,7 @@ export class IoSocketController { console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); // Let's join the room - this.handleJoinRoom(client, client.roomId, client.position, client.viewport, client.name, client.characterLayers); + this.handleJoinRoom(client, client.position, client.viewport); }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; @@ -266,11 +267,6 @@ export class IoSocketController { Client.disconnecting = true; //leave room this.leaveRoom(Client); - - //delete all socket information - /*delete Client.roomId; - delete Client.token; - delete Client.position;*/ } catch (e) { console.error('An error occurred on "disconnect"'); console.error(e); @@ -283,21 +279,6 @@ export class IoSocketController { console.log('A user left (', this.sockets.size, ' connected users)'); } }) - - // TODO: finish this! - /*this.Io.on(SocketIoEvent.CONNECTION, (socket: Socket) => { - - - - socket.on(SocketIoEvent.WEBRTC_SIGNAL, (data: unknown) => { - this.emitVideo((socket as ExSocketInterface), data); - }); - - socket.on(SocketIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, (data: unknown) => { - this.emitScreenSharing((socket as ExSocketInterface), data); - }); - - });*/ } private emitError(Client: ExSocketInterface, message: string): void { @@ -313,10 +294,10 @@ export class IoSocketController { console.warn(message); } - private handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): void { + private handleJoinRoom(client: ExSocketInterface, position: PointInterface, viewport: ViewportInterface): void { try { //join new previous room - const gameRoom = this.joinRoom(client, roomId, position); + const gameRoom = this.joinRoom(client, position); const things = gameRoom.setViewport(client, viewport); @@ -357,7 +338,6 @@ export class IoSocketController { } roomJoinedMessage.setCurrentuserid(client.userId); - roomJoinedMessage.setTagList(client.tags); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); @@ -575,77 +555,42 @@ export class IoSocketController { } } } - - private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom { - - //join user in room - this.nbClientsPerRoomGauge.inc({ room: roomId }); - client.roomId = roomId; - client.position = position; - + + private async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room let world = this.Worlds.get(roomId) if(world === undefined){ - world = new GameRoom((user1: User, group: Group) => { - this.joinWebRtcRoom(user1, group); - }, (user1: User, group: Group) => { - this.disConnectedUser(user1, group); - }, MINIMUM_DISTANCE, GROUP_RADIUS, (thing: Movable, listener: User) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userJoinedMessage = new UserJoinedMessage(); - if (!Number.isInteger(clientUser.userId)) { - throw new Error('clientUser.userId is not an integer '+clientUser.userId); - } - userJoinedMessage.setUserid(clientUser.userId); - userJoinedMessage.setName(clientUser.name); - userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUserjoinedmessage(userJoinedMessage); - - emitInBatch(clientListener, subMessage); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - }, (thing: Movable, position, listener) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userMovedMessage = new UserMovedMessage(); - userMovedMessage.setUserid(clientUser.userId); - userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUsermovedmessage(userMovedMessage); - - clientListener.emitInBatch(subMessage); - //console.log("Sending USER_MOVED event"); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - }, (thing: Movable, listener) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - this.emitUserLeftEvent(clientListener, clientUser.userId); - } else if (thing instanceof Group) { - this.emitDeleteGroupEvent(clientListener, thing.getId()); - } else { - console.error('Unexpected type for Movable.'); - } - - }); + world = new GameRoom( + roomId, + (user: User, group: Group) => this.joinWebRtcRoom(user, group), + (user: User, group: Group) => this.disConnectedUser(user, group), + MINIMUM_DISTANCE, + GROUP_RADIUS, + (thing: Movable, listener: User) => this.onRoomEnter(thing, listener), + (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), + (thing: Movable, listener:User) => this.onClientLeave(thing, listener) + ); + if (!world.anonymous) { + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + world.tags = data.tags + world.policyType = Number(data.policy_type) + } this.Worlds.set(roomId, world); } + return Promise.resolve(world) + } + + private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { + + const roomId = client.roomId; + //join user in room + this.nbClientsPerRoomGauge.inc({ room: roomId }); + client.position = position; + + const world = this.Worlds.get(roomId) + if(world === undefined){ + throw new Error('Could not find room for ID: '+client.roomId) + } // Dispatch groups position to newly connected user world.getGroups().forEach((group: Group) => { @@ -655,6 +600,64 @@ export class IoSocketController { world.join(client, client.position); return world; } + + private onRoomEnter(thing: Movable, listener: User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userJoinedMessage = new UserJoinedMessage(); + if (!Number.isInteger(clientUser.userId)) { + throw new Error('clientUser.userId is not an integer '+clientUser.userId); + } + userJoinedMessage.setUserid(clientUser.userId); + userJoinedMessage.setName(clientUser.name); + userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUserjoinedmessage(userJoinedMessage); + + emitInBatch(clientListener, subMessage); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userMovedMessage = new UserMovedMessage(); + userMovedMessage.setUserid(clientUser.userId); + userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUsermovedmessage(userMovedMessage); + + clientListener.emitInBatch(subMessage); + //console.log("Sending USER_MOVED event"); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientLeave(thing: Movable, listener:User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + this.emitUserLeftEvent(clientListener, clientUser.userId); + } else if (thing instanceof Group) { + this.emitDeleteGroupEvent(clientListener, thing.getId()); + } else { + console.error('Unexpected type for Movable.'); + } + } private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void { const position = group.getPosition(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 1f438e61..baa54896 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -8,10 +8,18 @@ import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import {PositionNotifier} from "./PositionNotifier"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {Movable} from "_Model/Movable"; +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; +import {arrayIntersect} from "../Services/ArrayHelper"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; +export enum GameRoomPolicyTypes { + ANONYMUS_POLICY = 1, + MEMBERS_ONLY_POLICY, + USE_TAGS_POLICY, +} + export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; @@ -26,8 +34,16 @@ export class GameRoom { private itemsState: Map = new Map(); private readonly positionNotifier: PositionNotifier; + public readonly roomId: string; + public readonly anonymous: boolean; + public tags: string[]; + public policyType: GameRoomPolicyTypes; + public readonly roomSlug: string; + public readonly worldSlug: string = ''; + public readonly organizationSlug: string = ''; - constructor(connectCallback: ConnectCallback, + constructor(roomId: string, + connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, groupRadius: number, @@ -35,6 +51,21 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback) { + this.roomId = roomId; + this.anonymous = isRoomAnonymous(roomId); + this.tags = []; + this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; + + if (this.anonymous) { + this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); + } else { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + this.roomSlug = roomSlug; + this.organizationSlug = organizationSlug; + this.worldSlug = worldSlug; + } + + this.users = new Map(); this.groups = new Set(); this.connectCallback = connectCallback; @@ -248,4 +279,8 @@ export class GameRoom { } return this.positionNotifier.setViewport(user, viewport); } + + canAccess(userTags: string[]): boolean { + return arrayIntersect(userTags, this.tags); + } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index d1516264..3ac62bca 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,25 +1,30 @@ -export class RoomIdentifier { - public readonly anonymous: boolean; - public readonly id:string - public readonly organizationSlug: string|undefined; - public readonly worldSlug: string|undefined; - public readonly roomSlug: string|undefined; - constructor(roomID: string) { - if (roomID.startsWith('_/')) { - this.anonymous = true; - } else if(roomID.startsWith('@/')) { - this.anonymous = false; +//helper functions to parse room IDs - const match = /@\/([^/]+)\/([^/]+)\/(.+)/.exec(roomID); - if (!match) { - throw new Error('Could not extract info from "'+roomID+'"'); - } - this.organizationSlug = match[1]; - this.worldSlug = match[2]; - this.roomSlug = match[3]; - } else { - throw new Error('Incorrect room ID: '+roomID); - } - this.id = roomID; +export const isRoomAnonymous = (roomID: string): boolean => { + if (roomID.startsWith('_/')) { + return true; + } else if(roomID.startsWith('@/')) { + return false; + } else { + throw new Error('Incorrect room ID: '+roomID); } } + +export const extractRoomSlugPublicRoomId = (roomId: string): string => { + const idParts = roomId.split('/'); + if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); + return idParts.slice(2).join('/'); +} +export interface extractDataFromPrivateRoomIdResponse { + organizationSlug: string; + worldSlug: string; + roomSlug: string; +} +export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { + const idParts = roomId.split('/'); + if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const organizationSlug = idParts[1]; + const worldSlug = idParts[2]; + const roomSlug = idParts[3]; + return {organizationSlug, worldSlug, roomSlug} +} \ No newline at end of file diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 605af738..739997fd 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,12 +1,13 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import {RoomIdentifier} from "../Model/RoomIdentifier"; export interface AdminApiData { organizationSlug: string worldSlug: string roomSlug: string mapUrlStart: string + tags: string[] + policy_type: number userUuid: string } @@ -15,6 +16,11 @@ export interface GrantedApiData { memberTags: string[] } +export interface fetchMemberDataByUuidResponse { + uuid: string; + tags: string[]; +} + class AdminApi { async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { @@ -40,6 +46,16 @@ class AdminApi { return res.data; } + async fetchMemberDataByUuid(uuid: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + const res = await Axios.get(ADMIN_API_URL+'/membership/'+uuid, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); @@ -50,24 +66,6 @@ class AdminApi { ) return res.data; } - - async memberIsGrantedAccessToRoom(memberId: string, roomIdentifier: RoomIdentifier): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - try { - const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access', - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, organizationSlug: roomIdentifier.organizationSlug, worldSlug: roomIdentifier.worldSlug, roomSlug: roomIdentifier.roomSlug} } - ) - return res.data; - } catch (e) { - console.log(e.message) - return { - granted: false, - memberTags: [] - }; - } - } } export const adminApi = new AdminApi(); diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts new file mode 100644 index 00000000..67321d1b --- /dev/null +++ b/back/src/Services/ArrayHelper.ts @@ -0,0 +1,3 @@ +export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { + return array1.filter(value => array2.includes(value)).length > 0; +} \ No newline at end of file diff --git a/back/tests/ArrayHelperTest.ts b/back/tests/ArrayHelperTest.ts new file mode 100644 index 00000000..51796682 --- /dev/null +++ b/back/tests/ArrayHelperTest.ts @@ -0,0 +1,14 @@ +import {arrayIntersect} from "../src/Services/ArrayHelper"; + + +describe("RoomIdentifier", () => { + it("should return true on intersect", () => { + expect(arrayIntersect(['admin', 'user'], ['admin', 'superAdmin'])).toBe(true); + }); + it("should be reflexive", () => { + expect(arrayIntersect(['admin', 'superAdmin'], ['admin', 'user'])).toBe(true); + }); + it("should return false on non intersect", () => { + expect(arrayIntersect(['admin', 'user'], ['superAdmin'])).toBe(false); + }); +}) \ No newline at end of file diff --git a/back/tests/RoomIdentifierTest.ts b/back/tests/RoomIdentifierTest.ts new file mode 100644 index 00000000..c3817ff7 --- /dev/null +++ b/back/tests/RoomIdentifierTest.ts @@ -0,0 +1,19 @@ +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier"; + +describe("RoomIdentifier", () => { + it("should flag public id as anonymous", () => { + expect(isRoomAnonymous('_/global/test')).toBe(true); + }); + it("should flag public id as not anonymous", () => { + expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false); + }); + it("should extract roomSlug from public ID", () => { + expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json'); + }); + it("should extract correct from private ID", () => { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor'); + expect(organizationSlug).toBe('afup'); + expect(worldSlug).toBe('afup2020'); + expect(roomSlug).toBe('1floor'); + }); +}) \ No newline at end of file diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 5e06414c..a4161bf5 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -21,7 +21,7 @@ describe("World", () => { } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -48,7 +48,7 @@ describe("World", () => { } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -77,7 +77,7 @@ describe("World", () => { disconnectCallNumber++; } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); From 7b435edd713622a472c36bcf591129259e5ad004 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Thu, 15 Oct 2020 10:37:40 +0200 Subject: [PATCH 10/24] Finish report --- back/src/Controller/IoSocketController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index e9398135..d39c9eb3 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -500,9 +500,9 @@ export class IoSocketController { } //TODO report user on admin application Axios.post(`${ADMIN_API_URL}/api/report`, { - reportedUserUuid: client.userUuid, + reportedUserUuid: reportedSocket.userUuid, reportedUserComment: reportPlayerMessage.getReportcomment(), - reporterUserUuid: client.userUuid, + reporterUserUuid: client.userUuid }, { headers: {"Authorization": `${ADMIN_API_TOKEN}`} From 8df56204e34c53e2c9b0025c9be8e4ec84e2482b Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Thu, 15 Oct 2020 11:51:24 +0200 Subject: [PATCH 11/24] Add teleport event --- back/src/Controller/IoSocketController.ts | 12 ++++++++---- back/src/Controller/ReportController.ts | 5 ++++- front/src/Administration/GlobalMessageManager.ts | 6 ++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index d39c9eb3..4dfe3576 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -891,12 +891,16 @@ export class IoSocketController { } public teleport(userUuid: string) { - const user = this.searchClientByUuid(userUuid); - if(!user){ + const userSocket = this.searchClientByUuid(userUuid); + if (!userSocket) { throw 'User not found'; } const teleportMessageMessage = new TeleportMessageMessage(); - teleportMessageMessage.setMap(`/teleport/${user.userUuid}`); - user.send(teleportMessageMessage.serializeBinary().buffer, true); + teleportMessageMessage.setMap(`wait/${userSocket.userUuid}`); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setTeleportmessagemessage(teleportMessageMessage); + + userSocket.send(serverToClientMessage.serializeBinary().buffer, true); } } diff --git a/back/src/Controller/ReportController.ts b/back/src/Controller/ReportController.ts index d39e9816..5b5e342d 100644 --- a/back/src/Controller/ReportController.ts +++ b/back/src/Controller/ReportController.ts @@ -27,7 +27,10 @@ export class ReportController extends BaseController { }) const param = await res.json(); this.ioSocketController.teleport(param.userUuid); - res.writeStatus("200 OK").end(); + res.writeHeader('Content-Type', 'application/json'); + res.writeStatus("200 OK").end(JSON.stringify({ + mapUrl: `wait/${param.userUuid}` + })); } catch (e) { console.log("An error happened", e) res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); diff --git a/front/src/Administration/GlobalMessageManager.ts b/front/src/Administration/GlobalMessageManager.ts index 963db8cc..e3b2b503 100644 --- a/front/src/Administration/GlobalMessageManager.ts +++ b/front/src/Administration/GlobalMessageManager.ts @@ -20,6 +20,12 @@ export class GlobalMessageManager { this.Connection.receiveStopGlobalMessage((messageId: string) => { this.stopMessage(messageId); }); + + //receive signal to close message + this.Connection.receiveTeleportMessage((map: string) => { + console.log('map to teleport user', map); + //TODO teleport user on map + }); } private playMessage(message : PlayGlobalMessageInterface){ From 38d2cc9a8cdfeda04bc06eea85e39badf95f5ca8 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Thu, 15 Oct 2020 12:12:11 +0200 Subject: [PATCH 12/24] Remove teleport feature --- back/src/App.ts | 3 -- back/src/Controller/BaseController.ts | 7 ---- back/src/Controller/IoSocketController.ts | 17 +--------- back/src/Controller/ReportController.ts | 41 ----------------------- front/src/WebRtc/SimplePeer.ts | 2 ++ 5 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 back/src/Controller/ReportController.ts diff --git a/back/src/App.ts b/back/src/App.ts index 2cd668d2..6430251a 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -6,7 +6,6 @@ import {PrometheusController} from "./Controller/PrometheusController"; import {FileController} from "./Controller/FileController"; import {DebugController} from "./Controller/DebugController"; import {App as uwsApp} from "./Server/sifrr.server"; -import {ReportController} from "./Controller/ReportController"; class App { public app: uwsApp; @@ -16,7 +15,6 @@ class App { public mapController: MapController; public prometheusController: PrometheusController; private debugController: DebugController; - private reportController: ReportController; constructor() { this.app = new uwsApp(); @@ -26,7 +24,6 @@ class App { this.authenticateController = new AuthenticateController(this.app); this.fileController = new FileController(this.app); this.mapController = new MapController(this.app); - this.reportController = new ReportController(this.app, this.ioSocketController); this.prometheusController = new PrometheusController(this.app, this.ioSocketController); this.debugController = new DebugController(this.app, this.ioSocketController); } diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 2757519d..0b744082 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -8,11 +8,4 @@ export class BaseController { res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.writeHeader('access-control-allow-origin', '*'); } - - protected checkAdminToken(req: HttpRequest): void { - //TODO - /*if(req.getHeader('Authorization') !== ADMIN_API_TOKEN){ - throw 'Error token api'; - }*/ - } } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 4dfe3576..244262d6 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -31,8 +31,7 @@ import { WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, - ReportPlayerMessage, - TeleportMessageMessage + ReportPlayerMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; @@ -889,18 +888,4 @@ export class IoSocketController { } return null; } - - public teleport(userUuid: string) { - const userSocket = this.searchClientByUuid(userUuid); - if (!userSocket) { - throw 'User not found'; - } - const teleportMessageMessage = new TeleportMessageMessage(); - teleportMessageMessage.setMap(`wait/${userSocket.userUuid}`); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setTeleportmessagemessage(teleportMessageMessage); - - userSocket.send(serverToClientMessage.serializeBinary().buffer, true); - } } diff --git a/back/src/Controller/ReportController.ts b/back/src/Controller/ReportController.ts deleted file mode 100644 index 5b5e342d..00000000 --- a/back/src/Controller/ReportController.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {BaseController} from "./BaseController"; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {IoSocketController} from "./IoSocketController"; - -export class ReportController extends BaseController { - - constructor(private App: TemplatedApp, private ioSocketController: IoSocketController) { - super(); - this.teleport(); - } - - teleport(){ - this.App.options("/teleport", (res: HttpResponse, req: HttpRequest) => { - this.checkAdminToken(req); - this.addCorsHeaders(res); - res.end(); - }); - - this.App.post("/teleport", (res: HttpResponse, req: HttpRequest) => { - (async () => { - try { - this.checkAdminToken(req); - this.addCorsHeaders(res); - - res.onAborted(() => { - console.warn('Login request was aborted'); - }) - const param = await res.json(); - this.ioSocketController.teleport(param.userUuid); - res.writeHeader('Content-Type', 'application/json'); - res.writeStatus("200 OK").end(JSON.stringify({ - mapUrl: `wait/${param.userUuid}` - })); - } catch (e) { - console.log("An error happened", e) - res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); - } - })(); - }); - } -} \ No newline at end of file diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 20d10f04..e9f507a0 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -146,6 +146,8 @@ export class SimplePeer { } mediaManager.removeActiveVideo("" + user.userId); + + //TODO ad condition isPublic or annonyme mediaManager.addActiveVideo("" + user.userId, (comment: string) => { this.reportUser(user.userId, comment); }, name); From 62dfb68aafc802565a248f0d3a384e5cacbbe6cb Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Thu, 15 Oct 2020 12:24:16 +0200 Subject: [PATCH 13/24] Update style report modal --- front/dist/resources/style/style.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 6ee8f090..fa91d1e7 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -587,13 +587,13 @@ div.modal-report-user{ .modal-report-user textarea{ position: absolute; - width: 400px; height: 200px; z-index: 999; - left: calc(50% - 200px); top: 200px; background-color: #000000; color: white; + width: calc(100% - 60px); + margin: 30px; } .modal-report-user img{ @@ -614,6 +614,7 @@ div.modal-report-user{ cursor: pointer; width: 15px; height: 15px; + margin: 10px; } .modal-report-user button{ @@ -649,7 +650,8 @@ div.modal-report-user{ font-size: 24px; color: white; position: absolute; - top: 90px; + top: 70px; width: 100%; text-align: left; + padding: 30px; } From d098a1b8db8e61baadda3c67f505af798ac5006e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 13:52:19 +0200 Subject: [PATCH 14/24] The server now sends regular ping requests to keep connections alive --- back/src/Controller/IoSocketController.ts | 46 ++++++++++++++----- back/src/Model/Websocket/ExSocketInterface.ts | 1 + 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 79e4ae59..44addb88 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -60,6 +60,26 @@ function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchTimeout = null; }, 100); } + + // If we send a message, we don't need to keep the connection alive + resetPing(socket); +} + +/** + * Schedule a ping to keep the connection open. + * If a ping is already set, the timeout of the ping is reset. + */ +function resetPing(ws: ExSocketInterface): void { + if (ws.pingTimeout) { + clearTimeout(ws.pingTimeout); + } + ws.pingTimeout = setTimeout(() => { + if (ws.disconnecting) { + return; + } + ws.ping(); + resetPing(ws); + }, 29000); } export class IoSocketController { @@ -232,6 +252,8 @@ export class IoSocketController { // Let's join the room this.handleJoinRoom(client, client.position, client.viewport); + + resetPing(client); }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; @@ -555,19 +577,19 @@ export class IoSocketController { } } } - + private async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room let world = this.Worlds.get(roomId) if(world === undefined){ world = new GameRoom( - roomId, - (user: User, group: Group) => this.joinWebRtcRoom(user, group), - (user: User, group: Group) => this.disConnectedUser(user, group), - MINIMUM_DISTANCE, - GROUP_RADIUS, - (thing: Movable, listener: User) => this.onRoomEnter(thing, listener), - (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), + roomId, + (user: User, group: Group) => this.joinWebRtcRoom(user, group), + (user: User, group: Group) => this.disConnectedUser(user, group), + MINIMUM_DISTANCE, + GROUP_RADIUS, + (thing: Movable, listener: User) => this.onRoomEnter(thing, listener), + (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), (thing: Movable, listener:User) => this.onClientLeave(thing, listener) ); if (!world.anonymous) { @@ -586,7 +608,7 @@ export class IoSocketController { //join user in room this.nbClientsPerRoomGauge.inc({ room: roomId }); client.position = position; - + const world = this.Worlds.get(roomId) if(world === undefined){ throw new Error('Could not find room for ID: '+client.roomId) @@ -600,7 +622,7 @@ export class IoSocketController { world.join(client, client.position); return world; } - + private onRoomEnter(thing: Movable, listener: User) { const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { @@ -625,7 +647,7 @@ export class IoSocketController { console.error('Unexpected type for Movable.'); } } - + private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { @@ -646,7 +668,7 @@ export class IoSocketController { console.error('Unexpected type for Movable.'); } } - + private onClientLeave(thing: Movable, listener:User) { const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index a41d34bb..dd897e1c 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -19,6 +19,7 @@ export interface ExSocketInterface extends WebSocket, Identificable { emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; batchTimeout: NodeJS.Timeout|null; + pingTimeout: NodeJS.Timeout|null; disconnecting: boolean, tags: string[] } From 67a9bd25209b86a00c6dc87dd2e9a081180f16bd Mon Sep 17 00:00:00 2001 From: arp Date: Thu, 15 Oct 2020 12:05:40 +0200 Subject: [PATCH 15/24] improved textField component and allowed 8 caracter names --- front/src/Connexion/LocalUserStore.ts | 9 ++++++++ front/src/Phaser/Components/TextField.ts | 7 ++++-- front/src/Phaser/Components/TextInput.ts | 22 ++++++++++++------- front/src/Phaser/Entity/Character.ts | 3 ++- front/src/Phaser/Login/CustomizeScene.ts | 6 +---- front/src/Phaser/Login/EnableCameraScene.ts | 4 ---- front/src/Phaser/Login/LoginScene.ts | 15 +++++-------- .../src/Phaser/Login/SelectCharacterScene.ts | 2 -- .../src/Phaser/Reconnecting/FourOFourScene.ts | 2 -- .../Phaser/Reconnecting/ReconnectingScene.ts | 1 - 10 files changed, 36 insertions(+), 35 deletions(-) diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 0976b5c9..cfecd3d4 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -1,5 +1,6 @@ import {LocalUser} from "./LocalUser"; +//todo: add localstorage fallback class LocalUserStore { saveUser(localUser: LocalUser) { @@ -10,6 +11,14 @@ class LocalUserStore { const data = localStorage.getItem('localUser'); return data ? JSON.parse(data) : null; } + + setName(name:string): void { + window.localStorage.setItem('playerName', name); + } + + getName(): string { + return window.localStorage.getItem('playerName') ?? ''; + } } diff --git a/front/src/Phaser/Components/TextField.ts b/front/src/Phaser/Components/TextField.ts index abdc0535..4eb6f41b 100644 --- a/front/src/Phaser/Components/TextField.ts +++ b/front/src/Phaser/Components/TextField.ts @@ -1,7 +1,10 @@ export class TextField extends Phaser.GameObjects.BitmapText { - constructor(scene: Phaser.Scene, x: number, y: number, text: string | string[]) { + constructor(scene: Phaser.Scene, x: number, y: number, text: string | string[], center: boolean = true) { super(scene, x, y, 'main_font', text, 8); - this.scene.add.existing(this) + this.scene.add.existing(this); + if (center) { + this.setOrigin(0.5).setCenterAlign() + } } } diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index 47f713e0..1e01029b 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -1,13 +1,15 @@ export class TextInput extends Phaser.GameObjects.BitmapText { - private underLineLength = 10; + private minUnderLineLength = 4; private underLine: Phaser.GameObjects.Text; constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) { super(scene, x, y, 'main_font', text, 32); + this.setOrigin(0.5).setCenterAlign() this.scene.add.existing(this); - this.underLine = this.scene.add.text(x, y+1, '_______', { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) + this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) + this.underLine.setOrigin(0.5) this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => { @@ -16,23 +18,27 @@ export class TextInput extends Phaser.GameObjects.BitmapText { } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { this.addLetter(event.key); } + this.underLine.text = this.getUnderLineBody(this.text.length); onChange(this.text); }); } + + private getUnderLineBody(textLength:number): string { + if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength; + let text = '_______'; + for (let i = this.minUnderLineLength; i < textLength; i++) { + text += '__' + } + return text; + } private deleteLetter() { this.text = this.text.substr(0, this.text.length - 1); - if (this.underLine.text.length > this.underLineLength) { - this.underLine.text = this.underLine.text.substr(0, this.underLine.text.length - 1); - } } private addLetter(letter: string) { this.text += letter; - if (this.text.length > this.underLineLength) { - this.underLine.text += '_'; - } } getText(): string { diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 6edb0e36..cb526c4a 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -77,7 +77,7 @@ export abstract class Character extends Container { } this.PlayerValue = name; - this.playerName = new BitmapText(scene, x, y - 25, 'main_font', name, 8); + this.playerName = new BitmapText(scene, x, y - 25, 'main_font', name, 7); this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999); scene.add.existing(this.playerName); @@ -178,6 +178,7 @@ export abstract class Character extends Container { //this.anims.playReverse(`${this.PlayerTexture}-${PlayerAnimationNames.WalkLeft}`, true); } + //todo:remove this, use a container tech to move the bubble instead if (this.bubble) { this.bubble.moveBubble(this.x, this.y); } diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index 4ada5026..0a014dae 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -54,12 +54,8 @@ export class CustomizeScene extends ResizableScene { create() { this.textField = new TextField(this, this.game.renderer.width / 2, 30, 'Customize your own Avatar!'); - this.textField.setOrigin(0.5).setCenterAlign(); - this.textField.setVisible(true); - this.enterField = new TextField(this, this.game.renderer.width / 2, 500, 'you can start the game by pressing SPACE..'); - this.enterField.setOrigin(0.5).setCenterAlign(); - this.enterField.setVisible(true); + this.enterField = new TextField(this, this.game.renderer.width / 2, 40, 'you can start the game by pressing SPACE..'); this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, CustomizeTextures.icon); this.add.existing(this.logo); diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 1d8bf3d3..32037858 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -53,16 +53,12 @@ export class EnableCameraScene extends Phaser.Scene { create() { this.textField = new TextField(this, this.game.renderer.width / 2, 20, 'Turn on your camera and microphone'); - this.textField.setOrigin(0.5).setCenterAlign(); this.pressReturnField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 30, 'Press enter to start'); - this.pressReturnField.setOrigin(0.5).setCenterAlign(); this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, ''); - this.cameraNameField.setOrigin(0.5).setCenterAlign(); this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, ''); - this.microphoneNameField.setOrigin(0.5).setCenterAlign(); this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight); this.arrowRight.setOrigin(0.5, 0.5); diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 6e6bf291..2c5c1882 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -8,6 +8,7 @@ import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Ch import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {ResizableScene} from "./ResizableScene"; +import {localUserStore} from "../../Connexion/LocalUserStore"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -28,9 +29,7 @@ export class LoginScene extends ResizableScene { super({ key: LoginSceneName }); - if (window.localStorage) { - this.name = window.localStorage.getItem('playerName') ?? ''; - } + this.name = localUserStore.getName(); } preload() { @@ -54,22 +53,18 @@ export class LoginScene extends ResizableScene { cypressAsserter.initStarted(); this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:'); - this.textField.setOrigin(0.5).setCenterAlign() - this.nameInput = new TextInput(this, this.game.renderer.width / 2 - 64, 70, 4, this.name,(text: string) => { + this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, 8, this.name,(text: string) => { this.name = text; - if (window.localStorage) { - window.localStorage.setItem('playerName', text); - } + localUserStore.setName(text); }); this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 130, 'Press enter to start'); - this.pressReturnField.setOrigin(0.5).setCenterAlign() this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); this.add.existing(this.logo); const infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; - this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText); + this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText, false); this.input.keyboard.on('keyup-ENTER', () => { if (this.name === '') { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index e5df0a0d..fce27aa3 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -57,10 +57,8 @@ export class SelectCharacterScene extends ResizableScene { create() { this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Select your character'); - this.textField.setOrigin(0.5).setCenterAlign() this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 256, 'Press enter to start'); - this.pressReturnField.setOrigin(0.5).setCenterAlign() const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; diff --git a/front/src/Phaser/Reconnecting/FourOFourScene.ts b/front/src/Phaser/Reconnecting/FourOFourScene.ts index 3e84b7e9..4a9b1a6f 100644 --- a/front/src/Phaser/Reconnecting/FourOFourScene.ts +++ b/front/src/Phaser/Reconnecting/FourOFourScene.ts @@ -45,7 +45,6 @@ export class FourOFourScene extends Phaser.Scene { this.add.existing(this.logo); this.mapNotFoundField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "404 - File not found"); - this.mapNotFoundField.setOrigin(0.5, 0.5).setCenterAlign(); let text: string = ''; if (this.file !== undefined) { @@ -56,7 +55,6 @@ export class FourOFourScene extends Phaser.Scene { } this.couldNotFindField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, text); - this.couldNotFindField.setOrigin(0.5, 0.5).setCenterAlign(); const url = this.file ? this.file : this.url; if (url !== undefined) { diff --git a/front/src/Phaser/Reconnecting/ReconnectingScene.ts b/front/src/Phaser/Reconnecting/ReconnectingScene.ts index 07d2b858..9b56dd63 100644 --- a/front/src/Phaser/Reconnecting/ReconnectingScene.ts +++ b/front/src/Phaser/Reconnecting/ReconnectingScene.ts @@ -34,7 +34,6 @@ export class ReconnectingScene extends Phaser.Scene { this.add.existing(this.logo); this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting..."); - this.reconnectingField.setOrigin(0.5, 0.5).setCenterAlign(); const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat'); this.anims.create({ From d35b3ff5be764df6158f43ef6b968ff9693d5130 Mon Sep 17 00:00:00 2001 From: arp Date: Thu, 15 Oct 2020 14:30:43 +0200 Subject: [PATCH 16/24] extended the exprire duration for jwt token --- back/src/Services/JWTTokenManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/Services/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts index 905e0ac6..580140b0 100644 --- a/back/src/Services/JWTTokenManager.ts +++ b/back/src/Services/JWTTokenManager.ts @@ -6,7 +6,7 @@ import {TokenInterface} from "../Controller/AuthenticateController"; class JWTTokenManager { public createJWTToken(userUuid: string) { - return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); + return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token } public async getUserUuidFromToken(token: unknown): Promise { From 517b3a644bbd92e4d2f1c31b5110e6a57365a431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 14:35:09 +0200 Subject: [PATCH 17/24] Fixing URL not being rewritten when changing maps --- front/src/Phaser/Game/GameManager.ts | 2 +- front/src/Phaser/Game/GameScene.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index cca1a5b2..7622381a 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -64,7 +64,7 @@ export class GameManager { public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) { const url = await this.startRoom.getMapUrl(); console.log('Starting scene '+url); - scenePlugin.start(url, {startLayerName: 'global'}); + scenePlugin.start(url); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4e209c42..ae3924d3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -417,6 +417,13 @@ export class GameScene extends ResizableScene implements CenterListener { context.stroke(); this.circleTexture.refresh(); + // Let's alter browser history + let path = this.room.id; + if (this.startLayerName) { + path += '#'+this.startLayerName; + } + window.history.pushState({}, 'WorkAdventure', path); + // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking From 36d73333f54c706c3b32ee5b626fd2f08d52a3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 15:50:51 +0200 Subject: [PATCH 18/24] Adding exitUrl property The exitUrl property can be applied on a layer to link to any kind of room (private or public) --- front/src/Connexion/ConnectionManager.ts | 6 +- front/src/Connexion/Room.ts | 9 + front/src/Phaser/Game/GameScene.ts | 69 +++--- .../src/Phaser/Login/SelectCharacterScene.ts | 24 --- maps/Floor0/floor0.json | 198 +++++++++--------- website/dist/create-map.html | 19 +- 6 files changed, 165 insertions(+), 160 deletions(-) diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index c3b4c39b..34731647 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -29,7 +29,7 @@ class ConnectionManager { const roomSlug = data.roomSlug; urlManager.editUrlForRoom(roomSlug, organizationSlug, worldSlug); - const room = new Room(window.location.pathname); + const room = new Room(window.location.pathname + window.location.hash); return Promise.resolve(room); } else if (connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { const localUser = localUserStore.getLocalUser(); @@ -46,7 +46,7 @@ class ConnectionManager { const defaultMapUrl = window.location.host.replace('play.', 'maps.') + URL_ROOM_STARTED; roomId = urlManager.editUrlForRoom(defaultMapUrl, null, null); } else { - roomId = window.location.pathname; + roomId = window.location.pathname + window.location.hash; } const room = new Room(roomId); return Promise.resolve(room); @@ -55,7 +55,7 @@ class ConnectionManager { if (localUser) { this.localUser = localUser - const room = new Room(window.location.pathname); + const room = new Room(window.location.pathname + window.location.hash); return Promise.resolve(room); } else { //todo: find some kind of fallback? diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 308ea5a2..dd2a8577 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -6,8 +6,10 @@ export class Room { public readonly isPublic: boolean; private mapUrl: string|undefined; private instance: string|undefined; + public readonly hash: string; constructor(id: string) { + this.hash = ''; if (id.startsWith('/')) { id = id.substr(1); } @@ -19,6 +21,13 @@ export class Room { } else { throw new Error('Invalid room ID'); } + + const indexOfHash = this.id.indexOf('#'); + if (indexOfHash !== -1) { + const idWithHash = this.id; + this.id = this.id.substr(0, indexOfHash); + this.hash = idWithHash.substr(indexOfHash + 1); + } } public async getMapUrl(): Promise { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ae3924d3..a2f90b06 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -54,8 +54,7 @@ export enum Textures { } export interface GameSceneInitInterface { - initPosition: PointInterface|null, - startLayerName: string|undefined + initPosition: PointInterface|null } interface InitUserPositionEventInterface { @@ -130,7 +129,6 @@ export class GameScene extends ResizableScene implements CenterListener { } private PositionNextScene: Array> = new Array>(); - private startLayerName: string|undefined; private presentationModeSprite!: Sprite; private chatModeSprite!: Sprite; private gameMap!: GameMap; @@ -315,8 +313,6 @@ export class GameScene extends ResizableScene implements CenterListener { init(initData : GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; - } else if (initData.startLayerName !== undefined) { - this.startLayerName = initData.startLayerName; } } @@ -341,7 +337,10 @@ export class GameScene extends ResizableScene implements CenterListener { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); } if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) { - this.loadNextGame(layer, this.mapFile.width, this.mapFile.tilewidth, this.mapFile.tileheight); + this.loadNextGameFromExitSceneUrl(layer, this.mapFile.width); + } else if (layer.type === 'tilelayer' && this.getExitUrl(layer) !== undefined) { + console.log('Loading exitUrl ', this.getExitUrl(layer)) + this.loadNextGameFromExitUrl(layer, this.mapFile.width); } if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { depth = 10000; @@ -357,9 +356,9 @@ export class GameScene extends ResizableScene implements CenterListener { this.startY = this.initPosition.y; } else { // Now, let's find the start layer - if (this.startLayerName) { + if (this.room.hash) { for (const layer of this.mapFile.layers) { - if (this.startLayerName === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { + if (this.room.hash === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { const startPosition = this.startUser(layer); this.startX = startPosition.x; this.startY = startPosition.y; @@ -419,8 +418,8 @@ export class GameScene extends ResizableScene implements CenterListener { // Let's alter browser history let path = this.room.id; - if (this.startLayerName) { - path += '#'+this.startLayerName; + if (this.room.hash) { + path += '#'+this.room.hash; } window.history.pushState({}, 'WorkAdventure', path); @@ -656,6 +655,10 @@ export class GameScene extends ResizableScene implements CenterListener { } } + private getExitUrl(layer: ITiledMapLayer): string|undefined { + return this.getProperty(layer, "exitUrl") as string|undefined; + } + private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { return this.getProperty(layer, "exitSceneUrl") as string|undefined; } @@ -680,15 +683,7 @@ export class GameScene extends ResizableScene implements CenterListener { return obj.value; } - /** - * - * @param layer - * @param mapWidth - * @param tileWidth - * @param tileHeight - */ - //todo: push that into the gameManager - private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ + private loadNextGameFromExitSceneUrl(layer: ITiledMapLayer, mapWidth: number) { const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl === undefined) { throw new Error('Layer is not an exit scene layer.'); @@ -698,17 +693,33 @@ export class GameScene extends ResizableScene implements CenterListener { instance = this.instance; } - console.log('existSceneUrl', exitSceneUrl); - console.log('existSceneInstance', instance); - - // TODO: eventually compute a relative URL - - // TODO: handle /@/ URL CASES! + //console.log('existSceneUrl', exitSceneUrl); + //console.log('existSceneInstance', instance); const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3); const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol; - console.log("Foo", instance, absoluteExitSceneUrlWithoutProtocol); + + this.loadNextGame(layer, mapWidth, roomId); + } + + private loadNextGameFromExitUrl(layer: ITiledMapLayer, mapWidth: number) { + const exitUrl = this.getExitUrl(layer); + if (exitUrl === undefined) { + throw new Error('Layer is not an exit layer.'); + } + const fullPath = new URL(exitUrl, window.location.toString()).pathname; + + this.loadNextGame(layer, mapWidth, fullPath); + } + + /** + * + * @param layer + * @param mapWidth + */ + //todo: push that into the gameManager + private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ const room = new Room(roomId); gameManager.loadMap(room, this.scene); const exitSceneKey = roomId; @@ -723,7 +734,7 @@ export class GameScene extends ResizableScene implements CenterListener { const y : number = parseInt(((key + 1) / mapWidth).toString()); const x : number = key - (y * mapWidth); - let hash = new URL(exitSceneUrl, this.MapUrlFile).hash; + let hash = new URL(roomId, this.MapUrlFile).hash; if (hash) { hash = hash.substr(1); } @@ -960,9 +971,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.simplePeer.unregister(); this.scene.stop(); this.scene.remove(this.scene.key); - this.scene.start(nextSceneKey.key, { - startLayerName: nextSceneKey.hash - }); + this.scene.start(nextSceneKey.key); } } diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index e5df0a0d..1e2dc1c6 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -123,30 +123,6 @@ export class SelectCharacterScene extends ResizableScene { } else { this.scene.start(CustomizeSceneName); } - - // Do we have a start URL in the address bar? If so, let's redirect to this address - /*const instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - const [mapUrl, instance] = instanceAndMapUrl; - const key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key, { - startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined - } as GameSceneInitInterface); - return { - mapUrlStart: mapUrl, - startInstance: instance - }; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((startMap: StartMapInterface) => { - const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); - this.scene.start(key); - return startMap; - }).catch((err) => { - console.error(err); - throw err; - }); - }*/ } /** diff --git a/maps/Floor0/floor0.json b/maps/Floor0/floor0.json index 97838ec8..7e6f179b 100644 --- a/maps/Floor0/floor0.json +++ b/maps/Floor0/floor0.json @@ -32,7 +32,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -50,7 +50,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -68,7 +68,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -80,7 +80,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -98,7 +98,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -116,7 +116,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -134,7 +134,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -152,7 +152,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -164,7 +164,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 257, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -182,7 +182,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -191,7 +191,7 @@ "opacity":1, "properties":[ { - "name":"exitSceneUrl", + "name":"exitUrl", "type":"string", "value":"..\/Floor1\/floor1.json" }], @@ -200,7 +200,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -218,7 +218,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -236,7 +236,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 294, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 271, 294, 271, 271, 271, 271, 294, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 0, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 294, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 294, 294, 294, 294, 294, 294, 294, 294, 294], "height":34, @@ -248,7 +248,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 31, 32, 0, 0, 31, 32, 0, 0, 31, 32, 0, 0, 0, 127, 128, 0, 0, 0, 0, 25, 26, 0, 0, 0, 0, 0, 0, 201, 0, 0, 63, 0, 201, 31, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 51, 0, 0, 0, 0, 0, 52, 51, 0, 0, 0, 143, 144, 0, 0, 0, 0, 41, 42, 0, 0, 0, 0, 0, 0, 217, 63, 0, 79, 0, 217, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 52, 0, 0, 0, 0, 0, 52, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 79, 0, 0, 0, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 68, 0, 0, 0, 0, 0, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 63, 0, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 68, 68, 0, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 103, 0, 0, 0, 79, 0, 217, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 209, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 103, 0, 0, 0, 0, 0, 0, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 0, 0, 52, 52, 0, 0, 0, 0, 0, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 21, 0, 0, 68, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 131, 131, 131, 131, 131, 131, 131, 131, 131, 0, 0, 0, 0, 0, 200, 245, 246, 247, 0, 0, 0, 0, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 131, 131, 131, 131, 131, 131, 131, 131, 131, 0, 0, 0, 0, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 131, 131, 131, 131, 131, 131, 131, 131, 131, 0, 0, 0, 0, 0, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 245, 246, 247, 0, 0, 0, 0, 153, 154, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 131, 131, 131, 131, 131, 131, 131, 131, 131, 0, 73, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 153, 154, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 153, 154, 155, 0, 0, 0, 199, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 153, 154, 155, 0, 0, 0, 215, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 162, 163, 0, 0, 162, 163, 0, 0, 162, 163, 0, 0, 0, 0, 0, 0, 0, 153, 154, 155, 0, 0, 0, 180, 0, 0, 0, 0, 0, 0, 115, 51, 52, 116, 115, 51, 52, 116, 0, 0, 0, 0, 0, 0, 0, 0, 178, 179, 0, 0, 178, 179, 0, 0, 178, 179, 0, 0, 0, 0, 0, 0, 0, 169, 170, 171, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 67, 68, 0, 0, 67, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 186, 187, 0, 0, 0, 151, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 66, 0, 0, 115, 51, 52, 116, 0, 0, 0, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 67, 68, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 51, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 115, 67, 68, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 98, 97, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 52, 116, 0, 0, 51, 52, 116, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 67, 68, 0, 0, 115, 67, 68, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 52, 116, 0, 0, 51, 52, 116, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 67, 68, 0, 0, 115, 67, 68, 0, 0, 0, 0, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 0, 0, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 52, 116, 0, 0, 51, 52, 116, 0, 0, 0, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 67, 68, 0, 0, 115, 67, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 113, 0, 0, 0, 0, 0, 0, 0, 113, 0, 113, 0, 113, 0, 0, 51, 52, 0, 0, 0, 113, 0, 113, 0, 113, 0, 0, 51, 49, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 50, 0, 0, 0, 0, 0, 51, 49, 50, 49, 50, 49, 50, 0, 115, 67, 68, 116, 0, 49, 50, 49, 50, 49, 50, 0, 115, 67, 65, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 52, 0, 0, 0, 0, 115, 67, 65, 66, 65, 66, 65, 66, 0, 0, 51, 52, 0, 0, 65, 66, 65, 66, 65, 66, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 67, 68, 116, 0, 0, 0, 0, 0, 114, 0, 114, 0, 114, 0, 0, 115, 67, 68, 116, 0, 114, 0, 114, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -260,7 +260,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 116, 0, 0, 0, 115, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 116, 0, 0, 0, 115, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 82, 81, 82, 81, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -272,7 +272,7 @@ "width":46, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 243, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 244, 0, 0, 0, 0, 0, 0, 243, 244, 243, 244, 243, 244, 0, 0, 0, 0, 0, 0, 243, 244, 243, 244, 243, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -284,7 +284,7 @@ "width":46, "x":0, "y":0 - }, + }, { "draworder":"topdown", "id":3, @@ -307,7 +307,7 @@ "visible":true, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 165, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, @@ -353,7 +353,7 @@ "type":"bool", "value":true }] - }, + }, { "id":2, "properties":[ @@ -362,7 +362,7 @@ "type":"bool", "value":true }] - }, + }, { "id":3, "properties":[ @@ -371,7 +371,7 @@ "type":"bool", "value":true }] - }, + }, { "id":4, "properties":[ @@ -380,7 +380,7 @@ "type":"bool", "value":true }] - }, + }, { "id":5, "properties":[ @@ -389,7 +389,7 @@ "type":"bool", "value":true }] - }, + }, { "id":6, "properties":[ @@ -398,7 +398,7 @@ "type":"bool", "value":true }] - }, + }, { "id":7, "properties":[ @@ -407,7 +407,7 @@ "type":"bool", "value":true }] - }, + }, { "id":12, "properties":[ @@ -416,7 +416,7 @@ "type":"bool", "value":false }] - }, + }, { "id":13, "properties":[ @@ -425,7 +425,7 @@ "type":"bool", "value":false }] - }, + }, { "id":14, "properties":[ @@ -434,7 +434,7 @@ "type":"bool", "value":true }] - }, + }, { "id":15, "properties":[ @@ -443,7 +443,7 @@ "type":"bool", "value":true }] - }, + }, { "id":17, "properties":[ @@ -452,7 +452,7 @@ "type":"bool", "value":true }] - }, + }, { "id":18, "properties":[ @@ -461,7 +461,7 @@ "type":"bool", "value":true }] - }, + }, { "id":19, "properties":[ @@ -470,7 +470,7 @@ "type":"bool", "value":true }] - }, + }, { "id":20, "properties":[ @@ -479,7 +479,7 @@ "type":"bool", "value":true }] - }, + }, { "id":21, "properties":[ @@ -488,7 +488,7 @@ "type":"bool", "value":true }] - }, + }, { "id":22, "properties":[ @@ -497,7 +497,7 @@ "type":"bool", "value":true }] - }, + }, { "id":23, "properties":[ @@ -506,7 +506,7 @@ "type":"bool", "value":true }] - }, + }, { "id":28, "properties":[ @@ -515,7 +515,7 @@ "type":"bool", "value":true }] - }, + }, { "id":29, "properties":[ @@ -524,7 +524,7 @@ "type":"bool", "value":true }] - }, + }, { "id":30, "properties":[ @@ -533,7 +533,7 @@ "type":"bool", "value":true }] - }, + }, { "id":31, "properties":[ @@ -542,7 +542,7 @@ "type":"bool", "value":true }] - }, + }, { "id":33, "properties":[ @@ -551,7 +551,7 @@ "type":"bool", "value":true }] - }, + }, { "id":34, "properties":[ @@ -560,7 +560,7 @@ "type":"bool", "value":true }] - }, + }, { "id":35, "properties":[ @@ -569,7 +569,7 @@ "type":"bool", "value":true }] - }, + }, { "id":36, "properties":[ @@ -578,7 +578,7 @@ "type":"bool", "value":true }] - }, + }, { "id":37, "properties":[ @@ -587,7 +587,7 @@ "type":"bool", "value":true }] - }, + }, { "id":38, "properties":[ @@ -596,7 +596,7 @@ "type":"bool", "value":true }] - }, + }, { "id":39, "properties":[ @@ -605,7 +605,7 @@ "type":"bool", "value":true }] - }, + }, { "id":44, "properties":[ @@ -614,7 +614,7 @@ "type":"bool", "value":false }] - }, + }, { "id":45, "properties":[ @@ -623,7 +623,7 @@ "type":"bool", "value":false }] - }, + }, { "id":48, "properties":[ @@ -632,7 +632,7 @@ "type":"bool", "value":true }] - }, + }, { "id":49, "properties":[ @@ -641,7 +641,7 @@ "type":"bool", "value":true }] - }, + }, { "id":50, "properties":[ @@ -650,7 +650,7 @@ "type":"bool", "value":true }] - }, + }, { "id":51, "properties":[ @@ -659,7 +659,7 @@ "type":"bool", "value":true }] - }, + }, { "id":52, "properties":[ @@ -668,7 +668,7 @@ "type":"bool", "value":true }] - }, + }, { "id":56, "properties":[ @@ -677,7 +677,7 @@ "type":"bool", "value":false }] - }, + }, { "id":57, "properties":[ @@ -686,7 +686,7 @@ "type":"bool", "value":false }] - }, + }, { "id":64, "properties":[ @@ -695,7 +695,7 @@ "type":"bool", "value":true }] - }, + }, { "id":65, "properties":[ @@ -704,7 +704,7 @@ "type":"bool", "value":true }] - }, + }, { "id":66, "properties":[ @@ -713,7 +713,7 @@ "type":"bool", "value":true }] - }, + }, { "id":67, "properties":[ @@ -722,7 +722,7 @@ "type":"bool", "value":true }] - }, + }, { "id":68, "properties":[ @@ -731,7 +731,7 @@ "type":"bool", "value":true }] - }, + }, { "id":72, "properties":[ @@ -740,7 +740,7 @@ "type":"bool", "value":true }] - }, + }, { "id":73, "properties":[ @@ -749,7 +749,7 @@ "type":"bool", "value":true }] - }, + }, { "id":84, "properties":[ @@ -758,7 +758,7 @@ "type":"bool", "value":true }] - }, + }, { "id":128, "properties":[ @@ -767,7 +767,7 @@ "type":"bool", "value":true }] - }, + }, { "id":144, "properties":[ @@ -776,7 +776,7 @@ "type":"bool", "value":true }] - }, + }, { "id":152, "properties":[ @@ -785,7 +785,7 @@ "type":"bool", "value":true }] - }, + }, { "id":153, "properties":[ @@ -794,7 +794,7 @@ "type":"bool", "value":true }] - }, + }, { "id":154, "properties":[ @@ -803,7 +803,7 @@ "type":"bool", "value":true }] - }, + }, { "id":155, "properties":[ @@ -812,7 +812,7 @@ "type":"bool", "value":true }] - }, + }, { "id":156, "properties":[ @@ -821,7 +821,7 @@ "type":"bool", "value":true }] - }, + }, { "id":157, "properties":[ @@ -830,7 +830,7 @@ "type":"bool", "value":true }] - }, + }, { "id":161, "properties":[ @@ -839,7 +839,7 @@ "type":"bool", "value":true }] - }, + }, { "id":162, "properties":[ @@ -848,7 +848,7 @@ "type":"bool", "value":true }] - }, + }, { "id":168, "properties":[ @@ -857,7 +857,7 @@ "type":"bool", "value":true }] - }, + }, { "id":169, "properties":[ @@ -866,7 +866,7 @@ "type":"bool", "value":true }] - }, + }, { "id":170, "properties":[ @@ -875,7 +875,7 @@ "type":"bool", "value":true }] - }, + }, { "id":171, "properties":[ @@ -884,7 +884,7 @@ "type":"bool", "value":true }] - }, + }, { "id":172, "properties":[ @@ -893,7 +893,7 @@ "type":"bool", "value":true }] - }, + }, { "id":173, "properties":[ @@ -902,7 +902,7 @@ "type":"bool", "value":true }] - }, + }, { "id":177, "properties":[ @@ -911,7 +911,7 @@ "type":"bool", "value":true }] - }, + }, { "id":178, "properties":[ @@ -920,7 +920,7 @@ "type":"bool", "value":true }] - }, + }, { "id":179, "properties":[ @@ -929,7 +929,7 @@ "type":"bool", "value":true }] - }, + }, { "id":184, "properties":[ @@ -938,7 +938,7 @@ "type":"bool", "value":true }] - }, + }, { "id":185, "properties":[ @@ -947,7 +947,7 @@ "type":"bool", "value":true }] - }, + }, { "id":186, "properties":[ @@ -956,7 +956,7 @@ "type":"bool", "value":true }] - }, + }, { "id":192, "properties":[ @@ -965,7 +965,7 @@ "type":"bool", "value":true }] - }, + }, { "id":195, "properties":[ @@ -974,7 +974,7 @@ "type":"bool", "value":true }] - }, + }, { "id":196, "properties":[ @@ -983,7 +983,7 @@ "type":"bool", "value":true }] - }, + }, { "id":198, "properties":[ @@ -992,7 +992,7 @@ "type":"bool", "value":true }] - }, + }, { "id":208, "properties":[ @@ -1001,7 +1001,7 @@ "type":"bool", "value":true }] - }, + }, { "id":211, "properties":[ @@ -1010,7 +1010,7 @@ "type":"bool", "value":true }] - }, + }, { "id":212, "properties":[ @@ -1019,7 +1019,7 @@ "type":"bool", "value":true }] - }, + }, { "id":214, "properties":[ @@ -1028,7 +1028,7 @@ "type":"bool", "value":true }] - }, + }, { "id":224, "properties":[ @@ -1037,7 +1037,7 @@ "type":"bool", "value":true }] - }, + }, { "id":227, "properties":[ @@ -1046,7 +1046,7 @@ "type":"bool", "value":true }] - }, + }, { "id":228, "properties":[ @@ -1057,7 +1057,7 @@ }] }], "tilewidth":32 - }, + }, { "columns":8, "firstgid":257, @@ -1085,4 +1085,4 @@ "type":"map", "version":1.2, "width":46 -} +} \ No newline at end of file diff --git a/website/dist/create-map.html b/website/dist/create-map.html index 6eb1a266..d34cea77 100644 --- a/website/dist/create-map.html +++ b/website/dist/create-map.html @@ -44,7 +44,7 @@
-
+ < class="col">

Tools you will need

In order to build your own map for WorkAdventure, you need:

    @@ -131,11 +131,22 @@

    In order to place an exit on your scene that leads to another scene:

    • You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene.
    • -
    • In layer properties, you MUST add "exitSceneUrl" property. It represents the map URL of the next scene. For example : /<map folder>/<map>.json. Be careful, if you want the next map to be correctly loaded, you must check that the map files are in folder back/src/Assets/Maps/<your map folder>. The files will be accessible by url <HOST>/map/files/<your map folder>/....
    • -
    • In layer properties, you CAN add an "exitInstance" property. If set, you will join the map of the specified instance. Otherwise, you will stay on the same instance.
    • +
    • In layer properties, you MUST add "exitUrl" property. It represents the URL of the next scene. You can put relative or absolute URLs.
    • If you want to have multiple exits, you can create many layers with name "exit". Each layer has a different key exitSceneUrl and have tiles that represent exits to another scene.
    +

    + Understanding map URLs in WorkAdventure:
    + There are 2 kinds of URLs in WorkAdventure: +

      +
    • Public URLs are in the form https://play.workadventu.re/_/[instance]/[server]/[path to map]
    • +
    • Private URLs (used in paid accounts) are in the form https://play.workadventu.re/@/[organization]/[world]/[map]
    • +
    + Assuming your JSON map is hosted at "https://example.com/my/map.json", then you can browse your map at "https://play.workadventu.re/_/global/example.com/my/map.json". + Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. + If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other. +

    +

    Note: in older releases of WorkAdventure, you could link to a map file directly using properties "exitSceneUrl" and "exitInstance". Those properties are now deprecated. Use "exitUrl" instead.

    Defining several entry points

    Often your map will have several exits, and therefore, several entry points. For instance, if there is an exit by a door that leads to the garden map, when you come back from the garden you expect to @@ -146,7 +157,7 @@

  • You must create a specific layer. When a character enters the map by this entry point, it will enter the map randomly on ANY tile of that layer.
  • In layer properties, you MUST add a boolean "startLayer" property. It should be set to true.
  • The name of the entry point is the name of the layer
  • -
  • To enter via this entry point, simply add a hash with the entry point name to the URL ("#[startLayerName]"). For instance: "https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point".
  • +
  • To enter via this entry point, simply add a hash with the entry point name to the URL ("#[startLayerName]"). For instance: "https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point".
  • You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
From a34800103616cfa414ceb343f7e84663084fc9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 16:48:42 +0200 Subject: [PATCH 19/24] Adding a new endpoint to verify the JWT token server-side before connecting --- back/src/Controller/AuthenticateController.ts | 41 +++++++++++++++++-- front/src/Connexion/ConnectionManager.ts | 26 +++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 4d950815..c298c684 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -3,6 +3,7 @@ import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; import {adminApi} from "../Services/AdminApi"; import {jwtTokenManager} from "../Services/JWTTokenManager"; +import {parse} from "query-string"; export interface TokenInterface { userUuid: string @@ -13,11 +14,12 @@ export class AuthenticateController extends BaseController { constructor(private App : TemplatedApp) { super(); this.register(); + this.verify(); this.anonymLogin(); } //Try to login with an admin token - register(){ + private register(){ this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -36,7 +38,7 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; - + try { if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); @@ -68,8 +70,41 @@ export class AuthenticateController extends BaseController { } + private verify(){ + this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => { + (async () => { + this.addCorsHeaders(res); + + const query = parse(req.getQuery()); + + res.onAborted(() => { + console.warn('verify request was aborted'); + }) + + try { + await jwtTokenManager.createJWTToken(query.token as string); + } catch (e) { + res.writeStatus("400 Bad Request").end(JSON.stringify({ + "success": false, + "message": "Invalid JWT token" + })); + } + res.writeStatus("200 OK").end(JSON.stringify({ + "success": true + })); + })(); + }); + + } + //permit to login on application. Return token to connect on Websocket IO. - anonymLogin(){ + private anonymLogin(){ this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 34731647..26432df5 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -35,11 +35,16 @@ class ConnectionManager { const localUser = localUserStore.getLocalUser(); if (localUser && localUser.jwtToken && localUser.uuid) { - this.localUser = localUser + this.localUser = localUser; + try { + await this.verifyToken(localUser.jwtToken); + } catch(e) { + // If the token is invalid, let's generate an anonymous one. + console.error('JWT token invalid. Did it expire? Login anonymously instead.'); + await this.anonymousLogin(); + } } else { - const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); - this.localUser = new LocalUser(data.userUuid, data.authToken); - localUserStore.saveUser(this.localUser); + await this.anonymousLogin(); } let roomId: string if (connexionType === GameConnexionTypes.empty) { @@ -54,7 +59,8 @@ class ConnectionManager { const localUser = localUserStore.getLocalUser(); if (localUser) { - this.localUser = localUser + this.localUser = localUser; + await this.verifyToken(localUser.jwtToken); const room = new Room(window.location.pathname + window.location.hash); return Promise.resolve(room); } else { @@ -66,6 +72,16 @@ class ConnectionManager { return Promise.reject('Invalid URL'); } + private async verifyToken(token: string): Promise { + await Axios.get(`${API_URL}/verify`, {params: {token}}); + } + + private async anonymousLogin(): Promise { + const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); + this.localUser = new LocalUser(data.userUuid, data.authToken); + localUserStore.saveUser(this.localUser); + } + public initBenchmark(): void { this.localUser = new LocalUser('', 'test'); } From 4f54c3cd867664cc7cb00a1a84edfd4322772f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 16:54:04 +0200 Subject: [PATCH 20/24] Fixing JWT verify --- back/src/Controller/AuthenticateController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index c298c684..9cfffab5 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -88,7 +88,7 @@ export class AuthenticateController extends BaseController { }) try { - await jwtTokenManager.createJWTToken(query.token as string); + await jwtTokenManager.getUserUuidFromToken(query.token as string); } catch (e) { res.writeStatus("400 Bad Request").end(JSON.stringify({ "success": false, From 2bf795d9c241e2dd97a1ffbc120564216ee10c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 17:27:40 +0200 Subject: [PATCH 21/24] Fixing HTTP status code not correctly outputed because of CORS headers --- back/src/Controller/AuthenticateController.ts | 26 ++++++++++++------- back/src/Controller/FileController.ts | 15 ++++++----- back/src/Controller/MapController.ts | 21 ++++++++++----- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 9cfffab5..16919b22 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -28,8 +28,6 @@ export class AuthenticateController extends BaseController { this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { - this.addCorsHeaders(res); - res.onAborted(() => { console.warn('Login request was aborted'); }) @@ -50,7 +48,9 @@ export class AuthenticateController extends BaseController { const mapUrlStart = data.mapUrlStart; const authToken = jwtTokenManager.createJWTToken(userUuid); - res.writeStatus("200 OK").end(JSON.stringify({ + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ authToken, userUuid, organizationSlug, @@ -61,7 +61,9 @@ export class AuthenticateController extends BaseController { } catch (e) { console.log("An error happened", e) - res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); + res.writeStatus(e.status || "500 Internal Server Error"); + this.addCorsHeaders(res); + res.end('An error happened'); } @@ -79,8 +81,6 @@ export class AuthenticateController extends BaseController { this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => { (async () => { - this.addCorsHeaders(res); - const query = parse(req.getQuery()); res.onAborted(() => { @@ -90,12 +90,17 @@ export class AuthenticateController extends BaseController { try { await jwtTokenManager.getUserUuidFromToken(query.token as string); } catch (e) { - res.writeStatus("400 Bad Request").end(JSON.stringify({ + res.writeStatus("400 Bad Request"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ "success": false, "message": "Invalid JWT token" })); + return; } - res.writeStatus("200 OK").end(JSON.stringify({ + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ "success": true })); })(); @@ -112,7 +117,6 @@ export class AuthenticateController extends BaseController { }); this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { - this.addCorsHeaders(res); res.onAborted(() => { console.warn('Login request was aborted'); @@ -120,7 +124,9 @@ export class AuthenticateController extends BaseController { const userUuid = v4(); const authToken = jwtTokenManager.createJWTToken(userUuid); - res.writeStatus("200 OK").end(JSON.stringify({ + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ authToken, userUuid, })); diff --git a/back/src/Controller/FileController.ts b/back/src/Controller/FileController.ts index 87b72b80..ae914aa8 100644 --- a/back/src/Controller/FileController.ts +++ b/back/src/Controller/FileController.ts @@ -44,8 +44,6 @@ export class FileController extends BaseController { this.App.post("/upload-audio-message", (res: HttpResponse, req: HttpRequest) => { (async () => { - this.addCorsHeaders(res); - res.onAborted(() => { console.warn('upload-audio-message request was aborted'); }) @@ -80,14 +78,18 @@ export class FileController extends BaseController { } }); - res.writeStatus("200 OK").end(JSON.stringify({ + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ id: audioMessageId, path: `/download-audio-message/${audioMessageId}` })); } catch (e) { console.log("An error happened", e) - res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); + res.writeStatus(e.status || "500 Internal Server Error"); + this.addCorsHeaders(res); + res.end('An error happened'); } })(); }); @@ -101,7 +103,6 @@ export class FileController extends BaseController { }); this.App.get("/download-audio-message/:id", (res: HttpResponse, req: HttpRequest) => { - this.addCorsHeaders(res); res.onAborted(() => { console.warn('upload-audio-message request was aborted'); @@ -111,7 +112,9 @@ export class FileController extends BaseController { const file = this.uploadedFileBuffers.get(id); if (file === undefined) { - res.writeStatus("404 Not found").end("Cannot find file"); + res.writeStatus("404 Not found"); + this.addCorsHeaders(res); + res.end("Cannot find file"); return; } diff --git a/back/src/Controller/MapController.ts b/back/src/Controller/MapController.ts index 86e3790f..027fc5b8 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -24,7 +24,6 @@ export class MapController extends BaseController{ }); this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { - this.addCorsHeaders(res); res.onAborted(() => { console.warn('/map request was aborted'); @@ -34,25 +33,35 @@ export class MapController extends BaseController{ if (typeof query.organizationSlug !== 'string') { console.error('Expected organizationSlug parameter'); - res.writeStatus("400 Bad request").end("Expected organizationSlug parameter"); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected organizationSlug parameter"); } if (typeof query.worldSlug !== 'string') { console.error('Expected worldSlug parameter'); - res.writeStatus("400 Bad request").end("Expected worldSlug parameter"); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected worldSlug parameter"); } if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { console.error('Expected only one roomSlug parameter'); - res.writeStatus("400 Bad request").end("Expected only one roomSlug parameter"); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected only one roomSlug parameter"); } (async () => { try { const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); - res.writeStatus("200 OK").end(JSON.stringify(mapDetails)); + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify(mapDetails)); } catch (e) { console.error(e); - res.writeStatus("500 Internal Server Error").end("An error occurred"); + res.writeStatus("500 Internal Server Error") + this.addCorsHeaders(res); + res.end("An error occurred"); } })(); From 0ea7240834c69596c8be4a862436399fe301180f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 17:58:27 +0200 Subject: [PATCH 22/24] Adding condition to enable reporting only on private rooms --- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/WebRtc/MediaManager.ts | 20 ++++++++++++-------- front/src/WebRtc/SimplePeer.ts | 14 ++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d993571f..d64d1757 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -592,7 +592,7 @@ export class GameScene extends ResizableScene implements CenterListener { })); // When connection is performed, let's connect SimplePeer - this.simplePeer = new SimplePeer(this.connection); + this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic); this.GlobalMessageManager = new GlobalMessageManager(this.connection); const self = this; diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index dfa6c694..eb65b555 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -342,7 +342,7 @@ export class MediaManager { * @param reportCallBack * @param userName */ - addActiveVideo(userId: string, reportCallBack: ReportCallback, userName: string = ""){ + addActiveVideo(userId: string, reportCallBack: ReportCallback|undefined, userName: string = ""){ this.webrtcInAudio.play(); userName = userName.toUpperCase(); @@ -354,18 +354,22 @@ export class MediaManager { ${userName} - - + ` + + ((reportCallBack!==undefined)?``:'') + + + `
`; layoutManager.add(DivImportance.Normal, userId, html); - const reportBtn = this.getElementByIdOrFail(`report-${userId}`); - reportBtn.addEventListener('click', (e: MouseEvent) => { - e.preventDefault(); - this.showReportModal(userId, userName, reportCallBack); - }); + if (reportCallBack) { + const reportBtn = this.getElementByIdOrFail(`report-${userId}`); + reportBtn.addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + this.showReportModal(userId, userName, reportCallBack); + }); + } this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index e9f507a0..5c77ab79 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -29,8 +29,6 @@ export interface PeerConnectionListener { * This class manages connections to all the peers in the same group as me. */ export class SimplePeer { - private Connection: RoomConnection; - private WebRtcRoomId: string; private Users: Array = new Array(); private PeerScreenSharingConnectionArray: Map = new Map(); @@ -40,9 +38,7 @@ export class SimplePeer { private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly peerConnectionListeners: Array = new Array(); - constructor(Connection: RoomConnection, WebRtcRoomId: string = "test-webrtc") { - this.Connection = Connection; - this.WebRtcRoomId = WebRtcRoomId; + constructor(private Connection: RoomConnection, private enableReporting: boolean) { // We need to go through this weird bound function pointer in order to be able to "free" this reference later. this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); @@ -148,9 +144,11 @@ export class SimplePeer { mediaManager.removeActiveVideo("" + user.userId); //TODO ad condition isPublic or annonyme - mediaManager.addActiveVideo("" + user.userId, (comment: string) => { - this.reportUser(user.userId, comment); - }, name); + const reportCallback = this.enableReporting ? (comment: string) => { + this.reportUser(user.userId, comment); + }: undefined; + + mediaManager.addActiveVideo("" + user.userId, reportCallback, name); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); // When a connection is established to a video stream, and if a screen sharing is taking place, From 267476c651a21013296c5b66216b79d88fa30725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 17:59:32 +0200 Subject: [PATCH 23/24] Removing TODO --- back/src/Controller/AuthenticateController.ts | 2 +- front/src/WebRtc/SimplePeer.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 446e5d19..321386a2 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -36,7 +36,7 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; - + try { if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 5c77ab79..718837b7 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -143,7 +143,6 @@ export class SimplePeer { mediaManager.removeActiveVideo("" + user.userId); - //TODO ad condition isPublic or annonyme const reportCallback = this.enableReporting ? (comment: string) => { this.reportUser(user.userId, comment); }: undefined; From c59a335a1a0805aebc9cc95500f579acdfdcb109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Oct 2020 18:00:34 +0200 Subject: [PATCH 24/24] Removing useless resource --- front/src/Phaser/Entity/body_character.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/front/src/Phaser/Entity/body_character.ts b/front/src/Phaser/Entity/body_character.ts index 50f1eea6..6fbeaadb 100644 --- a/front/src/Phaser/Entity/body_character.ts +++ b/front/src/Phaser/Entity/body_character.ts @@ -313,7 +313,6 @@ export const loadAllLayers = (load: LoaderPlugin) => { } export const OBJECTS: Array = [ - {name:'report_flag', img:'resources/objects/report_flag.png'}, {name:'layout_modes', img:'resources/objects/layout_modes.png'}, {name:'teleportation', img:'resources/objects/teleportation.png'}, ];