Merge
This commit is contained in:
commit
3a9196fb82
@ -4,10 +4,10 @@ import * as http from "http";
|
|||||||
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||||
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
import {World} from "../Model/World";
|
import {World} from "../Model/World";
|
||||||
import {Group} from "_Model/Group";
|
import {Group} from "../Model/Group";
|
||||||
import {UserInterface} from "_Model/UserInterface";
|
import {UserInterface} from "../Model/UserInterface";
|
||||||
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||||
@ -19,12 +19,15 @@ import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterfac
|
|||||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||||
import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage";
|
import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage";
|
||||||
|
import {uuid} from 'uuidv4';
|
||||||
|
import {isUserMovesInterface} from "../Model/Websocket/UserMovesMessage";
|
||||||
|
import {isViewport} from "../Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
enum SockerIoEvent {
|
enum SockerIoEvent {
|
||||||
CONNECTION = "connection",
|
CONNECTION = "connection",
|
||||||
DISCONNECT = "disconnect",
|
DISCONNECT = "disconnect",
|
||||||
JOIN_ROOM = "join-room", // bi-directional
|
JOIN_ROOM = "join-room", // bi-directional
|
||||||
USER_POSITION = "user-position", // bi-directional
|
USER_POSITION = "user-position", // From client to server
|
||||||
USER_MOVED = "user-moved", // From server to client
|
USER_MOVED = "user-moved", // From server to client
|
||||||
USER_LEFT = "user-left", // From server to client
|
USER_LEFT = "user-left", // From server to client
|
||||||
WEBRTC_SIGNAL = "webrtc-signal",
|
WEBRTC_SIGNAL = "webrtc-signal",
|
||||||
@ -36,6 +39,21 @@ enum SockerIoEvent {
|
|||||||
GROUP_DELETE = "group-delete",
|
GROUP_DELETE = "group-delete",
|
||||||
SET_PLAYER_DETAILS = "set-player-details",
|
SET_PLAYER_DETAILS = "set-player-details",
|
||||||
ITEM_EVENT = 'item-event',
|
ITEM_EVENT = 'item-event',
|
||||||
|
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
||||||
|
SET_VIEWPORT = "set-viewport",
|
||||||
|
BATCH = "batch",
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitInBatch(socket: ExSocketInterface, event: string | symbol, payload: unknown): void {
|
||||||
|
socket.batchedMessages.push({ event, payload});
|
||||||
|
|
||||||
|
if (socket.batchTimeout === null) {
|
||||||
|
socket.batchTimeout = setTimeout(() => {
|
||||||
|
socket.emit(SockerIoEvent.BATCH, socket.batchedMessages);
|
||||||
|
socket.batchedMessages = [];
|
||||||
|
socket.batchTimeout = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IoSocketController {
|
export class IoSocketController {
|
||||||
@ -61,10 +79,25 @@ export class IoSocketController {
|
|||||||
// Authentication with token. it will be decoded and stored in the socket.
|
// Authentication with token. it will be decoded and stored in the socket.
|
||||||
// Completely commented for now, as we do not use the "/login" route at all.
|
// Completely commented for now, as we do not use the "/login" route at all.
|
||||||
this.Io.use((socket: Socket, next) => {
|
this.Io.use((socket: Socket, next) => {
|
||||||
|
console.log(socket.handshake.query.token);
|
||||||
if (!socket.handshake.query || !socket.handshake.query.token) {
|
if (!socket.handshake.query || !socket.handshake.query.token) {
|
||||||
console.error('An authentication error happened, a user tried to connect without a token.');
|
console.error('An authentication error happened, a user tried to connect without a token.');
|
||||||
return next(new Error('Authentication error'));
|
return next(new Error('Authentication error'));
|
||||||
}
|
}
|
||||||
|
if(socket.handshake.query.token === 'test'){
|
||||||
|
if (ALLOW_ARTILLERY) {
|
||||||
|
(socket as ExSocketInterface).token = socket.handshake.query.token;
|
||||||
|
(socket as ExSocketInterface).userId = uuid();
|
||||||
|
(socket as ExSocketInterface).isArtillery = true;
|
||||||
|
console.log((socket as ExSocketInterface).userId);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.warn("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(socket as ExSocketInterface).isArtillery = false;
|
||||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||||
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
||||||
return next(new Error('Authentication error'));
|
return next(new Error('Authentication error'));
|
||||||
@ -137,6 +170,11 @@ export class IoSocketController {
|
|||||||
ioConnection() {
|
ioConnection() {
|
||||||
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
||||||
const client : ExSocketInterface = socket as ExSocketInterface;
|
const client : ExSocketInterface = socket as ExSocketInterface;
|
||||||
|
client.batchedMessages = [];
|
||||||
|
client.batchTimeout = null;
|
||||||
|
client.emitInBatch = (event: string | symbol, payload: unknown): void => {
|
||||||
|
emitInBatch(client, event, payload);
|
||||||
|
}
|
||||||
this.sockets.set(client.userId, client);
|
this.sockets.set(client.userId, client);
|
||||||
|
|
||||||
// Let's log server load when a user joins
|
// Let's log server load when a user joins
|
||||||
@ -156,6 +194,7 @@ export class IoSocketController {
|
|||||||
y: user y position on map
|
y: user y position on map
|
||||||
*/
|
*/
|
||||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||||
|
console.log(SockerIoEvent.JOIN_ROOM, message);
|
||||||
try {
|
try {
|
||||||
if (!isJoinRoomMessageInterface(message)) {
|
if (!isJoinRoomMessageInterface(message)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
||||||
@ -176,28 +215,30 @@ export class IoSocketController {
|
|||||||
//join new previous room
|
//join new previous room
|
||||||
const world = this.joinRoom(Client, roomId, message.position);
|
const world = this.joinRoom(Client, roomId, message.position);
|
||||||
|
|
||||||
//add function to refresh position user in real time.
|
const users = world.setViewport(Client, message.viewport);
|
||||||
//this.refreshUserPosition(Client);
|
const listOfUsers = users.map((user: UserInterface) => {
|
||||||
|
|
||||||
const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.characterLayers, Client.position);
|
|
||||||
|
|
||||||
socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
|
||||||
|
|
||||||
// The answer shall contain the list of all users of the room with their positions:
|
|
||||||
const listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
|
|
||||||
const player: ExSocketInterface|undefined = this.sockets.get(user.id);
|
const player: ExSocketInterface|undefined = this.sockets.get(user.id);
|
||||||
if (player === undefined) {
|
if (player === undefined) {
|
||||||
console.warn('Something went wrong. The World contains a user "'+user.id+"' but this user does not exist in the sockets list!");
|
console.warn('Something went wrong. The World contains a user "'+user.id+"' but this user does not exist in the sockets list!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position);
|
return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position);
|
||||||
}).filter((item: MessageUserPosition|null) => item !== null);
|
}, users);
|
||||||
|
|
||||||
const listOfItems: {[itemId: string]: unknown} = {};
|
const listOfItems: {[itemId: string]: unknown} = {};
|
||||||
for (const [itemId, item] of world.getItemsState().entries()) {
|
for (const [itemId, item] of world.getItemsState().entries()) {
|
||||||
listOfItems[itemId] = item;
|
listOfItems[itemId] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.warn('ANSWER PLAYER POSITIONS', listOfUsers);
|
||||||
|
if (answerFn === undefined && ALLOW_ARTILLERY === true) {
|
||||||
|
/*console.error("TYPEOF answerFn", typeof(answerFn));
|
||||||
|
console.error("answerFn", answerFn);
|
||||||
|
process.exit(1)*/
|
||||||
|
// For some reason, answerFn can be undefined if we use Artillery (?)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
answerFn({
|
answerFn({
|
||||||
users: listOfUsers,
|
users: listOfUsers,
|
||||||
items: listOfItems
|
items: listOfItems
|
||||||
@ -208,28 +249,53 @@ export class IoSocketController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
socket.on(SockerIoEvent.SET_VIEWPORT, (message: unknown): void => {
|
||||||
try {
|
try {
|
||||||
if (!isPointInterface(position)) {
|
//console.log('SET_VIEWPORT')
|
||||||
|
if (!isViewport(message)) {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_VIEWPORT message.'});
|
||||||
|
console.warn('Invalid SET_VIEWPORT message received: ', message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
Client.viewport = message;
|
||||||
|
|
||||||
|
const world = this.Worlds.get(Client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In SET_VIEWPORT, could not find world with id '", Client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.setViewport(Client, Client.viewport);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "SET_VIEWPORT" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(SockerIoEvent.USER_POSITION, (userMovesMessage: unknown): void => {
|
||||||
|
//console.log(SockerIoEvent.USER_POSITION, userMovesMessage);
|
||||||
|
try {
|
||||||
|
if (!isUserMovesInterface(userMovesMessage)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
||||||
console.warn('Invalid USER_POSITION message received: ', position);
|
console.warn('Invalid USER_POSITION message received: ', userMovesMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Client = (socket as ExSocketInterface);
|
const Client = (socket as ExSocketInterface);
|
||||||
|
|
||||||
// sending to all clients in room except sender
|
// sending to all clients in room except sender
|
||||||
Client.position = position;
|
Client.position = userMovesMessage.position;
|
||||||
|
Client.viewport = userMovesMessage.viewport;
|
||||||
|
|
||||||
// update position in the world
|
// update position in the world
|
||||||
const world = this.Worlds.get(Client.roomId);
|
const world = this.Worlds.get(Client.roomId);
|
||||||
if (!world) {
|
if (!world) {
|
||||||
console.error("Could not find world with id '", Client.roomId, "'");
|
console.error("In USER_POSITION, could not find world with id '", Client.roomId, "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
world.updatePosition(Client, position);
|
world.updatePosition(Client, Client.position);
|
||||||
|
world.setViewport(Client, Client.viewport);
|
||||||
socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('An error occurred on "user_position" event');
|
console.error('An error occurred on "user_position" event');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -275,6 +341,7 @@ export class IoSocketController {
|
|||||||
|
|
||||||
// Let's send the user id to the user
|
// Let's send the user id to the user
|
||||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||||
|
console.log(SockerIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||||
@ -283,7 +350,34 @@ export class IoSocketController {
|
|||||||
const Client = (socket as ExSocketInterface);
|
const Client = (socket as ExSocketInterface);
|
||||||
Client.name = playerDetails.name;
|
Client.name = playerDetails.name;
|
||||||
Client.characterLayers = playerDetails.characterLayers;
|
Client.characterLayers = playerDetails.characterLayers;
|
||||||
answerFn(Client.userId);
|
// Artillery fails when receiving an acknowledgement that is not a JSON object
|
||||||
|
if (!Client.isArtillery) {
|
||||||
|
answerFn(Client.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(SockerIoEvent.SET_SILENT, (silent: unknown) => {
|
||||||
|
console.log(SockerIoEvent.SET_SILENT, silent);
|
||||||
|
if (typeof silent !== "boolean") {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_SILENT message.'});
|
||||||
|
console.warn('Invalid SET_SILENT message received: ', silent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
|
||||||
|
// update position in the world
|
||||||
|
const world = this.Worlds.get(Client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In SET_SILENT, could not find world with id '", Client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.setSilent(Client, silent);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "SET_SILENT"');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on(SockerIoEvent.ITEM_EVENT, (itemEvent: unknown) => {
|
socket.on(SockerIoEvent.ITEM_EVENT, (itemEvent: unknown) => {
|
||||||
@ -359,8 +453,6 @@ export class IoSocketController {
|
|||||||
// leave previous room and world
|
// leave previous room and world
|
||||||
if(Client.roomId){
|
if(Client.roomId){
|
||||||
try {
|
try {
|
||||||
Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
|
|
||||||
|
|
||||||
//user leave previous world
|
//user leave previous world
|
||||||
const world: World | undefined = this.Worlds.get(Client.roomId);
|
const world: World | undefined = this.Worlds.get(Client.roomId);
|
||||||
if (world) {
|
if (world) {
|
||||||
@ -396,6 +488,25 @@ export class IoSocketController {
|
|||||||
this.sendUpdateGroupEvent(group);
|
this.sendUpdateGroupEvent(group);
|
||||||
}, (groupUuid: string, lastUser: UserInterface) => {
|
}, (groupUuid: string, lastUser: UserInterface) => {
|
||||||
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
||||||
|
}, (user, listener) => {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
const messageUserJoined = new MessageUserJoined(clientUser.userId, clientUser.name, clientUser.characterLayers, clientUser.position);
|
||||||
|
|
||||||
|
clientListener.emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
||||||
|
//console.log("Sending JOIN_ROOM event");
|
||||||
|
}, (user, position, listener) => {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
|
||||||
|
clientListener.emitInBatch(SockerIoEvent.USER_MOVED, new MessageUserMoved(clientUser.userId, clientUser.position));
|
||||||
|
//console.log("Sending USER_MOVED event");
|
||||||
|
}, (user, listener) => {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
|
||||||
|
clientListener.emit(SockerIoEvent.USER_LEFT, clientUser.userId);
|
||||||
|
//console.log("Sending USER_LEFT event");
|
||||||
});
|
});
|
||||||
this.Worlds.set(roomId, world);
|
this.Worlds.set(roomId, world);
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
|||||||
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
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 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;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SECRET_KEY,
|
SECRET_KEY,
|
||||||
URL_ROOM_STARTED,
|
URL_ROOM_STARTED,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
GROUP_RADIUS
|
GROUP_RADIUS,
|
||||||
|
ALLOW_ARTILLERY
|
||||||
}
|
}
|
||||||
|
120
back/src/Model/PositionNotifier.ts
Normal file
120
back/src/Model/PositionNotifier.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move
|
||||||
|
* (i.e. players that are looking at the zone the player is currently in)
|
||||||
|
*
|
||||||
|
* Internally, the PositionNotifier works with Zones. A zone is a square area of a map.
|
||||||
|
* Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport)
|
||||||
|
*
|
||||||
|
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||||
|
* number of players around the current player.
|
||||||
|
*/
|
||||||
|
import {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "./Zone";
|
||||||
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
import {UserInterface} from "_Model/UserInterface";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
|
interface ZoneDescriptor {
|
||||||
|
i: number;
|
||||||
|
j: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PositionNotifier {
|
||||||
|
|
||||||
|
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||||
|
|
||||||
|
private zones: Zone[][] = [];
|
||||||
|
|
||||||
|
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
|
return {
|
||||||
|
i: Math.floor(x / this.zoneWidth),
|
||||||
|
j: Math.floor(y / this.zoneHeight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the viewport coordinates.
|
||||||
|
* Returns the list of new users to add
|
||||||
|
*/
|
||||||
|
public setViewport(user: UserInterface, viewport: ViewportInterface): UserInterface[] {
|
||||||
|
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
||||||
|
console.warn('Invalid viewport received: ', viewport);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldZones = user.listenedZones;
|
||||||
|
const newZones = new Set<Zone>();
|
||||||
|
|
||||||
|
const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top);
|
||||||
|
const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom);
|
||||||
|
|
||||||
|
for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) {
|
||||||
|
for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) {
|
||||||
|
newZones.add(this.getZone(i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedZones = [...newZones].filter(x => !oldZones.has(x));
|
||||||
|
const removedZones = [...oldZones].filter(x => !newZones.has(x));
|
||||||
|
|
||||||
|
|
||||||
|
let users: UserInterface[] = [];
|
||||||
|
for (const zone of addedZones) {
|
||||||
|
zone.startListening(user);
|
||||||
|
users = users.concat(Array.from(zone.getPlayers()))
|
||||||
|
}
|
||||||
|
for (const zone of removedZones) {
|
||||||
|
zone.stopListening(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePosition(user: UserInterface, userPosition: PointInterface): void {
|
||||||
|
// Did we change zone?
|
||||||
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y);
|
||||||
|
const newZoneDesc = this.getZoneDescriptorFromCoordinates(userPosition.x, userPosition.y);
|
||||||
|
|
||||||
|
if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) {
|
||||||
|
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j);
|
||||||
|
|
||||||
|
// Leave old zone
|
||||||
|
oldZone.leave(user, newZone);
|
||||||
|
|
||||||
|
// Enter new zone
|
||||||
|
newZone.enter(user, oldZone, userPosition);
|
||||||
|
} else {
|
||||||
|
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
zone.move(user, userPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public leave(user: UserInterface): void {
|
||||||
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y);
|
||||||
|
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
oldZone.leave(user, null);
|
||||||
|
|
||||||
|
// Also, let's stop listening on viewports
|
||||||
|
for (const zone of user.listenedZones) {
|
||||||
|
zone.stopListening(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getZone(i: number, j: number): Zone {
|
||||||
|
let zoneRow = this.zones[j];
|
||||||
|
if (zoneRow === undefined) {
|
||||||
|
zoneRow = new Array<Zone>();
|
||||||
|
this.zones[j] = zoneRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zone = this.zones[j][i];
|
||||||
|
if (zone === undefined) {
|
||||||
|
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves);
|
||||||
|
this.zones[j][i] = zone;
|
||||||
|
}
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import { Group } from "./Group";
|
import { Group } from "./Group";
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
|
||||||
export interface UserInterface {
|
export interface UserInterface {
|
||||||
id: string,
|
id: string,
|
||||||
group?: Group,
|
group?: Group,
|
||||||
position: PointInterface
|
position: PointInterface,
|
||||||
|
silent: boolean,
|
||||||
|
listenedZones: Set<Zone>
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import {Socket} from "socket.io";
|
|||||||
import {PointInterface} from "./PointInterface";
|
import {PointInterface} from "./PointInterface";
|
||||||
import {Identificable} from "./Identificable";
|
import {Identificable} from "./Identificable";
|
||||||
import {TokenInterface} from "../../Controller/AuthenticateController";
|
import {TokenInterface} from "../../Controller/AuthenticateController";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
export interface ExSocketInterface extends Socket, Identificable {
|
export interface ExSocketInterface extends Socket, Identificable {
|
||||||
token: string;
|
token: string;
|
||||||
@ -11,4 +12,12 @@ export interface ExSocketInterface extends Socket, Identificable {
|
|||||||
name: string;
|
name: string;
|
||||||
characterLayers: string[];
|
characterLayers: string[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
|
viewport: ViewportInterface;
|
||||||
|
isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack)
|
||||||
|
/**
|
||||||
|
* Pushes an event that will be sent in the next batch of events
|
||||||
|
*/
|
||||||
|
emitInBatch: (event: string | symbol, payload: unknown) => void;
|
||||||
|
batchedMessages: Array<{ event: string | symbol, payload: unknown }>;
|
||||||
|
batchTimeout: NodeJS.Timeout|null;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import {isPointInterface} from "./PointInterface";
|
import {isPointInterface} from "./PointInterface";
|
||||||
|
import {isViewport} from "./ViewportMessage";
|
||||||
|
|
||||||
export const isJoinRoomMessageInterface =
|
export const isJoinRoomMessageInterface =
|
||||||
new tg.IsInterface().withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
roomId: tg.isString,
|
roomId: tg.isString,
|
||||||
position: isPointInterface,
|
position: isPointInterface,
|
||||||
|
viewport: isViewport
|
||||||
}).get();
|
}).get();
|
||||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
||||||
|
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import {isPointInterface} from "./PointInterface";
|
||||||
|
import {isViewport} from "./ViewportMessage";
|
||||||
|
|
||||||
|
|
||||||
|
export const isUserMovesInterface =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
position: isPointInterface,
|
||||||
|
viewport: isViewport,
|
||||||
|
}).get();
|
||||||
|
export type UserMovesInterface = tg.GuardedType<typeof isUserMovesInterface>;
|
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isViewport =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
left: tg.isNumber,
|
||||||
|
top: tg.isNumber,
|
||||||
|
right: tg.isNumber,
|
||||||
|
bottom: tg.isNumber,
|
||||||
|
}).get();
|
||||||
|
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
@ -6,6 +6,9 @@ import {UserInterface} from "./UserInterface";
|
|||||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import {Identificable} from "_Model/Websocket/Identificable";
|
import {Identificable} from "_Model/Websocket/Identificable";
|
||||||
|
import {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "_Model/Zone";
|
||||||
|
import {PositionNotifier} from "./PositionNotifier";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
export type ConnectCallback = (user: string, group: Group) => void;
|
export type ConnectCallback = (user: string, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: string, group: Group) => void;
|
export type DisconnectCallback = (user: string, group: Group) => void;
|
||||||
@ -29,12 +32,17 @@ export class World {
|
|||||||
|
|
||||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||||
|
|
||||||
|
private readonly positionNotifier: PositionNotifier;
|
||||||
|
|
||||||
constructor(connectCallback: ConnectCallback,
|
constructor(connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
minDistance: number,
|
minDistance: number,
|
||||||
groupRadius: number,
|
groupRadius: number,
|
||||||
groupUpdatedCallback: GroupUpdatedCallback,
|
groupUpdatedCallback: GroupUpdatedCallback,
|
||||||
groupDeletedCallback: GroupDeletedCallback)
|
groupDeletedCallback: GroupDeletedCallback,
|
||||||
|
onUserEnters: UserEntersCallback,
|
||||||
|
onUserMoves: UserMovesCallback,
|
||||||
|
onUserLeaves: UserLeavesCallback)
|
||||||
{
|
{
|
||||||
this.users = new Map<string, UserInterface>();
|
this.users = new Map<string, UserInterface>();
|
||||||
this.groups = new Set<Group>();
|
this.groups = new Set<Group>();
|
||||||
@ -44,6 +52,8 @@ export class World {
|
|||||||
this.groupRadius = groupRadius;
|
this.groupRadius = groupRadius;
|
||||||
this.groupUpdatedCallback = groupUpdatedCallback;
|
this.groupUpdatedCallback = groupUpdatedCallback;
|
||||||
this.groupDeletedCallback = groupDeletedCallback;
|
this.groupDeletedCallback = groupDeletedCallback;
|
||||||
|
// A zone is 10 sprites wide.
|
||||||
|
this.positionNotifier = new PositionNotifier(320, 320, onUserEnters, onUserMoves, onUserLeaves);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
public getGroups(): Group[] {
|
||||||
@ -57,7 +67,9 @@ export class World {
|
|||||||
public join(socket : Identificable, userPosition: PointInterface): void {
|
public join(socket : Identificable, userPosition: PointInterface): void {
|
||||||
this.users.set(socket.userId, {
|
this.users.set(socket.userId, {
|
||||||
id: socket.userId,
|
id: socket.userId,
|
||||||
position: userPosition
|
position: userPosition,
|
||||||
|
silent: false, // FIXME: silent should be set at the correct value when joining a room.
|
||||||
|
listenedZones: new Set<Zone>()
|
||||||
});
|
});
|
||||||
// Let's call update position to trigger the join / leave room
|
// Let's call update position to trigger the join / leave room
|
||||||
this.updatePosition(socket, userPosition);
|
this.updatePosition(socket, userPosition);
|
||||||
@ -72,6 +84,10 @@ export class World {
|
|||||||
this.leaveGroup(userObj);
|
this.leaveGroup(userObj);
|
||||||
}
|
}
|
||||||
this.users.delete(user.userId);
|
this.users.delete(user.userId);
|
||||||
|
|
||||||
|
if (userObj !== undefined) {
|
||||||
|
this.positionNotifier.leave(userObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
@ -84,8 +100,14 @@ export class World {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.positionNotifier.updatePosition(user, userPosition);
|
||||||
|
|
||||||
user.position = userPosition;
|
user.position = userPosition;
|
||||||
|
|
||||||
|
if (user.silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof user.group === 'undefined') {
|
if (typeof user.group === 'undefined') {
|
||||||
// If the user is not part of a group:
|
// If the user is not part of a group:
|
||||||
// should he join a group?
|
// should he join a group?
|
||||||
@ -120,6 +142,26 @@ export class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSilent(socket: Identificable, silent: boolean) {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
console.warn('In setSilent, could not find user with ID "'+socket.userId+'" in world.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.silent === silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.silent = silent;
|
||||||
|
if (silent && user.group !== undefined) {
|
||||||
|
this.leaveGroup(user);
|
||||||
|
}
|
||||||
|
if (!silent) {
|
||||||
|
// If we are back to life, let's trigger a position update to see if we can join some group.
|
||||||
|
this.updatePosition(socket, user.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
||||||
*
|
*
|
||||||
@ -147,6 +189,7 @@ export class World {
|
|||||||
* Looks for the closest user that is:
|
* Looks for the closest user that is:
|
||||||
* - close enough (distance <= minDistance)
|
* - close enough (distance <= minDistance)
|
||||||
* - not in a group
|
* - not in a group
|
||||||
|
* - not silent
|
||||||
* OR
|
* OR
|
||||||
* - close enough to a group (distance <= groupRadius)
|
* - close enough to a group (distance <= groupRadius)
|
||||||
*/
|
*/
|
||||||
@ -162,6 +205,9 @@ export class World {
|
|||||||
if(currentUser === user) {
|
if(currentUser === user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (currentUser.silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const distance = World.computeDistance(user, currentUser); // compute distance between peers.
|
const distance = World.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
@ -297,4 +343,12 @@ export class World {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}*/
|
}*/
|
||||||
|
setViewport(socket : Identificable, viewport: ViewportInterface): UserInterface[] {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.positionNotifier.setViewport(user, viewport);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
96
back/src/Model/Zone.ts
Normal file
96
back/src/Model/Zone.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {UserInterface} from "./UserInterface";
|
||||||
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
export type UserEntersCallback = (user: UserInterface, listener: UserInterface) => void;
|
||||||
|
export type UserMovesCallback = (user: UserInterface, position: PointInterface, listener: UserInterface) => void;
|
||||||
|
export type UserLeavesCallback = (user: UserInterface, listener: UserInterface) => void;
|
||||||
|
|
||||||
|
export class Zone {
|
||||||
|
private players: Set<UserInterface> = new Set<UserInterface>();
|
||||||
|
private listeners: Set<UserInterface> = new Set<UserInterface>();
|
||||||
|
|
||||||
|
constructor(private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user leaves the zone
|
||||||
|
*/
|
||||||
|
public leave(user: UserInterface, newZone: Zone|null) {
|
||||||
|
this.players.delete(user);
|
||||||
|
this.notifyUserLeft(user, newZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify listeners of this zone that this user left
|
||||||
|
*/
|
||||||
|
private notifyUserLeft(user: UserInterface, newZone: Zone|null) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener !== user && (newZone === null || !listener.listenedZones.has(newZone))) {
|
||||||
|
this.onUserLeaves(user, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enter(user: UserInterface, oldZone: Zone|null, position: PointInterface) {
|
||||||
|
this.players.add(user);
|
||||||
|
this.notifyUserEnter(user, oldZone, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify listeners of this zone that this user entered
|
||||||
|
*/
|
||||||
|
private notifyUserEnter(user: UserInterface, oldZone: Zone|null, position: PointInterface) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener === user) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
|
||||||
|
this.onUserEnters(user, listener);
|
||||||
|
} else {
|
||||||
|
this.onUserMoves(user, position, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public move(user: UserInterface, position: PointInterface) {
|
||||||
|
if (!this.players.has(user)) {
|
||||||
|
this.players.add(user);
|
||||||
|
const foo = this.players;
|
||||||
|
this.notifyUserEnter(user, null, position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener !== user) {
|
||||||
|
this.onUserMoves(user,position, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startListening(listener: UserInterface): void {
|
||||||
|
for (const player of this.players) {
|
||||||
|
if (player !== listener) {
|
||||||
|
this.onUserEnters(player, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.add(listener);
|
||||||
|
listener.listenedZones.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopListening(listener: UserInterface): void {
|
||||||
|
for (const player of this.players) {
|
||||||
|
if (player !== listener) {
|
||||||
|
this.onUserLeaves(player, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
listener.listenedZones.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPlayers(): Set<UserInterface> {
|
||||||
|
return this.players;
|
||||||
|
}
|
||||||
|
}
|
196
back/tests/PositionNotifierTest.ts
Normal file
196
back/tests/PositionNotifierTest.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import "jasmine";
|
||||||
|
import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World";
|
||||||
|
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||||
|
import { Group } from "../src/Model/Group";
|
||||||
|
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||||
|
import {UserInterface} from "../src/Model/UserInterface";
|
||||||
|
import {PointInterface} from "../src/Model/Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
|
||||||
|
function move(user: UserInterface, x: number, y: number, positionNotifier: PositionNotifier): void {
|
||||||
|
positionNotifier.updatePosition(user, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
});
|
||||||
|
user.position.x = x;
|
||||||
|
user.position.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PositionNotifier", () => {
|
||||||
|
it("should receive notifications when player moves", () => {
|
||||||
|
let enterTriggered = false;
|
||||||
|
let moveTriggered = false;
|
||||||
|
let leaveTriggered = false;
|
||||||
|
|
||||||
|
const positionNotifier = new PositionNotifier(300, 300, (user: UserInterface) => {
|
||||||
|
enterTriggered = true;
|
||||||
|
}, (user: UserInterface, position: PointInterface) => {
|
||||||
|
moveTriggered = true;
|
||||||
|
}, (user: UserInterface) => {
|
||||||
|
leaveTriggered = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = {
|
||||||
|
id: "1",
|
||||||
|
position: {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
const user2 = {
|
||||||
|
id: "2",
|
||||||
|
position: {
|
||||||
|
x: -9999,
|
||||||
|
y: -9999,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
move(user2, 500, 500, positionNotifier);
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
// Move inside the zone
|
||||||
|
move(user2, 501, 500, positionNotifier);
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(true);
|
||||||
|
moveTriggered = false;
|
||||||
|
|
||||||
|
// Move out of the zone in a zone that we don't track
|
||||||
|
move(user2, 901, 500, positionNotifier);
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
|
||||||
|
// Move back in
|
||||||
|
move(user2, 500, 500, positionNotifier);
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
// Move out of the zone in a zone that we do track
|
||||||
|
move(user2, 200, 500, positionNotifier);
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(true);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
moveTriggered = false;
|
||||||
|
|
||||||
|
// Leave the room
|
||||||
|
positionNotifier.leave(user2);
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should receive notifications when camera moves", () => {
|
||||||
|
let enterTriggered = false;
|
||||||
|
let moveTriggered = false;
|
||||||
|
let leaveTriggered = false;
|
||||||
|
|
||||||
|
const positionNotifier = new PositionNotifier(300, 300, (user: UserInterface) => {
|
||||||
|
enterTriggered = true;
|
||||||
|
}, (user: UserInterface, position: PointInterface) => {
|
||||||
|
moveTriggered = true;
|
||||||
|
}, (user: UserInterface) => {
|
||||||
|
leaveTriggered = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = {
|
||||||
|
id: "1",
|
||||||
|
position: {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
const user2 = {
|
||||||
|
id: "2",
|
||||||
|
position: {
|
||||||
|
x: -9999,
|
||||||
|
y: -9999,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
let newUsers = positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newUsers.length).toBe(0);
|
||||||
|
|
||||||
|
move(user2, 500, 500, positionNotifier);
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
// Move the viewport but the user stays inside.
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 201,
|
||||||
|
right: 601,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
|
||||||
|
// Move the viewport out of the user.
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 901,
|
||||||
|
right: 1001,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
|
||||||
|
// Move the viewport back on the user.
|
||||||
|
newUsers = positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
expect(newUsers.length).toBe(1);
|
||||||
|
});
|
||||||
|
})
|
@ -13,7 +13,7 @@ describe("World", () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
world.join({ userId: "foo" }, new Point(100, 100));
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ describe("World", () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
world.join({ userId: "foo" }, new Point(100, 100));
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe("World", () => {
|
|||||||
disconnectCallNumber++;
|
disconnectCallNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
world.join({ userId: "foo" }, new Point(100, 100));
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
69
benchmark/README.md
Normal file
69
benchmark/README.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Load testing
|
||||||
|
|
||||||
|
Load testing is performed with Artillery.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd benchmark
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the tests (on one core):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd benchmark
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
You can adapt the `socketio-load-test.yaml` file to increase/decrease load.
|
||||||
|
|
||||||
|
Default settings are:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
phases:
|
||||||
|
- duration: 20
|
||||||
|
arrivalRate: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
which means: during 20 seconds, 2 users will be added every second (peaking at 40 simultaneous users).
|
||||||
|
|
||||||
|
Important: don't go above 40 simultaneous users for Artillery, otherwise, it is Artillery that will fail to run the tests properly.
|
||||||
|
To know, simply run "top". The "node" process for Artillery should never reach 100%.
|
||||||
|
|
||||||
|
Reports are generated in `artillery_output.html`.
|
||||||
|
|
||||||
|
# Multicore tests
|
||||||
|
|
||||||
|
You will want to test with Artillery running on multiple cores.
|
||||||
|
|
||||||
|
You can use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./artillery_multi_core.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will trigger 4 Artillery instances in parallel.
|
||||||
|
|
||||||
|
Beware, the report generated is generated for only one instance.
|
||||||
|
|
||||||
|
# How to test, what to track?
|
||||||
|
|
||||||
|
While testing, you can check:
|
||||||
|
|
||||||
|
- CPU load of WorkAdventure API node process (it should not reach 100%)
|
||||||
|
- Get metrics at the end of the run: `http://api.workadventure.localhost/metrics`
|
||||||
|
In particular, look for:
|
||||||
|
```
|
||||||
|
# HELP nodejs_eventloop_lag_max_seconds The maximum recorded event loop delay.
|
||||||
|
# TYPE nodejs_eventloop_lag_max_seconds gauge
|
||||||
|
nodejs_eventloop_lag_max_seconds 23.991418879
|
||||||
|
```
|
||||||
|
This is the maximum time it took Node to process an event (you need to restart node after each test to reset this counter)
|
||||||
|
- Generate a profiling using "node --prof" by switching the command in docker-compose.yaml:
|
||||||
|
```
|
||||||
|
#command: yarn dev
|
||||||
|
command: yarn run profile
|
||||||
|
```
|
||||||
|
Read https://nodejs.org/en/docs/guides/simple-profiling/ on how to generate a profile.
|
||||||
|
|
17
benchmark/artillery_multi_core.sh
Executable file
17
benchmark/artillery_multi_core.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
npm run start &
|
||||||
|
pid1=$!
|
||||||
|
npm run start:nooutput &
|
||||||
|
pid2=$!
|
||||||
|
npm run start:nooutput &
|
||||||
|
pid3=$!
|
||||||
|
npm run start:nooutput &
|
||||||
|
pid4=$!
|
||||||
|
|
||||||
|
wait $pid1
|
||||||
|
wait $pid2
|
||||||
|
wait $pid3
|
||||||
|
wait $pid4
|
||||||
|
|
||||||
|
|
27
benchmark/package.json
Normal file
27
benchmark/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "workadventure-artillery",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Load testing for WorkAdventure",
|
||||||
|
"scripts": {
|
||||||
|
"start": "artillery run socketio-load-test.yaml -o artillery_output.json && artillery report --output artillery_output.html artillery_output.json",
|
||||||
|
"start:nooutput": "artillery run socketio-load-test.yaml"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Grégoire Parant",
|
||||||
|
"email": "g.parant@thecodingmachine.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "David Négrier",
|
||||||
|
"email": "d.negrier@thecodingmachine.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Arthmaël Poly",
|
||||||
|
"email": "a.poly@thecodingmachine.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "SEE LICENSE IN LICENSE.txt",
|
||||||
|
"dependencies": {
|
||||||
|
"artillery": "^1.6.1"
|
||||||
|
}
|
||||||
|
}
|
54
benchmark/socketio-load-test.yaml
Normal file
54
benchmark/socketio-load-test.yaml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
config:
|
||||||
|
target: "http://api.workadventure.localhost/"
|
||||||
|
socketio:
|
||||||
|
transports: ["websocket"]
|
||||||
|
query:
|
||||||
|
token: "test"
|
||||||
|
phases:
|
||||||
|
- duration: 20
|
||||||
|
arrivalRate: 2
|
||||||
|
processor: "./socketioLoadTest.js"
|
||||||
|
scenarios:
|
||||||
|
- name: "Connects and moves player for 20 seconds"
|
||||||
|
weight: 90
|
||||||
|
engine: "socketio"
|
||||||
|
flow:
|
||||||
|
- emit:
|
||||||
|
channel: "set-player-details"
|
||||||
|
data:
|
||||||
|
name: 'TEST'
|
||||||
|
characterLayers: ['male3']
|
||||||
|
- think: 1
|
||||||
|
- emit:
|
||||||
|
channel: "join-room"
|
||||||
|
data:
|
||||||
|
roomId: 'global__api.workadventure.localhost/map/files/Floor0/floor0'
|
||||||
|
position:
|
||||||
|
x: 783
|
||||||
|
y: 170
|
||||||
|
direction: 'down'
|
||||||
|
moving: false
|
||||||
|
viewport:
|
||||||
|
left: 500
|
||||||
|
top: 0
|
||||||
|
right: 800
|
||||||
|
bottom: 200
|
||||||
|
- think: 1
|
||||||
|
- loop:
|
||||||
|
- function: "setYRandom"
|
||||||
|
- emit:
|
||||||
|
channel: "user-position"
|
||||||
|
data:
|
||||||
|
position:
|
||||||
|
x: "{{ x }}"
|
||||||
|
y: "{{ y }}"
|
||||||
|
direction: 'down'
|
||||||
|
moving: false
|
||||||
|
viewport:
|
||||||
|
left: "{{ left }}"
|
||||||
|
top: "{{ top }}"
|
||||||
|
right: "{{ right }}"
|
||||||
|
bottom: "{{ bottom }}"
|
||||||
|
- think: 0.2
|
||||||
|
count: 100
|
||||||
|
- think: 10
|
20
benchmark/socketioLoadTest.js
Normal file
20
benchmark/socketioLoadTest.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setYRandom
|
||||||
|
};
|
||||||
|
|
||||||
|
function setYRandom(context, events, done) {
|
||||||
|
if (context.angle === undefined) {
|
||||||
|
context.angle = Math.random() * Math.PI * 2;
|
||||||
|
}
|
||||||
|
context.angle += 0.05;
|
||||||
|
|
||||||
|
context.vars.x = 320 + 1472/2 * (1 + Math.sin(context.angle));
|
||||||
|
context.vars.y = 200 + 1090/2 * (1 + Math.cos(context.angle));
|
||||||
|
context.vars.left = context.vars.x - 320;
|
||||||
|
context.vars.top = context.vars.y - 200;
|
||||||
|
context.vars.right = context.vars.x + 320;
|
||||||
|
context.vars.bottom = context.vars.y + 200;
|
||||||
|
return done();
|
||||||
|
}
|
@ -25,7 +25,11 @@
|
|||||||
},
|
},
|
||||||
"ports": [80],
|
"ports": [80],
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "api."+url
|
"API_URL": "api."+url,
|
||||||
|
"JITSI_URL": "meet.jit.si",
|
||||||
|
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
||||||
|
"TURN_USER": "workadventure",
|
||||||
|
"TURN_PASSWORD": "WorkAdventure123"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"maps": {
|
"maps": {
|
||||||
|
@ -22,10 +22,14 @@ services:
|
|||||||
image: thecodingmachine/nodejs:14
|
image: thecodingmachine/nodejs:14
|
||||||
environment:
|
environment:
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
DEBUG_MODE: "$DEBUG_MODE"
|
||||||
|
JITSI_URL: $JITSI_URL
|
||||||
HOST: "0.0.0.0"
|
HOST: "0.0.0.0"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
API_URL: api.workadventure.localhost
|
API_URL: api.workadventure.localhost
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
||||||
|
TURN_USER: workadventure
|
||||||
|
TURN_PASSWORD: WorkAdventure123
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/usr/src/app
|
- ./front:/usr/src/app
|
||||||
@ -68,6 +72,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
|
ALLOW_ARTILLERY: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
31
front/dist/resources/style/style.css
vendored
31
front/dist/resources/style/style.css
vendored
@ -256,6 +256,10 @@ body {
|
|||||||
.sidebar > div {
|
.sidebar > div {
|
||||||
max-height: 21%;
|
max-height: 21%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar > div:hover {
|
||||||
|
max-height: 25%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-aspect-ratio: 1/1) {
|
@media (max-aspect-ratio: 1/1) {
|
||||||
.main-container {
|
.main-container {
|
||||||
@ -274,6 +278,10 @@ body {
|
|||||||
.sidebar > div {
|
.sidebar > div {
|
||||||
max-width: 21%;
|
max-width: 21%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar > div:hover {
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.game {
|
.game {
|
||||||
@ -291,12 +299,11 @@ body {
|
|||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
.cowebsite iframe {
|
.cowebsite > iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.game-overlay {
|
.game-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -324,9 +331,16 @@ body {
|
|||||||
.main-section > div {
|
.main-section > div {
|
||||||
margin: 2%;
|
margin: 2%;
|
||||||
flex-basis: 96%;
|
flex-basis: 96%;
|
||||||
|
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, flex-basis 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
/*flex-shrink: 2;*/
|
/*flex-shrink: 2;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-section > div:hover {
|
||||||
|
margin: 0%;
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
flex: 0 0 25%;
|
flex: 0 0 25%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -334,6 +348,12 @@ body {
|
|||||||
|
|
||||||
.sidebar > div {
|
.sidebar > div {
|
||||||
margin: 2%;
|
margin: 2%;
|
||||||
|
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar > div:hover {
|
||||||
|
margin: 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Let's make sure videos are vertically centered if they need to be cropped */
|
/* Let's make sure videos are vertically centered if they need to be cropped */
|
||||||
@ -354,11 +374,16 @@ body {
|
|||||||
padding: 1%;
|
padding: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-mode div {
|
.chat-mode > div {
|
||||||
margin: 1%;
|
margin: 1%;
|
||||||
max-height: 96%;
|
max-height: 96%;
|
||||||
|
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-mode > div:hover {
|
||||||
|
margin: 0%;
|
||||||
|
}
|
||||||
.chat-mode.one-col > div {
|
.chat-mode.one-col > div {
|
||||||
flex-basis: 98%;
|
flex-basis: 98%;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ enum EventMessage{
|
|||||||
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
||||||
WEBRTC_START = "webrtc-start",
|
WEBRTC_START = "webrtc-start",
|
||||||
JOIN_ROOM = "join-room", // bi-directional
|
JOIN_ROOM = "join-room", // bi-directional
|
||||||
USER_POSITION = "user-position", // bi-directional
|
USER_POSITION = "user-position", // From client to server
|
||||||
USER_MOVED = "user-moved", // From server to client
|
USER_MOVED = "user-moved", // From server to client
|
||||||
USER_LEFT = "user-left", // From server to client
|
USER_LEFT = "user-left", // From server to client
|
||||||
MESSAGE_ERROR = "message-error",
|
MESSAGE_ERROR = "message-error",
|
||||||
@ -25,6 +25,9 @@ enum EventMessage{
|
|||||||
ITEM_EVENT = 'item-event',
|
ITEM_EVENT = 'item-event',
|
||||||
|
|
||||||
CONNECT_ERROR = "connect_error",
|
CONNECT_ERROR = "connect_error",
|
||||||
|
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
||||||
|
SET_VIEWPORT = "set-viewport",
|
||||||
|
BATCH = "batch",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointInterface {
|
export interface PointInterface {
|
||||||
@ -95,6 +98,23 @@ export interface StartMapInterface {
|
|||||||
startInstance: string
|
startInstance: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewportInterface {
|
||||||
|
left: number,
|
||||||
|
top: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMovesInterface {
|
||||||
|
position: PositionInterface,
|
||||||
|
viewport: ViewportInterface,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchedMessageInterface {
|
||||||
|
event: string,
|
||||||
|
payload: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export interface ItemEventMessageInterface {
|
export interface ItemEventMessageInterface {
|
||||||
itemId: number,
|
itemId: number,
|
||||||
event: string,
|
event: string,
|
||||||
@ -123,6 +143,18 @@ export class Connection implements Connection {
|
|||||||
this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => {
|
this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => {
|
||||||
console.error(EventMessage.MESSAGE_ERROR, message);
|
console.error(EventMessage.MESSAGE_ERROR, message);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages inside batched messages are extracted and sent to listeners directly.
|
||||||
|
*/
|
||||||
|
this.socket.on(EventMessage.BATCH, (batchedMessages: BatchedMessageInterface[]) => {
|
||||||
|
for (const message of batchedMessages) {
|
||||||
|
const listeners = this.socket.listeners(message.event);
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(message.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createConnection(name: string, characterLayersSelected: string[]): Promise<Connection> {
|
public static createConnection(name: string, characterLayersSelected: string[]): Promise<Connection> {
|
||||||
@ -163,21 +195,33 @@ export class Connection implements Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise<RoomJoinedMessageInterface> {
|
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise<RoomJoinedMessageInterface> {
|
||||||
const promise = new Promise<RoomJoinedMessageInterface>((resolve, reject) => {
|
const promise = new Promise<RoomJoinedMessageInterface>((resolve, reject) => {
|
||||||
this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (roomJoinedMessage: RoomJoinedMessageInterface) => {
|
this.socket.emit(EventMessage.JOIN_ROOM, {
|
||||||
resolve(roomJoinedMessage);
|
roomId,
|
||||||
});
|
position: {x: startX, y: startY, direction, moving },
|
||||||
|
viewport,
|
||||||
|
}, (roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||||
|
resolve(roomJoinedMessage);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sharePosition(x : number, y : number, direction : string, moving: boolean) : void{
|
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{
|
||||||
if(!this.socket){
|
if(!this.socket){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const point = new Point(x, y, direction, moving);
|
const point = new Point(x, y, direction, moving);
|
||||||
this.socket.emit(EventMessage.USER_POSITION, point);
|
this.socket.emit(EventMessage.USER_POSITION, { position: point, viewport } as UserMovesInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSilent(silent: boolean): void {
|
||||||
|
this.socket.emit(EventMessage.SET_SILENT, silent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewport(viewport: ViewportInterface): void {
|
||||||
|
this.socket.emit(EventMessage.SET_VIEWPORT, viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUserJoins(callback: (message: MessageUserJoined) => void): void {
|
public onUserJoins(callback: (message: MessageUserJoined) => void): void {
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
||||||
const API_URL = (typeof(window) !== 'undefined' ? window.location.protocol : 'http:') + '//' + (process.env.API_URL || "api.workadventure.localhost");
|
const API_URL = (typeof(window) !== 'undefined' ? window.location.protocol : 'http:') + '//' + (process.env.API_URL || "api.workadventure.localhost");
|
||||||
|
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
|
||||||
|
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
|
||||||
|
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
|
||||||
|
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||||
const RESOLUTION = 3;
|
const RESOLUTION = 3;
|
||||||
const ZOOM_LEVEL = 1/*3/4*/;
|
const ZOOM_LEVEL = 1/*3/4*/;
|
||||||
const POSITION_DELAY = 200; // Wait 200ms between sending position events
|
const POSITION_DELAY = 200; // Wait 200ms between sending position events
|
||||||
@ -11,5 +15,9 @@ export {
|
|||||||
RESOLUTION,
|
RESOLUTION,
|
||||||
ZOOM_LEVEL,
|
ZOOM_LEVEL,
|
||||||
POSITION_DELAY,
|
POSITION_DELAY,
|
||||||
MAX_EXTRAPOLATION_TIME
|
MAX_EXTRAPOLATION_TIME,
|
||||||
|
TURN_SERVER,
|
||||||
|
TURN_USER,
|
||||||
|
TURN_PASSWORD,
|
||||||
|
JITSI_URL
|
||||||
}
|
}
|
||||||
|
97
front/src/Phaser/Game/GameMap.ts
Normal file
97
front/src/Phaser/Game/GameMap.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {ITiledMap} from "../Map/ITiledMap";
|
||||||
|
|
||||||
|
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||||
|
* It is used to handle layer properties.
|
||||||
|
*/
|
||||||
|
export class GameMap {
|
||||||
|
private key: number|undefined;
|
||||||
|
private lastProperties = new Map<string, string|boolean|number>();
|
||||||
|
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||||
|
|
||||||
|
public constructor(private map: ITiledMap) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position of the current player (in pixels)
|
||||||
|
* This will trigger events if properties are changing.
|
||||||
|
*/
|
||||||
|
public setPosition(x: number, y: number) {
|
||||||
|
const xMap = Math.floor(x / this.map.tilewidth);
|
||||||
|
const yMap = Math.floor(y / this.map.tileheight);
|
||||||
|
const key = xMap + yMap * this.map.width;
|
||||||
|
if (key === this.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.key = key;
|
||||||
|
|
||||||
|
const newProps = this.getProperties(key);
|
||||||
|
const oldProps = this.lastProperties;
|
||||||
|
|
||||||
|
// Let's compare the 2 maps:
|
||||||
|
// First new properties vs oldProperties
|
||||||
|
for (const [newPropName, newPropValue] of newProps.entries()) {
|
||||||
|
const oldPropValue = oldProps.get(newPropName);
|
||||||
|
if (oldPropValue !== newPropValue) {
|
||||||
|
this.trigger(newPropName, oldPropValue, newPropValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [oldPropName, oldPropValue] of oldProps.entries()) {
|
||||||
|
if (!newProps.has(oldPropName)) {
|
||||||
|
// We found a property that disappeared
|
||||||
|
this.trigger(oldPropName, oldPropValue, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastProperties = newProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProperties(key: number): Map<string, string|boolean|number> {
|
||||||
|
const properties = new Map<string, string|boolean|number>();
|
||||||
|
|
||||||
|
for (const layer of this.map.layers) {
|
||||||
|
if (layer.type !== 'tilelayer') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tiles = layer.data as number[];
|
||||||
|
if (tiles[key] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// There is a tile in this layer, let's embed the properties
|
||||||
|
if (layer.properties !== undefined) {
|
||||||
|
for (const layerProperty of layer.properties) {
|
||||||
|
if (layerProperty.value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
properties.set(layerProperty.name, layerProperty.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined) {
|
||||||
|
const callbacksArray = this.callbacks.get(propName);
|
||||||
|
if (callbacksArray !== undefined) {
|
||||||
|
for (const callback of callbacksArray) {
|
||||||
|
callback(newValue, oldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on.
|
||||||
|
*/
|
||||||
|
public onPropertyChange(propName: string, callback: PropertyChangeCallback) {
|
||||||
|
let callbacksArray = this.callbacks.get(propName);
|
||||||
|
if (callbacksArray === undefined) {
|
||||||
|
callbacksArray = new Array<PropertyChangeCallback>();
|
||||||
|
this.callbacks.set(propName, callbacksArray);
|
||||||
|
}
|
||||||
|
callbacksArray.push(callback);
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import {
|
|||||||
RoomJoinedMessageInterface
|
RoomJoinedMessageInterface
|
||||||
} from "../../Connection";
|
} from "../../Connection";
|
||||||
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
|
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
|
||||||
import {DEBUG_MODE, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
|
import {DEBUG_MODE, JITSI_URL, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
|
||||||
import {
|
import {
|
||||||
ITiledMap,
|
ITiledMap,
|
||||||
ITiledMapLayer,
|
ITiledMapLayer,
|
||||||
@ -33,6 +33,9 @@ import Sprite = Phaser.GameObjects.Sprite;
|
|||||||
import CanvasTexture = Phaser.Textures.CanvasTexture;
|
import CanvasTexture = Phaser.Textures.CanvasTexture;
|
||||||
import GameObject = Phaser.GameObjects.GameObject;
|
import GameObject = Phaser.GameObjects.GameObject;
|
||||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||||
|
import {GameMap} from "./GameMap";
|
||||||
|
import {CoWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||||
|
import {mediaManager} from "../../WebRtc/MediaManager";
|
||||||
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene";
|
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene";
|
||||||
import {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
|
import {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
|
||||||
import {ActionableItem} from "../Items/ActionableItem";
|
import {ActionableItem} from "../Items/ActionableItem";
|
||||||
@ -122,7 +125,8 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
private startLayerName: string|undefined;
|
private startLayerName: string|undefined;
|
||||||
private presentationModeSprite!: Sprite;
|
private presentationModeSprite!: Sprite;
|
||||||
private chatModeSprite!: Sprite;
|
private chatModeSprite!: Sprite;
|
||||||
private repositionCallback!: (this: Window, ev: UIEvent) => void;
|
private onResizeCallback!: (this: Window, ev: UIEvent) => void;
|
||||||
|
private gameMap!: GameMap;
|
||||||
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
|
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
|
||||||
// The item that can be selected by pressing the space key.
|
// The item that can be selected by pressing the space key.
|
||||||
private outlinedItem: ActionableItem|null = null;
|
private outlinedItem: ActionableItem|null = null;
|
||||||
@ -247,7 +251,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
|
|
||||||
this.scene.stop(this.scene.key);
|
this.scene.stop(this.scene.key);
|
||||||
this.scene.remove(this.scene.key);
|
this.scene.remove(this.scene.key);
|
||||||
window.removeEventListener('resize', this.repositionCallback);
|
window.removeEventListener('resize', this.onResizeCallback);
|
||||||
})
|
})
|
||||||
|
|
||||||
connection.onActionableEvent((message => {
|
connection.onActionableEvent((message => {
|
||||||
@ -398,6 +402,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
create(): void {
|
create(): void {
|
||||||
//initalise map
|
//initalise map
|
||||||
this.Map = this.add.tilemap(this.MapKey);
|
this.Map = this.add.tilemap(this.MapKey);
|
||||||
|
this.gameMap = new GameMap(this.mapFile);
|
||||||
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
|
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
|
||||||
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
|
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
|
||||||
this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/));
|
this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/));
|
||||||
@ -521,12 +526,14 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.presentationModeSprite.setOrigin(0, 1);
|
this.presentationModeSprite.setOrigin(0, 1);
|
||||||
this.presentationModeSprite.setInteractive();
|
this.presentationModeSprite.setInteractive();
|
||||||
this.presentationModeSprite.setVisible(false);
|
this.presentationModeSprite.setVisible(false);
|
||||||
|
this.presentationModeSprite.setDepth(99999);
|
||||||
this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
||||||
this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3);
|
this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3);
|
||||||
this.chatModeSprite.setScrollFactor(0, 0);
|
this.chatModeSprite.setScrollFactor(0, 0);
|
||||||
this.chatModeSprite.setOrigin(0, 1);
|
this.chatModeSprite.setOrigin(0, 1);
|
||||||
this.chatModeSprite.setInteractive();
|
this.chatModeSprite.setInteractive();
|
||||||
this.chatModeSprite.setVisible(false);
|
this.chatModeSprite.setVisible(false);
|
||||||
|
this.chatModeSprite.setDepth(99999);
|
||||||
this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
||||||
|
|
||||||
// FIXME: change this to use the UserInputManager class for input
|
// FIXME: change this to use the UserInputManager class for input
|
||||||
@ -534,12 +541,58 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.switchLayoutMode();
|
this.switchLayoutMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.repositionCallback = this.reposition.bind(this);
|
this.onResizeCallback = this.onResize.bind(this);
|
||||||
window.addEventListener('resize', this.repositionCallback);
|
window.addEventListener('resize', this.onResizeCallback);
|
||||||
this.reposition();
|
this.reposition();
|
||||||
|
|
||||||
// From now, this game scene will be notified of reposition events
|
// From now, this game scene will be notified of reposition events
|
||||||
layoutManager.setListener(this);
|
layoutManager.setListener(this);
|
||||||
|
|
||||||
|
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
|
||||||
|
if (newValue === undefined) {
|
||||||
|
CoWebsiteManager.closeCoWebsite();
|
||||||
|
} else {
|
||||||
|
CoWebsiteManager.loadCoWebsite(newValue as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue) => {
|
||||||
|
if (newValue === undefined) {
|
||||||
|
this.connection.setSilent(false);
|
||||||
|
jitsiApi?.dispose();
|
||||||
|
CoWebsiteManager.closeCoWebsite();
|
||||||
|
mediaManager.showGameOverlay();
|
||||||
|
} else {
|
||||||
|
CoWebsiteManager.insertCoWebsite((cowebsiteDiv => {
|
||||||
|
const domain = JITSI_URL;
|
||||||
|
const options = {
|
||||||
|
roomName: this.instance + "-" + newValue,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
parentNode: cowebsiteDiv,
|
||||||
|
configOverwrite: {
|
||||||
|
prejoinPageEnabled: false
|
||||||
|
},
|
||||||
|
interfaceConfigOverwrite: {
|
||||||
|
SHOW_CHROME_EXTENSION_BANNER: false,
|
||||||
|
MOBILE_APP_PROMO: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jitsiApi = new (window as any).JitsiMeetExternalAPI(domain, options); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
jitsiApi.executeCommand('displayName', gameManager.getPlayerName());
|
||||||
|
}));
|
||||||
|
this.connection.setSilent(true);
|
||||||
|
mediaManager.hideGameOverlay();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
|
||||||
|
if (newValue === undefined || newValue === false || newValue === '') {
|
||||||
|
this.connection.setSilent(false);
|
||||||
|
} else {
|
||||||
|
this.connection.setSilent(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private switchLayoutMode(): void {
|
private switchLayoutMode(): void {
|
||||||
@ -713,7 +766,17 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
|
|
||||||
//join room
|
//join room
|
||||||
this.connectionPromise.then((connection: Connection) => {
|
this.connectionPromise.then((connection: Connection) => {
|
||||||
connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((roomJoinedMessage: RoomJoinedMessageInterface) => {
|
const camera = this.cameras.main;
|
||||||
|
connection.joinARoom(this.RoomId,
|
||||||
|
this.startX,
|
||||||
|
this.startY,
|
||||||
|
PlayerAnimationNames.WalkDown,
|
||||||
|
false, {
|
||||||
|
left: camera.scrollX,
|
||||||
|
top: camera.scrollY,
|
||||||
|
right: camera.scrollX + camera.width,
|
||||||
|
bottom: camera.scrollY + camera.height,
|
||||||
|
}).then((roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||||
this.initUsersPosition(roomJoinedMessage.users);
|
this.initUsersPosition(roomJoinedMessage.users);
|
||||||
this.connectionAnswerPromiseResolve(roomJoinedMessage);
|
this.connectionAnswerPromiseResolve(roomJoinedMessage);
|
||||||
});
|
});
|
||||||
@ -721,6 +784,9 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
//listen event to share position of user
|
//listen event to share position of user
|
||||||
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
|
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
|
||||||
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this))
|
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this))
|
||||||
|
this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => {
|
||||||
|
this.gameMap.setPosition(event.x, event.y);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,7 +862,13 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
private doPushPlayerPosition(event: HasMovedEvent): void {
|
private doPushPlayerPosition(event: HasMovedEvent): void {
|
||||||
this.lastMoveEventSent = event;
|
this.lastMoveEventSent = event;
|
||||||
this.lastSentTick = this.currentTick;
|
this.lastSentTick = this.currentTick;
|
||||||
this.connection.sharePosition(event.x, event.y, event.direction, event.moving);
|
const camera = this.cameras.main;
|
||||||
|
this.connection.sharePosition(event.x, event.y, event.direction, event.moving, {
|
||||||
|
left: camera.scrollX,
|
||||||
|
top: camera.scrollY,
|
||||||
|
right: camera.scrollX + camera.width,
|
||||||
|
bottom: camera.scrollY + camera.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EventToClickOnTile(){
|
EventToClickOnTile(){
|
||||||
@ -860,7 +932,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.simplePeer.unregister();
|
this.simplePeer.unregister();
|
||||||
this.scene.stop();
|
this.scene.stop();
|
||||||
this.scene.remove(this.scene.key);
|
this.scene.remove(this.scene.key);
|
||||||
window.removeEventListener('resize', this.repositionCallback);
|
window.removeEventListener('resize', this.onResizeCallback);
|
||||||
this.scene.start(nextSceneKey.key, {
|
this.scene.start(nextSceneKey.key, {
|
||||||
startLayerName: nextSceneKey.hash
|
startLayerName: nextSceneKey.hash
|
||||||
});
|
});
|
||||||
@ -1058,6 +1130,19 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.connection.emitActionableEvent(itemId, eventName, state, parameters);
|
this.connection.emitActionableEvent(itemId, eventName, state, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onResize(): void {
|
||||||
|
this.reposition();
|
||||||
|
|
||||||
|
// Send new viewport to server
|
||||||
|
const camera = this.cameras.main;
|
||||||
|
this.connection.setViewport({
|
||||||
|
left: camera.scrollX,
|
||||||
|
top: camera.scrollY,
|
||||||
|
right: camera.scrollX + camera.width,
|
||||||
|
bottom: camera.scrollY + camera.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private reposition(): void {
|
private reposition(): void {
|
||||||
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
||||||
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
||||||
|
@ -266,6 +266,9 @@ export class EnableCameraScene extends Phaser.Scene {
|
|||||||
this.soundMeter.stop();
|
this.soundMeter.stop();
|
||||||
window.removeEventListener('resize', this.repositionCallback);
|
window.removeEventListener('resize', this.repositionCallback);
|
||||||
|
|
||||||
|
mediaManager.stopCamera();
|
||||||
|
mediaManager.stopMicrophone();
|
||||||
|
|
||||||
// Do we have a start URL in the address bar? If so, let's redirect to this address
|
// Do we have a start URL in the address bar? If so, let's redirect to this address
|
||||||
const instanceAndMapUrl = this.findMapUrl();
|
const instanceAndMapUrl = this.findMapUrl();
|
||||||
if (instanceAndMapUrl !== null) {
|
if (instanceAndMapUrl !== null) {
|
||||||
|
@ -14,7 +14,24 @@ export class CoWebsiteManager {
|
|||||||
iframe.id = 'cowebsite-iframe';
|
iframe.id = 'cowebsite-iframe';
|
||||||
iframe.src = url;
|
iframe.src = url;
|
||||||
cowebsiteDiv.appendChild(iframe);
|
cowebsiteDiv.appendChild(iframe);
|
||||||
|
//iframe.onload = () => {
|
||||||
|
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||||
|
CoWebsiteManager.fire();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just like loadCoWebsite but the div can be filled by the user.
|
||||||
|
*/
|
||||||
|
public static insertCoWebsite(callback: (cowebsite: HTMLDivElement) => void): void {
|
||||||
|
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite");
|
||||||
|
cowebsiteDiv.innerHTML = '';
|
||||||
|
|
||||||
|
callback(cowebsiteDiv);
|
||||||
|
//iframe.onload = () => {
|
||||||
|
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||||
CoWebsiteManager.fire();
|
CoWebsiteManager.fire();
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static closeCoWebsite(): void {
|
public static closeCoWebsite(): void {
|
||||||
@ -24,8 +41,8 @@ export class CoWebsiteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static getGameSize(): {width: number, height: number} {
|
public static getGameSize(): {width: number, height: number} {
|
||||||
const iframe = document.getElementById('cowebsite-iframe');
|
const hasChildren = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite").children.length > 0;
|
||||||
if (iframe === null) {
|
if (hasChildren === false) {
|
||||||
return {
|
return {
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight
|
height: window.innerHeight
|
||||||
|
@ -42,6 +42,14 @@ class LayoutManager {
|
|||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
div.id = "user-"+userId;
|
div.id = "user-"+userId;
|
||||||
div.className = "media-container"
|
div.className = "media-container"
|
||||||
|
div.onclick = () => {
|
||||||
|
const parentId = div.parentElement?.id;
|
||||||
|
if (parentId === 'sidebar' || parentId === 'chat-mode') {
|
||||||
|
this.focusOn(userId);
|
||||||
|
} else {
|
||||||
|
this.removeFocusOn(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (importance === DivImportance.Important) {
|
if (importance === DivImportance.Important) {
|
||||||
this.importantDivs.set(userId, div);
|
this.importantDivs.set(userId, div);
|
||||||
@ -76,6 +84,48 @@ class LayoutManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode)
|
||||||
|
*/
|
||||||
|
private focusOn(userId: string): void {
|
||||||
|
const focusedDiv = this.getDivByUserId(userId);
|
||||||
|
for (const [importantUserId, importantDiv] of this.importantDivs.entries()) {
|
||||||
|
//this.positionDiv(importantDiv, DivImportance.Normal);
|
||||||
|
this.importantDivs.delete(importantUserId);
|
||||||
|
this.normalDivs.set(importantUserId, importantDiv);
|
||||||
|
}
|
||||||
|
this.normalDivs.delete(userId);
|
||||||
|
this.importantDivs.set(userId, focusedDiv);
|
||||||
|
//this.positionDiv(focusedDiv, DivImportance.Important);
|
||||||
|
this.switchLayoutMode(LayoutMode.Presentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes userId from presentation mode
|
||||||
|
*/
|
||||||
|
private removeFocusOn(userId: string): void {
|
||||||
|
const importantDiv = this.importantDivs.get(userId);
|
||||||
|
if (importantDiv === undefined) {
|
||||||
|
throw new Error('Div with user id "'+userId+'" is not in important mode');
|
||||||
|
}
|
||||||
|
this.normalDivs.set(userId, importantDiv);
|
||||||
|
this.importantDivs.delete(userId);
|
||||||
|
|
||||||
|
this.positionDiv(importantDiv, DivImportance.Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDivByUserId(userId: string): HTMLDivElement {
|
||||||
|
let div = this.importantDivs.get(userId);
|
||||||
|
if (div !== undefined) {
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
div = this.normalDivs.get(userId);
|
||||||
|
if (div !== undefined) {
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
throw new Error('Could not find media with user id '+userId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the DIV matching userId.
|
* Removes the DIV matching userId.
|
||||||
*/
|
*/
|
||||||
@ -154,6 +204,7 @@ class LayoutManager {
|
|||||||
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
||||||
*/
|
*/
|
||||||
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
|
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
|
||||||
|
const game = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
|
||||||
if (this.mode === LayoutMode.VideoChat) {
|
if (this.mode === LayoutMode.VideoChat) {
|
||||||
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
||||||
const htmlChildren = Array.from(children.values());
|
const htmlChildren = Array.from(children.values());
|
||||||
@ -163,27 +214,27 @@ class LayoutManager {
|
|||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
||||||
// Compute area between top right of the last div and bottom right of window
|
// Compute area between top right of the last div and bottom right of window
|
||||||
const area1 = (window.innerWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
||||||
* (window.innerHeight - lastDiv.offsetTop);
|
* (game.offsetHeight - lastDiv.offsetTop);
|
||||||
|
|
||||||
// Compute area between bottom of last div and bottom of the screen on whole width
|
// Compute area between bottom of last div and bottom of the screen on whole width
|
||||||
const area2 = window.innerWidth
|
const area2 = game.offsetWidth
|
||||||
* (window.innerHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
||||||
|
|
||||||
if (area1 < 0 && area2 < 0) {
|
if (area1 < 0 && area2 < 0) {
|
||||||
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (area1 <= area2) {
|
if (area1 <= area2) {
|
||||||
@ -191,16 +242,16 @@ class LayoutManager {
|
|||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('lastDiv', lastDiv.offsetTop);
|
console.log('lastDiv', lastDiv.offsetTop);
|
||||||
return {
|
return {
|
||||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||||
yStart: lastDiv.offsetTop,
|
yStart: lastDiv.offsetTop,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -213,15 +264,15 @@ class LayoutManager {
|
|||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, we know we have at least one element in the main section.
|
// At this point, we know we have at least one element in the main section.
|
||||||
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
||||||
|
|
||||||
const presentationArea = (window.innerHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
||||||
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||||
|
|
||||||
let leftSideBar: number;
|
let leftSideBar: number;
|
||||||
@ -234,22 +285,22 @@ class LayoutManager {
|
|||||||
leftSideBar = lastSideBarChildren.offsetLeft;
|
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||||
}
|
}
|
||||||
const sideBarArea = (window.innerWidth - leftSideBar)
|
const sideBarArea = (game.offsetWidth - leftSideBar)
|
||||||
* (window.innerHeight - bottomSideBar);
|
* (game.offsetHeight - bottomSideBar);
|
||||||
|
|
||||||
if (presentationArea <= sideBarArea) {
|
if (presentationArea <= sideBarArea) {
|
||||||
return {
|
return {
|
||||||
xStart: leftSideBar,
|
xStart: leftSideBar,
|
||||||
yStart: bottomSideBar,
|
yStart: bottomSideBar,
|
||||||
xEnd: window.innerWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
||||||
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ window.innerWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
||||||
yEnd: window.innerHeight
|
yEnd: game.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ const videoConstraint: boolean|MediaTrackConstraints = {
|
|||||||
facingMode: "user"
|
facingMode: "user"
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdatedLocalStreamCallback = (media: MediaStream) => void;
|
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
|
||||||
type StartScreenSharingCallback = (media: MediaStream) => void;
|
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||||
type StopScreenSharingCallback = (media: MediaStream) => void;
|
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||||
|
|
||||||
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
|
// 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!!!!)
|
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
|
||||||
@ -109,7 +109,7 @@ export class MediaManager {
|
|||||||
this.updatedLocalStreamCallBacks.delete(callback);
|
this.updatedLocalStreamCallBacks.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream): void {
|
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream|null): void {
|
||||||
for (const callback of this.updatedLocalStreamCallBacks) {
|
for (const callback of this.updatedLocalStreamCallBacks) {
|
||||||
callback(stream);
|
callback(stream);
|
||||||
}
|
}
|
||||||
@ -127,11 +127,16 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showGameOverlay(){
|
public showGameOverlay(){
|
||||||
const gameOverlay = this.getElementByIdOrFail('game-overlay');
|
const gameOverlay = this.getElementByIdOrFail('game-overlay');
|
||||||
gameOverlay.classList.add('active');
|
gameOverlay.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hideGameOverlay(){
|
||||||
|
const gameOverlay = this.getElementByIdOrFail('game-overlay');
|
||||||
|
gameOverlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
private enableCamera() {
|
private enableCamera() {
|
||||||
this.cinemaClose.style.display = "none";
|
this.cinemaClose.style.display = "none";
|
||||||
this.cinemaBtn.classList.remove("disabled");
|
this.cinemaBtn.classList.remove("disabled");
|
||||||
@ -142,20 +147,20 @@ export class MediaManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private disableCamera() {
|
private async disableCamera() {
|
||||||
this.cinemaClose.style.display = "block";
|
this.cinemaClose.style.display = "block";
|
||||||
this.cinema.style.display = "none";
|
this.cinema.style.display = "none";
|
||||||
this.cinemaBtn.classList.add("disabled");
|
this.cinemaBtn.classList.add("disabled");
|
||||||
this.constraintsMedia.video = false;
|
this.constraintsMedia.video = false;
|
||||||
this.myCamVideo.srcObject = null;
|
this.myCamVideo.srcObject = null;
|
||||||
if (this.localStream) {
|
this.stopCamera();
|
||||||
this.localStream.getVideoTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
|
|
||||||
MediaStreamTrack.stop();
|
if (this.constraintsMedia.audio !== false) {
|
||||||
});
|
const stream = await this.getCamera();
|
||||||
}
|
|
||||||
this.getCamera().then((stream) => {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||||
});
|
} else {
|
||||||
|
this.triggerUpdatedLocalStreamCallbacks(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableMicrophone() {
|
private enableMicrophone() {
|
||||||
@ -163,24 +168,25 @@ export class MediaManager {
|
|||||||
this.microphone.style.display = "block";
|
this.microphone.style.display = "block";
|
||||||
this.microphoneBtn.classList.remove("disabled");
|
this.microphoneBtn.classList.remove("disabled");
|
||||||
this.constraintsMedia.audio = true;
|
this.constraintsMedia.audio = true;
|
||||||
|
|
||||||
this.getCamera().then((stream) => {
|
this.getCamera().then((stream) => {
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private disableMicrophone() {
|
private async disableMicrophone() {
|
||||||
this.microphoneClose.style.display = "block";
|
this.microphoneClose.style.display = "block";
|
||||||
this.microphone.style.display = "none";
|
this.microphone.style.display = "none";
|
||||||
this.microphoneBtn.classList.add("disabled");
|
this.microphoneBtn.classList.add("disabled");
|
||||||
this.constraintsMedia.audio = false;
|
this.constraintsMedia.audio = false;
|
||||||
if(this.localStream) {
|
this.stopMicrophone();
|
||||||
this.localStream.getAudioTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
|
|
||||||
MediaStreamTrack.stop();
|
if (this.constraintsMedia.video !== false) {
|
||||||
});
|
const stream = await this.getCamera();
|
||||||
}
|
|
||||||
this.getCamera().then((stream) => {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||||
});
|
} else {
|
||||||
|
this.triggerUpdatedLocalStreamCallbacks(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableScreenSharing() {
|
private enableScreenSharing() {
|
||||||
@ -287,6 +293,28 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the camera from filming
|
||||||
|
*/
|
||||||
|
public stopCamera(): void {
|
||||||
|
if (this.localStream) {
|
||||||
|
for (const track of this.localStream.getVideoTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the microphone from listening
|
||||||
|
*/
|
||||||
|
public stopMicrophone(): void {
|
||||||
|
if (this.localStream) {
|
||||||
|
for (const track of this.localStream.getAudioTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setCamera(id: string): Promise<MediaStream> {
|
setCamera(id: string): Promise<MediaStream> {
|
||||||
let video = this.constraintsMedia.video;
|
let video = this.constraintsMedia.video;
|
||||||
if (typeof(video) === 'boolean' || video === undefined) {
|
if (typeof(video) === 'boolean' || video === undefined) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as SimplePeerNamespace from "simple-peer";
|
import * as SimplePeerNamespace from "simple-peer";
|
||||||
import {mediaManager} from "./MediaManager";
|
import {mediaManager} from "./MediaManager";
|
||||||
import {Connection} from "../Connection";
|
import {Connection} from "../Connection";
|
||||||
|
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||||
|
|
||||||
@ -23,9 +24,9 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
urls: 'stun:stun.l.google.com:19302'
|
urls: 'stun:stun.l.google.com:19302'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
urls: 'turn:numb.viagenie.ca',
|
urls: TURN_SERVER.split(','),
|
||||||
username: 'g.parant@thecodingmachine.com',
|
username: TURN_USER,
|
||||||
credential: 'itcugcOHxle9Acqi$'
|
credential: TURN_PASSWORD
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -70,7 +71,7 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendWebrtcScreenSharingSignal(data: unknown) {
|
private sendWebrtcScreenSharingSignal(data: unknown) {
|
||||||
console.log("sendWebrtcScreenSharingSignal", data);
|
//console.log("sendWebrtcScreenSharingSignal", data);
|
||||||
try {
|
try {
|
||||||
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
||||||
}catch (e) {
|
}catch (e) {
|
||||||
@ -82,8 +83,8 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
* Sends received stream to screen.
|
* Sends received stream to screen.
|
||||||
*/
|
*/
|
||||||
private stream(stream?: MediaStream) {
|
private stream(stream?: MediaStream) {
|
||||||
console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
//console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
||||||
console.log(`stream => ${this.userId} => `, stream);
|
//console.log(`stream => ${this.userId} => `, stream);
|
||||||
if(!stream){
|
if(!stream){
|
||||||
mediaManager.removeActiveScreenSharingVideo(this.userId);
|
mediaManager.removeActiveScreenSharingVideo(this.userId);
|
||||||
this.isReceivingStream = false;
|
this.isReceivingStream = false;
|
||||||
|
@ -4,7 +4,12 @@ import {
|
|||||||
WebRtcSignalReceivedMessageInterface,
|
WebRtcSignalReceivedMessageInterface,
|
||||||
WebRtcStartMessageInterface
|
WebRtcStartMessageInterface
|
||||||
} from "../Connection";
|
} from "../Connection";
|
||||||
import { mediaManager } from "./MediaManager";
|
import {
|
||||||
|
mediaManager,
|
||||||
|
StartScreenSharingCallback,
|
||||||
|
StopScreenSharingCallback,
|
||||||
|
UpdatedLocalStreamCallback
|
||||||
|
} from "./MediaManager";
|
||||||
import * as SimplePeerNamespace from "simple-peer";
|
import * as SimplePeerNamespace from "simple-peer";
|
||||||
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
||||||
import {VideoPeer} from "./VideoPeer";
|
import {VideoPeer} from "./VideoPeer";
|
||||||
@ -32,9 +37,9 @@ export class SimplePeer {
|
|||||||
|
|
||||||
private PeerScreenSharingConnectionArray: Map<string, ScreenSharingPeer> = new Map<string, ScreenSharingPeer>();
|
private PeerScreenSharingConnectionArray: Map<string, ScreenSharingPeer> = new Map<string, ScreenSharingPeer>();
|
||||||
private PeerConnectionArray: Map<string, VideoPeer> = new Map<string, VideoPeer>();
|
private PeerConnectionArray: Map<string, VideoPeer> = new Map<string, VideoPeer>();
|
||||||
private readonly sendLocalVideoStreamCallback: (media: MediaStream) => void;
|
private readonly sendLocalVideoStreamCallback: UpdatedLocalStreamCallback;
|
||||||
private readonly sendLocalScreenSharingStreamCallback: (media: MediaStream) => void;
|
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
|
||||||
private readonly stopLocalScreenSharingStreamCallback: (media: MediaStream) => void;
|
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
|
||||||
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
||||||
|
|
||||||
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
|
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
|
||||||
@ -326,9 +331,9 @@ export class SimplePeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sendLocalVideoStream(){
|
public sendLocalVideoStream(){
|
||||||
this.Users.forEach((user: UserSimplePeerInterface) => {
|
for (const user of this.Users) {
|
||||||
this.pushVideoToRemoteUser(user.userId);
|
this.pushVideoToRemoteUser(user.userId);
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as SimplePeerNamespace from "simple-peer";
|
import * as SimplePeerNamespace from "simple-peer";
|
||||||
import {mediaManager} from "./MediaManager";
|
import {mediaManager} from "./MediaManager";
|
||||||
import {Connection} from "../Connection";
|
import {Connection} from "../Connection";
|
||||||
|
import {TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||||
|
|
||||||
@ -18,14 +19,31 @@ export class VideoPeer extends Peer {
|
|||||||
urls: 'stun:stun.l.google.com:19302'
|
urls: 'stun:stun.l.google.com:19302'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
urls: 'turn:numb.viagenie.ca',
|
urls: TURN_SERVER.split(','),
|
||||||
username: 'g.parant@thecodingmachine.com',
|
username: TURN_USER,
|
||||||
credential: 'itcugcOHxle9Acqi$'
|
credential: TURN_PASSWORD
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('PEER SETUP ', {
|
||||||
|
initiator: initiator ? initiator : false,
|
||||||
|
reconnectTimer: 10000,
|
||||||
|
config: {
|
||||||
|
iceServers: [
|
||||||
|
{
|
||||||
|
urls: 'stun:stun.l.google.com:19302'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: TURN_SERVER,
|
||||||
|
username: TURN_USER,
|
||||||
|
credential: TURN_PASSWORD
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
//start listen signal for the peer connection
|
//start listen signal for the peer connection
|
||||||
this.on('signal', (data: unknown) => {
|
this.on('signal', (data: unknown) => {
|
||||||
this.sendWebrtcSignal(data);
|
this.sendWebrtcSignal(data);
|
||||||
@ -85,7 +103,7 @@ export class VideoPeer extends Peer {
|
|||||||
* Sends received stream to screen.
|
* Sends received stream to screen.
|
||||||
*/
|
*/
|
||||||
private stream(stream?: MediaStream) {
|
private stream(stream?: MediaStream) {
|
||||||
console.log(`VideoPeer::stream => ${this.userId}`, stream);
|
//console.log(`VideoPeer::stream => ${this.userId}`, stream);
|
||||||
if(!stream){
|
if(!stream){
|
||||||
mediaManager.disabledVideoByUserId(this.userId);
|
mediaManager.disabledVideoByUserId(this.userId);
|
||||||
mediaManager.disabledMicrophoneByUserId(this.userId);
|
mediaManager.disabledMicrophoneByUserId(this.userId);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'phaser';
|
import 'phaser';
|
||||||
import GameConfig = Phaser.Types.Core.GameConfig;
|
import GameConfig = Phaser.Types.Core.GameConfig;
|
||||||
import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable";
|
import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable";
|
||||||
import {cypressAsserter} from "./Cypress/CypressAsserter";
|
import {cypressAsserter} from "./Cypress/CypressAsserter";
|
||||||
import {LoginScene} from "./Phaser/Login/LoginScene";
|
import {LoginScene} from "./Phaser/Login/LoginScene";
|
||||||
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
|
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
|
||||||
@ -15,6 +15,13 @@ import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
|||||||
|
|
||||||
//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com');
|
//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com');
|
||||||
|
|
||||||
|
// Load Jitsi if the environment variable is set.
|
||||||
|
if (JITSI_URL) {
|
||||||
|
const jitsiScript = document.createElement('script');
|
||||||
|
jitsiScript.src = 'https://' + JITSI_URL + '/external_api.js';
|
||||||
|
document.head.appendChild(jitsiScript);
|
||||||
|
}
|
||||||
|
|
||||||
const {width, height} = CoWebsiteManager.getGameSize();
|
const {width, height} = CoWebsiteManager.getGameSize();
|
||||||
|
|
||||||
const config: GameConfig = {
|
const config: GameConfig = {
|
||||||
|
@ -45,7 +45,7 @@ module.exports = {
|
|||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
Phaser: 'phaser'
|
Phaser: 'phaser'
|
||||||
}),
|
}),
|
||||||
new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE'])
|
new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL'])
|
||||||
],
|
],
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,60 @@
|
|||||||
"height":34,
|
"height":34,
|
||||||
"infinite":false,
|
"infinite":false,
|
||||||
"layers":[
|
"layers":[
|
||||||
|
{
|
||||||
|
"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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 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, 309, 309, 309, 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, 309, 309, 309, 309, 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, 309, 309, 309, 309, 309, 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, 309, 309, 309, 309, 309, 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, 309, 309, 309, 309, 309, 309, 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, 309, 309, 309, 309, 309, 309, 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],
|
||||||
|
"height":34,
|
||||||
|
"id":23,
|
||||||
|
"name":"patio",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-patio"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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,
|
||||||
|
"id":22,
|
||||||
|
"name":"chillzone-2",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-chillzone-2"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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,
|
||||||
|
"id":21,
|
||||||
|
"name":"chillzone-1",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-chillzone-1"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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],
|
"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,
|
"height":34,
|
||||||
@ -63,6 +117,42 @@
|
|||||||
"x":0,
|
"x":0,
|
||||||
"y":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,
|
||||||
|
"id":18,
|
||||||
|
"name":"openSwile",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"openWebsite",
|
||||||
|
"type":"string",
|
||||||
|
"value":"https:\/\/app.swile.co\/"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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,
|
||||||
|
"id":19,
|
||||||
|
"name":"jitsyAmphi",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-amphi"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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],
|
"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,
|
"height":34,
|
||||||
@ -236,7 +326,7 @@
|
|||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
}],
|
}],
|
||||||
"nextlayerid":18,
|
"nextlayerid":24,
|
||||||
"nextobjectid":2,
|
"nextobjectid":2,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
|
@ -21,6 +21,60 @@
|
|||||||
"x":0,
|
"x":0,
|
||||||
"y":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, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":18,
|
||||||
|
"id":14,
|
||||||
|
"name":"radiant_meeting_room",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-radiant"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":18,
|
||||||
|
"id":16,
|
||||||
|
"name":"white-meeting-room",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-white-room"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"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, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 4861, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":18,
|
||||||
|
"id":15,
|
||||||
|
"name":"dire-meeting-room",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"jitsiRoom",
|
||||||
|
"type":"string",
|
||||||
|
"value":"tcm-dire"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"data":[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168],
|
"data":[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168],
|
||||||
"height":18,
|
"height":18,
|
||||||
@ -134,7 +188,7 @@
|
|||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
}],
|
}],
|
||||||
"nextlayerid":14,
|
"nextlayerid":17,
|
||||||
"nextobjectid":1,
|
"nextobjectid":1,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
@ -505,6 +559,29 @@
|
|||||||
}]
|
}]
|
||||||
}],
|
}],
|
||||||
"tilewidth":32
|
"tilewidth":32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns":8,
|
||||||
|
"firstgid":4809,
|
||||||
|
"image":"..\/Floor0\/floortileset.png",
|
||||||
|
"imageheight":256,
|
||||||
|
"imagewidth":256,
|
||||||
|
"margin":0,
|
||||||
|
"name":"floortileset",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":64,
|
||||||
|
"tileheight":32,
|
||||||
|
"tiles":[
|
||||||
|
{
|
||||||
|
"id":37,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"tilewidth":32
|
||||||
}],
|
}],
|
||||||
"tilewidth":32,
|
"tilewidth":32,
|
||||||
"type":"map",
|
"type":"map",
|
||||||
|
26
website/dist/create-map.html
vendored
26
website/dist/create-map.html
vendored
@ -150,10 +150,32 @@
|
|||||||
<li>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)</li>
|
<li>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)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="opening-a-website-when-walking-on-the-map" class="pixel-title">Opening a website when walking on the map</h3>
|
||||||
|
<p>On your map, you can define special zones. When a player will pass over these zones, a website will open
|
||||||
|
(as an iframe on the right side of the screen)</p>
|
||||||
|
<p>In order to create a zone that opens websites:</p>
|
||||||
|
<ul>
|
||||||
|
<li>You must create a specific layer.</li>
|
||||||
|
<li>In layer properties, you MUST add a "openWebsite" property (of type "string"). The value of the property is the URL of the website to open (the URL must start with "https://")</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="opening-a-jitsi-meet-when-walking-on-the-map" class="pixel-title">Opening a Jitsi meet when walking on the map</h3>
|
||||||
|
<p>On your map, you can define special zones (meeting rooms) that will trigger the opening of a Jitsi meet. When a player will pass over these zones, a Jitsi meet will open
|
||||||
|
(as an iframe on the right side of the screen)</p>
|
||||||
|
<p>In order to create Jitsi meet zones:</p>
|
||||||
|
<ul>
|
||||||
|
<li>You must create a specific layer.</li>
|
||||||
|
<li>In layer properties, you MUST add a boolean "jitsiRoom" property (of type "string"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be prepended with the name of the instance of the map (so that different instances of the map have different rooms)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="making-a-silent-zone" class="pixel-title">Making a "silent" zone</h3>
|
||||||
|
<p>On your map, you can define special silent zones where nobody is allowed to talk.
|
||||||
|
In these zones, users will not speak to each others, even if they are next to each others.</p>
|
||||||
|
<p>In order to create a silent zone:</p>
|
||||||
|
<ul>
|
||||||
|
<li>You must create a specific layer.</li>
|
||||||
|
<li>In layer properties, you MUST add a boolean "silent" property. If the silent property is checked, the users are entering the silent zone when they walk on any tile of the layer.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 id="pushing-the-map" class="pixel-title">Pushing the map</h3>
|
<h3 id="pushing-the-map" class="pixel-title">Pushing the map</h3>
|
||||||
<p>When your changes are ready, you need to "commit" and "push" (i.e. "upload") the changes back to GitHub.
|
<p>When your changes are ready, you need to "commit" and "push" (i.e. "upload") the changes back to GitHub.
|
||||||
|
Loading…
Reference in New Issue
Block a user