Adding the capability of create nodes.

This commit is contained in:
Sergiotarxz 2023-12-31 20:43:53 +01:00
parent 85a104caa5
commit 8c27095ad1
25 changed files with 734 additions and 308 deletions

View File

@ -21,6 +21,8 @@ import CreateNode from '@burguillosinfo/conquer/create-node'
import MapState from '@burguillosinfo/conquer/map-state'
import MapNode from '@burguillosinfo/conquer/map-node'
import NewNodeUI from '@burguillosinfo/conquer/interface/new-node'
import WebSocket from '@burguillosinfo/conquer/websocket'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
type StylesInterface = Record<string, Style>
@ -28,6 +30,7 @@ export default class Conquer {
private conquerContainer: HTMLDivElement
private map: Map
private currentLongitude: number
private intervalSendCoordinates: number | null = null;
private currentLatitude: number
private rotationOffset = 0
private heading = 0
@ -45,6 +48,8 @@ export default class Conquer {
private state: MapState = MapState.NOTHING
private createNodeObject: CreateNode
private serverNodes: Record<string, MapNode> = {}
private coordinate_1 = 0;
private coordinate_2 = 0;
public getServerNodes(): Record<string, MapNode> {
return this.serverNodes
@ -78,8 +83,11 @@ export default class Conquer {
const conquer = new Conquer(conquerContainer)
conquer.run()
}
setCenterDisplaced(lat: number, lon: number) {
if (this.firstSetCenter || !(this.state & MapState.FREE_MOVE)) {
this.coordinate_1 = lon;
this.coordinate_2 = lat;
const olCoordinates = this.realCoordinatesToOl(lat, lon)
const size = this.map.getSize()
if (size === undefined) {
@ -112,24 +120,35 @@ export default class Conquer {
const feature = new Feature({
geometry: new Point(coordinates)
})
console.log(coordinates)
const style = new Style({
image: new CircleStyle({
radius: 14,
fill: new Fill({color: 'white'}),
stroke: new Stroke({
color: 'gray',
width: 2,
})
})
const newNodeUI = new NewNodeUI(coordinates)
const oldState = this.getState();
newNodeUI.on('close', () => {
this.interfaceManager.remove(newNodeUI)
this.setState(oldState);
})
const mapNode = new MapNode(style, feature, `server-node-${++this.createNodeCounter}`)
this.getServerNodes()[mapNode.getId()] = mapNode
this.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
this.refreshLayers()
this.interfaceManager.push(newNodeUI)
this.setState(MapState.FILLING_FORM_CREATE_NODE);
// const style = new Style({
// image: new CircleStyle({
// radius: 14,
// fill: new Fill({color: 'white'}),
// stroke: new Stroke({
// color: 'gray',
// width: 2,
// })
// })
// })
// const mapNode = new MapNode(style, feature, `server-node-${++this.createNodeCounter}`)
// this.getServerNodes()[mapNode.getId()] = mapNode
// this.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
// this.refreshLayers()
}
private isStateFillingFormCreateNode(): boolean {
return !!(this.getState() & MapState.FILLING_FORM_CREATE_NODE)
}
async onClickMap(event: MapEvent): Promise<void> {
if (this.isStateCreatingNode() && this.isStateSelectWhereToCreateNode()) {
this.onClickWhereToCreateNode(event)
@ -194,13 +213,89 @@ export default class Conquer {
}
async onLoginSuccess(): Promise<void> {
const currentPositionFeature = this.currentPositionFeature
if (currentPositionFeature === null) {
return
this.clearIntervalSendCoordinates();
this.createIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
this.createIntervalPollNearbyNodes();
}
private intervalPollNearbyNodes: number | null = null;
private clearIntervalPollNearbyNodes(): void {
if (this.intervalPollNearbyNodes !== null) {
window.clearInterval(this.intervalPollNearbyNodes)
this.intervalPollNearbyNodes = null;
}
this.map.on('click', (event: MapEvent) => {
this.onClickMap(event)
})
}
private getNearbyNodes(): void {
const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlNodes).then(async (response) => {
let responseBody;
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
const serverNodes: Record<string, MapNode> = {};
const nodes = JsonSerializer.deserialize(responseBody, MapNode);
if (!(nodes instanceof Array)) {
console.error('Received null instead of node list.');
return;
}
for (const node of nodes) {
if (!(node instanceof MapNode)) {
console.error('Received node is not a MapNode.');
continue;
}
serverNodes[node.getId()] = node;
}
this.serverNodes = serverNodes;
this.refreshLayers();
});
}
private createIntervalPollNearbyNodes(): void {
this.intervalPollNearbyNodes = window.setInterval(() => {
this.getNearbyNodes();
}, 10000)
}
private createIntervalSendCoordinates(): void {
this.intervalSendCoordinates = window.setInterval(() => {
this.sendCoordinatesToServer();
}, 10000);
}
private sendCoordinatesToServer(): void {
const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlLog, {
method: 'POST',
body: JSON.stringify([
this.coordinate_1,
this.coordinate_2,
]),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch(error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (res.status !== 200) {
console.error(responseBody.error);
}
}).catch((error) => {
console.error(error)
});
}
private runPreStartState(): void {
@ -210,12 +305,27 @@ export default class Conquer {
this.interfaceManager = interfaceManager
const conquerLogin = new ConquerLogin(interfaceManager)
conquerLogin.on('login', () => {
this.onLoginSuccess()
})
this.onLoginSuccess();
});
conquerLogin.on('logout', () => {
this.onLogout();
});
conquerLogin.start()
this.conquerLogin = conquerLogin
}
private onLogout(): void {
this.clearIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
}
private clearIntervalSendCoordinates(): void {
if (this.intervalSendCoordinates !== null) {
window.clearInterval(this.intervalSendCoordinates);
this.intervalSendCoordinates = null;
}
}
async run() {
this.runPreStartState()
this.setState(MapState.NORMAL)
@ -286,7 +396,7 @@ export default class Conquer {
this.refreshLayers()
}
private refreshLayers(): void {
private async refreshLayers(): Promise<void> {
if (this.currentPositionFeature === null) {
return
}
@ -305,7 +415,7 @@ export default class Conquer {
const features = [this.currentPositionFeature]
for (const key in this.getServerNodes()) {
styles[key] = this.getServerNodes()[key].getStyle()
features.push(this.getServerNodes()[key].getNode())
features.push(this.getServerNodes()[key].getFeature())
}
const vectorLayer = new VectorLayer<VectorSource>({
source: new VectorSource({
@ -314,6 +424,7 @@ export default class Conquer {
})
if (this.vectorLayer !== null) {
this.map.removeLayer(this.vectorLayer)
this.vectorLayer = null;
}
vectorLayer.setStyle((feature) => {
return styles[feature.getProperties().type]

View File

@ -15,12 +15,14 @@ export default abstract class ConquerInterface {
public run(): void {
return
}
public prune(): void {
this.callbacks = {};
return
}
protected getNodeFromTemplateId(id: string): HTMLElement {
const template = document.getElementById(id)
let template = document.getElementById(id)
if (template === null) {
Conquer.fail(`Unable to find template id ${id}.`)
}
@ -28,6 +30,7 @@ export default abstract class ConquerInterface {
if (!(finalNode instanceof HTMLElement)) {
Conquer.fail('The node is not an Element.')
}
finalNode.classList.remove('conquer-display-none')
return finalNode
}

View File

@ -25,6 +25,9 @@ export default class LoginUI extends ConquerInterface {
}
public run() {
this.conquerLogin.on('login', () => {
this.runCallbacks('close');
});
this.storeRegisterElements()
this.storeLoginElements()
}

View File

@ -3,8 +3,102 @@ import Conquer from '@burguillosinfo/conquer'
export default class NewNodeUI extends AbstractTopBarInterface {
private coordinates: number[];
public getSubmitButton(): HTMLElement {
const submitButton = this.getMainNode().querySelector('button.new-node-form-submit')
if (submitButton === null || !(submitButton instanceof HTMLElement)) {
Conquer.fail('SubmitButton is null');
}
return submitButton;
}
public getErrorElement(): HTMLElement {
const errorElement = this.getMainNode().querySelector('p.conquer-error');
if (errorElement === null || !(errorElement instanceof HTMLElement)) {
Conquer.fail('No error element set');
}
return errorElement;
}
public getSelectNodeType(): HTMLSelectElement {
const selectElement = this.getMainNode().querySelector('select.conquer-node-type');
if (selectElement === null || !(selectElement instanceof HTMLSelectElement)) {
Conquer.fail('SelectElementNodeType is null');
}
return selectElement
}
public getInputNodeName(): HTMLInputElement {
const nodeName = this.getMainNode().querySelector('input.conquer-node-name')
if (nodeName === null || !(nodeName instanceof HTMLInputElement)) {
Conquer.fail('NodeName is null');
}
return nodeName
}
public getTextAreaNodeDescription(): HTMLTextAreaElement {
const nodeDescription = this.getMainNode().querySelector('textarea.conquer-node-description')
if (nodeDescription === null || !(nodeDescription instanceof HTMLTextAreaElement)) {
Conquer.fail('NodeDescription is null');
}
return nodeDescription
}
constructor(coordinates: number[]) {
super()
this.coordinates = coordinates
}
public run() {
const mainNode = this.getMainNode()
const form = this.getNodeFromTemplateId('conquer-new-node-form-creation-template')
mainNode.append(form)
this.getSubmitButton().addEventListener('click', (event) => {
event.preventDefault();
this.onSubmit();
});
form.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private setError(error: string): void {
const errorElement = this.getErrorElement();
errorElement.classList.remove('conquer-display-none')
errorElement.innerText = error
}
private onSubmit(): void {
const selectNodeType = this.getSelectNodeType();
const inputNodeName = this.getInputNodeName();
const textAreaNodeDescription = this.getTextAreaNodeDescription();
const description = textAreaNodeDescription.value;
const nodeName = inputNodeName.value;
const selectedOptionsNodeType = selectNodeType.selectedOptions;
if (selectedOptionsNodeType.length < 1) {
this.setError('Debes selecionar un tipo de nodo.');
return;
}
const selectedOptionNodeType = selectedOptionsNodeType[0];
const nodeType = selectedOptionNodeType.value;
if (nodeName.length < 5) {
this.setError('Todos los nodos deben tener un nombre mayor a 4 caracteres.');
return;
}
const urlNode = new URL('/conquer/node', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlNode, {
method: 'PUT',
body: JSON.stringify({
description: description,
name: nodeName,
type: nodeType,
coordinates: this.coordinates,
}),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch (error) {
this.setError( 'Respuesta erronea del servidor.');
return;
}
if (res.status !== 200) {
this.setError(responseBody.error);
return;
}
this.runCallbacks('close')
});
}
}

View File

@ -7,7 +7,7 @@ export type ConquerLoginEventCallback = () => void
export default class Login {
private conquerLogin: HTMLDivElement
private conquerInterfaceManager: ConquerInterfaceManager
private cachedIsLoggedIn = true
private cachedIsLoggedIn: boolean | null = null
constructor(conquerInterfaceManager: ConquerInterfaceManager) {
this.conquerInterfaceManager = conquerInterfaceManager
@ -49,9 +49,16 @@ export default class Login {
private async loopCheckLogin(): Promise<void> {
window.setInterval(() => {
this.isLogged().then((isLogged) => {
if (!isLogged && this.cachedIsLoggedIn) {
if (isLogged) {
if (this.cachedIsLoggedIn !== true) {
this.cachedIsLoggedIn = true;
this.onLoginSuccess();
}
return;
}
if (this.cachedIsLoggedIn !== false) {
this.cachedIsLoggedIn = false;
this.onLogout()
this.cachedIsLoggedIn = false
}
})
}, 5000)
@ -60,6 +67,12 @@ export default class Login {
private async onLogout(): Promise<void> {
const interfaceManager = this.conquerInterfaceManager
const loginUI = new LoginUI(this)
for (const callback of this.callbacks.logout) {
callback();
}
loginUI.on('close', () => {
interfaceManager.remove(loginUI);
})
interfaceManager.push(loginUI)
}
@ -71,9 +84,8 @@ export default class Login {
this.callbacks[name].push(callback)
}
private async onLoginSuccess(loginUI: LoginUI): Promise<void> {
private async onLoginSuccess(): Promise<void> {
this.cachedIsLoggedIn = true
this.conquerInterfaceManager.remove(loginUI)
for (const callback of this.callbacks.login) {
callback()
}
@ -105,7 +117,7 @@ export default class Login {
loginUI.unsetLoginAndRegisterErrors()
const isLogged = await this.isLogged()
if (isLogged) {
this.onLoginSuccess(loginUI)
this.onLoginSuccess()
}
}

View File

@ -1,28 +1,66 @@
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Style from 'ol/style/Style'
import Feature from 'ol/Feature'
import CircleStyle from 'ol/style/Circle'
import Point from 'ol/geom/Point'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
@JsonObject()
export default class MapNode {
private style: Style
private node: Feature
private id: string
private feature: Feature | null = null;
constructor(style: Style, node: Feature, id: string) {
this.style = style
this.node = node.clone()
this.id = id
this.node.setProperties({type: this.id})
constructor(
@JsonProperty() private uuid: string,
@JsonProperty() private coordinate_1: number,
@JsonProperty() private coordinate_2: number,
@JsonProperty() private type: string,
@JsonProperty() private name: string,
@JsonProperty() private description: string,
@JsonProperty() private kind: string,
) {
}
public getType(): string {
return this.type;
}
public getName(): string {
return this.name;
}
public getDescription(): string {
return this.description;
}
public getId(): string {
return this.id
return 'node-' + this.uuid;
}
public getNode(): Feature {
return this.node
public getFeature(): Feature {
if (this.feature === null) {
console.log(this.coordinate_1);
console.log(this.coordinate_2);
this.feature = new Feature({
geometry: new Point([this.coordinate_1, this.coordinate_2]),
type: 'node-' + this.uuid,
})
}
return this.feature;
}
public getStyle(): Style {
return this.style
return new Style({
image: new CircleStyle({
radius: 14,
fill: new Fill({color: 'white'}),
stroke: new Stroke({
color: 'gray',
width: 2,
})
})
});
}
}

View File

@ -5,6 +5,7 @@ enum MapState {
FREE_ROTATION = 0x4,
CREATE_NODE = 0x8,
SELECT_WHERE_TO_CREATE_NODE = 0x10,
FILLING_FORM_CREATE_NODE = 0x20,
}
export default MapState

View File

@ -0,0 +1,6 @@
import { JsonSerializer, throwError } from 'typescript-json-serializer';
export default new JsonSerializer({
errorCallback: throwError,
additionalPropertiesPolicy: 'disallow',
});

View File

@ -3,7 +3,7 @@ export default class ConquerWebSocket {
private socketReady = false
private getWebSocket(): WebSocket {
if (this.webSocket !== null) {
if (this.webSocket !== null && this.socketReady) {
return this.webSocket
}
this.webSocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/conquer/websocket`)
@ -18,7 +18,7 @@ export default class ConquerWebSocket {
})
return this.webSocket
}
private onSocketOpen(event: Event) {
this.socketReady = true
}

View File

@ -1,29 +0,0 @@
// package: proto.v1.packet
// file: v1/packet/open-new-node.proto
import * as jspb from "google-protobuf";
export class OpenNewNode extends jspb.Message {
getLatitude(): number;
setLatitude(value: number): void;
getLongitude(): number;
setLongitude(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): OpenNewNode.AsObject;
static toObject(includeInstance: boolean, msg: OpenNewNode): OpenNewNode.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: OpenNewNode, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): OpenNewNode;
static deserializeBinaryFromReader(message: OpenNewNode, reader: jspb.BinaryReader): OpenNewNode;
}
export namespace OpenNewNode {
export type AsObject = {
latitude: number,
longitude: number,
}
}

View File

@ -1,206 +0,0 @@
// source: v1/packet/open-new-node.proto
/**
* @fileoverview
* @enhanceable
* @suppress {missingRequire} reports error on implicit type usages.
* @suppress {messageConventions} JS Compiler reports an error if a variable or
* field starts with 'MSG_' and isn't a translatable message.
* @public
*/
// GENERATED CODE -- DO NOT EDIT!
/* eslint-disable */
// @ts-nocheck
var jspb = require('google-protobuf');
var goog = jspb;
var global =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof window !== 'undefined' && window) ||
(typeof global !== 'undefined' && global) ||
(typeof self !== 'undefined' && self) ||
(function () { return this; }).call(null) ||
Function('return this')();
goog.exportSymbol('proto.proto.v1.packet.OpenNewNode', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.proto.v1.packet.OpenNewNode = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.proto.v1.packet.OpenNewNode, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.proto.v1.packet.OpenNewNode.displayName = 'proto.proto.v1.packet.OpenNewNode';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.proto.v1.packet.OpenNewNode.prototype.toObject = function(opt_includeInstance) {
return proto.proto.v1.packet.OpenNewNode.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.proto.v1.packet.OpenNewNode} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.v1.packet.OpenNewNode.toObject = function(includeInstance, msg) {
var f, obj = {
latitude: jspb.Message.getFloatingPointFieldWithDefault(msg, 1, 0.0),
longitude: jspb.Message.getFloatingPointFieldWithDefault(msg, 2, 0.0)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.proto.v1.packet.OpenNewNode}
*/
proto.proto.v1.packet.OpenNewNode.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.proto.v1.packet.OpenNewNode;
return proto.proto.v1.packet.OpenNewNode.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.proto.v1.packet.OpenNewNode} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.proto.v1.packet.OpenNewNode}
*/
proto.proto.v1.packet.OpenNewNode.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {number} */ (reader.readDouble());
msg.setLatitude(value);
break;
case 2:
var value = /** @type {number} */ (reader.readDouble());
msg.setLongitude(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.proto.v1.packet.OpenNewNode.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.proto.v1.packet.OpenNewNode.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.proto.v1.packet.OpenNewNode} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.v1.packet.OpenNewNode.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getLatitude();
if (f !== 0.0) {
writer.writeDouble(
1,
f
);
}
f = message.getLongitude();
if (f !== 0.0) {
writer.writeDouble(
2,
f
);
}
};
/**
* optional double latitude = 1;
* @return {number}
*/
proto.proto.v1.packet.OpenNewNode.prototype.getLatitude = function() {
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 1, 0.0));
};
/**
* @param {number} value
* @return {!proto.proto.v1.packet.OpenNewNode} returns this
*/
proto.proto.v1.packet.OpenNewNode.prototype.setLatitude = function(value) {
return jspb.Message.setProto3FloatField(this, 1, value);
};
/**
* optional double longitude = 2;
* @return {number}
*/
proto.proto.v1.packet.OpenNewNode.prototype.getLongitude = function() {
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 2, 0.0));
};
/**
* @param {number} value
* @return {!proto.proto.v1.packet.OpenNewNode} returns this
*/
proto.proto.v1.packet.OpenNewNode.prototype.setLongitude = function(value) {
return jspb.Message.setProto3FloatField(this, 2, value);
};
goog.object.extend(exports, proto.proto.v1.packet);

View File

@ -88,10 +88,12 @@ sub startup ($self) {
$r->get('/sitemap.xml')->to('Sitemap#sitemap');
$r->get('/robots.txt')->to('Robots#robots');
# $r->get('/:post')->to('Page#post');
$r->get('/stats')->to('Metrics#stats');
$r->get('/conquer')->to('Conquer#index');
$r->put('/conquer/user')->to('UserConquer#create');
$r->post('/conquer/user/coordinates')->to('UserConquer#setCoordinates');
$r->put('/conquer/node')->to('ConquerNode#create');
$r->get('/conquer/node/near')->to('ConquerNode#nearbyNodes');
$r->get('/conquer/user')->to('UserConquer#get_self');
$r->post('/conquer/user/login')->to('UserConquer#login');
$r->get('/conquer/tile/<zoom>/<x>/<y>.png')->to('ConquerTile#tile');

View File

@ -0,0 +1,135 @@
package BurguillosInfo::Controller::ConquerNode;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
sub create ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'No estás autenticado.',
}
);
}
if ( !$user->is_admin ) {
return $self->render(
status => 403,
json => {
error => 'No tienes permiso para hacer eso.',
}
);
}
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $name = $input->{name};
my $coordinates = $input->{coordinates};
my $type = $input->{type};
my $description = $input->{description};
if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
return $self->render(
status => 400,
json => {
error => 'Formato erroneo de coordenadas.',
}
);
}
my ($coordinate_1, $coordinate_2) = $coordinates->@*;
if ( !defined $name && length $name < 5 ) {
return $self->render(
status => 400,
json => {
error =>
'Número incorrecto de carácteres en el nombre del nodo.',
}
);
}
if ( !defined $description ) {
return $self->render(
status => 400,
json => {
error => 'La descripción puede estar vacía, '
. 'pero debe existir, si ves este error '
. 'desde la aplicación es un error de programación.',
}
);
}
if ( !defined $type ) {
return $self->render(
status => 400,
json => {
error => 'Los nodos deben tener un tipo.'
}
);
}
if ( $type ne 'normal' ) {
return $self->render(
status => 400,
json => {
error => 'Tipo de nodo no soportado.',
}
);
}
my $uuid_node = create_uuid_string();
my $node;
eval {
$node = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->new(
{
uuid => $uuid_node,
description => $description,
name => $name,
type => $type,
coordinate_1 => $coordinate_1,
coordinate_2 => $coordinate_2
}
);
$node->insert;
};
if ($@) {
warn $@;
return $self->render(
status => 500,
json => {
error => 'El servidor no pudo almacenar el nodo, reporta este error.',
}
);
}
return $self->render(
status => 200,
json => $node->serialize,
);
}
sub nearbyNodes($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'No estás loggeado.',
});
}
my @nodes = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->search({});
@nodes = map { $_->serialize } @nodes;
return $self->render(json => \@nodes);
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
1;

View File

@ -21,12 +21,12 @@ my $username_maximum_chars = 15;
my $password_minimum_chars = 8;
my $password_maximum_chars = 4096;
sub get_self($self) {
sub get_self ($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->_renderError(401, 'No estás loggeado.');
if ( !defined $user ) {
return $self->_renderError( 401, 'No estás loggeado.' );
}
return $self->render(json => $user->serialize_to_owner, status => 200);
return $self->render( json => $user->serialize_to_owner, status => 200 );
}
sub create ($self) {
@ -47,7 +47,7 @@ sub _expectJson ($self) {
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError(400, 'Se esperaba JSON.');
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
@ -66,8 +66,8 @@ sub login ($self) {
my @tentative_users =
$resultset_conquer_user->search( { username => $username } );
my $tentative_user = $tentative_users[0];
if (!defined $tentative_user) {
$self->_renderError(401, 'El usuario especificado no existe.');
if ( !defined $tentative_user ) {
$self->_renderError( 401, 'El usuario especificado no existe.' );
return;
}
if ( !bcrypt_check( $password, $tentative_user->encrypted_password ) ) {
@ -84,6 +84,40 @@ sub login ($self) {
);
}
sub setCoordinates ($self) {
my $input = $self->_expectJson;
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'Debes estar loggeado para cambiar tus'
. ' coordenadas.',
}
);
}
if ( !defined $input ) {
return;
}
if ( ref $input ne 'ARRAY' && scalar $input->@* == 2 ) {
return $self->render(
status => 400,
json => {
error => 'Mal formato de coordenadas, debe ser '
. 'un array de exactamente 2 números reales.',
}
);
}
$user->coordinates($input);
$user->update;
return $self->render(
status => 200,
json => {
ok => $JSON::true,
}
);
}
sub _createUser ( $self, $username, $password ) {
my $user;
my $uuid = create_uuid_string();
@ -97,6 +131,7 @@ sub _createUser ( $self, $username, $password ) {
username => $username
}
);
$user->coordinates( [ 0, 0 ] );
$user->insert;
};
if ($@) {

View File

@ -57,6 +57,20 @@ sub MIGRATIONS {
is_admin BOOLEAN NOT NULL DEFAULT false,
registration_date TIMESTAMP NOT NULL DEFAULT NOW()
);',
'CREATE TABLE conquer_node (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
coordinate_1 REAL NOT NULL,
coordinate_2 REAL NOT NULL,
description TEXT NOT NULL
);',
'CREATE INDEX index_conquer_node_coordinate_1 on conquer_node (coordinate_1);',
'CREATE INDEX index_conquer_node_coordinate_2 on conquer_node (coordinate_2);',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_1 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_1 DROP DEFAULT;',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_2 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_2 DROP DEFAULT;',
);
}

View File

@ -20,6 +20,8 @@ my $schema;
sub Schema ($class) {
if ( !defined $schema ) {
use BurguillosInfo::DB;
BurguillosInfo::DB->connect;
my $app = BurguillosInfo->new;
my $config = $app->{config};
my $database_config = $config->{db};

View File

@ -0,0 +1,56 @@
package BurguillosInfo::Schema::Result::ConquerNode;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
__PACKAGE__->table('conquer_node');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
default_value => \'0',
},
coordinate_1 => {
data_type => 'real',
is_nullable => 0,
},
coordinate_2 => {
data_type => 'real',
is_nullable => 0,
},
type => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
}
);
sub serialize ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerNode',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
type => $self->type,
coordinate_1 => $self->coordinate_1,
coordinate_2 => $self->coordinate_2,
};
}
__PACKAGE__->set_primary_key('uuid');
1;

View File

@ -39,9 +39,31 @@ __PACKAGE__->add_columns(
data_type => 'boolean',
is_nullable => 0,
default_value => \'0',
}
},
last_coordinate_1 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
last_coordinate_2 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
);
sub coordinates($self, $coordinates = undef) {
if (defined $coordinates) {
if (ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2) {
die 'The second parameter of this subroutine '
. 'must be an ARRAYREF of exactly two elements.';
}
$self->last_coordinate_1($coordinates->[0]);
$self->last_coordinate_2($coordinates->[1]);
}
return [$self->last_coordinate_1, $self->last_coordinate_2];
}
sub serialize_to_owner ($self) {
$self = $self->get_from_storage();
return {

View File

@ -28,6 +28,7 @@
"protoc-gen-js": "^3.21.2",
"tablesort": "^5.3.0",
"ts-loader": "^9.5.0",
"ts-protoc-gen": "^0.15.0"
"ts-protoc-gen": "^0.15.0",
"typescript-json-serializer": "^6.0.1"
}
}

View File

@ -1,8 +0,0 @@
syntax = "proto3";
package proto.v1.packet;
message OpenNewNode {
double latitude = 1;
double longitude = 2;
}

View File

@ -9,20 +9,41 @@ body {
min-height: 100%;
width: 100%;
height: 100%; }
body p.conquer-register-error, body p.conquer-login-error, body p.conquer-login-success {
body p.conquer-register-error, body p.conquer-login-error, body p.conquer-login-success, body p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
background: blanchedalmond;
padding: 3px;
border-radius: 10px;
border: solid 1px black; }
border: solid 1px black;
overflow-y: scroll; }
body form {
display: flex;
flex-direction: column;
width: 100%; }
body form label {
width: 100%; }
body form label input, body form label textarea, body form label select {
width: 100%;
border: none;
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem; }
body form label textarea {
height: 100px; }
body div.conquer-interface-element-padded {
width: calc(100% - 60px);
padding-left: 30px;
padding-right: 30px;
display: flex;
justify-content: center; }
body div.conquer-interface-element-padded.conquer-display-none {
display: none; }
body div.create-node-slide {
display: flex;
position: fixed;

View File

@ -17,7 +17,7 @@ html {
}
body {
p.conquer-register-error, p.conquer-login-error, p.conquer-login-success {
p.conquer-register-error, p.conquer-login-error, p.conquer-login-success,p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
@ -25,6 +25,29 @@ body {
padding: 3px;
border-radius: 10px;
border: solid 1px black;
overflow-y: scroll;
}
form {
display: flex;
flex-direction: column;
width: 100%;
label {
width: 100%;
input, textarea, select {
width: 100%;
border:none;
background-image:none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem;
}
textarea {
height: 100px;
}
}
}
div.conquer-interface-element-padded {
width: calc(100% - 60px);
@ -32,6 +55,9 @@ body {
padding-right: 30px;
display: flex;
justify-content: center;
&.conquer-display-none {
display: none;
}
}
div.create-node-slide {
display: flex;

File diff suppressed because one or more lines are too long

View File

@ -10,11 +10,30 @@
<body>
<div id="conquer-overlay-transparent-template" class="conquer-overlay-transparent conquer-display-none">
</div>
<div id="conquer-new-node-form-creation-template" class="conquer-new-node-form-creation conquer-display-none conquer-interface-element-padded">
<form>
<p class="conquer-error conquer-display-none"></p>
<label>Tipo de Nodo<br/>
<select class="conquer-node-type">
<option value="normal">Normal (Conquistable)</option>
</select>
</label>
<div>
<label>Nuevo Nombre de Nodo<br/>
<input type="text" class="conquer-node-name"/></label>
</div>
<label>Descripción de la Zona<br/>
<textarea class="conquer-node-description"></textarea></label>
<div>
<button class="new-node-form-submit">Finalizar creación de nodo.</button>
</div>
</form>
</div>
<div class="create-node-slide conquer-display-none" id="create-node-slide">
<button id="create-node-new-node">Autogenerado.</button>
<button id="create-node-exit">Salir del modo crear nodo.</button>
</div>
<div id="conquer-interface-element-padded-template" class="conquer-interface-element-padded">
<div id="conquer-interface-element-padded-template" class="conquer-interface-element-padded conquer-display-none">
</div>
<div id="conquer-interface-with-top-bar-template" class="conquer-self-player conquer-display-none">
<div class="conquer-top-bar">

View File

@ -1,5 +1,7 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"outDir": "./public/js/",
"noImplicitAny": true,
"module": "es2020",