514 lines
18 KiB
TypeScript
514 lines
18 KiB
TypeScript
import Map from "ol/Map"
|
|
import MapEvent from "ol/MapEvent"
|
|
import MapBrowserEvent from "ol/MapBrowserEvent"
|
|
import View from "ol/View"
|
|
import Projection from "ol/proj/Projection.js"
|
|
import TileLayer from 'ol/layer/Tile'
|
|
import OSM from 'ol/source/OSM'
|
|
import * as olProj from "ol/proj"
|
|
import Feature from 'ol/Feature'
|
|
import Point from 'ol/geom/Point'
|
|
import VectorLayer from 'ol/layer/Vector'
|
|
import VectorSource from 'ol/source/Vector'
|
|
import Stroke from 'ol/style/Stroke'
|
|
import Fill from 'ol/style/Fill'
|
|
import CircleStyle from 'ol/style/Circle'
|
|
import Style from 'ol/style/Style'
|
|
import ConquerLogin from '@burguillosinfo/conquer/login'
|
|
import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
|
|
import SelfPlayerUI from '@burguillosinfo/conquer/interface/self-player'
|
|
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>
|
|
|
|
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
|
|
private disableSetRotationOffset = false
|
|
private currentPositionFeature: Feature | null
|
|
private vectorLayer: VectorLayer<VectorSource> | null = null
|
|
private alpha = 0
|
|
private beta = 0
|
|
private gamma = 0
|
|
private conquerLogin: ConquerLogin
|
|
private selfPlayerUI: SelfPlayerUI | null = null
|
|
private interfaceManager: InterfaceManager
|
|
private firstSetCenter = true
|
|
private firstSetRotation = true
|
|
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
|
|
}
|
|
public getState(): MapState {
|
|
return this.state
|
|
}
|
|
|
|
public setState(state: MapState) {
|
|
this.state = state
|
|
this.refreshState()
|
|
}
|
|
public removeState(state: MapState) {
|
|
this.state &= ~state
|
|
this.refreshState()
|
|
}
|
|
public addState(state: MapState) {
|
|
this.state |= state
|
|
this.refreshState()
|
|
}
|
|
private refreshState(): void {
|
|
this.createNodeObject.refreshState()
|
|
return
|
|
}
|
|
|
|
static start() {
|
|
const conquerContainer = document.querySelector(".conquer-container")
|
|
if (conquerContainer === null || !(conquerContainer instanceof HTMLDivElement)) {
|
|
Conquer.fail('.conquer-container is not a div.')
|
|
}
|
|
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) {
|
|
return
|
|
}
|
|
this.map.getView().centerOn(olCoordinates, size, [size[0]/2, size[1]-60])
|
|
this.firstSetCenter = false
|
|
}
|
|
}
|
|
static fail(error: string): never {
|
|
alert('Error de interfaz')
|
|
throw new Error(error)
|
|
}
|
|
|
|
public isStateCreatingNode(): boolean {
|
|
return !!(this.getState() & MapState.CREATE_NODE)
|
|
}
|
|
public isStateSelectWhereToCreateNode(): boolean {
|
|
return !!(this.getState() & MapState.SELECT_WHERE_TO_CREATE_NODE)
|
|
}
|
|
|
|
private createNodeCounter = 0
|
|
|
|
async onClickWhereToCreateNode(event: MapEvent) {
|
|
if (!(event instanceof MapBrowserEvent)) {
|
|
return
|
|
}
|
|
const pixel = event.pixel
|
|
const coordinates = this.map.getCoordinateFromPixel(pixel)
|
|
const feature = new Feature({
|
|
geometry: new Point(coordinates)
|
|
})
|
|
const newNodeUI = new NewNodeUI(coordinates)
|
|
const oldState = this.getState();
|
|
newNodeUI.on('close', () => {
|
|
this.interfaceManager.remove(newNodeUI)
|
|
this.setState(oldState);
|
|
})
|
|
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)
|
|
}
|
|
if (!(this.getState() & MapState.NORMAL)) {
|
|
return
|
|
}
|
|
if (this.vectorLayer === null) {
|
|
return
|
|
}
|
|
if (!(event instanceof MapBrowserEvent)) {
|
|
return
|
|
}
|
|
if (event.dragging) {
|
|
return
|
|
}
|
|
const pixel = event.pixel
|
|
const features = this.map.getFeaturesAtPixel(pixel)
|
|
const feature = features.length ? features[0] : undefined
|
|
if (feature === undefined) {
|
|
return
|
|
}
|
|
if (!(feature instanceof Feature)) {
|
|
return
|
|
}
|
|
this.onClickFeature(feature)
|
|
}
|
|
|
|
async onClickSelf(): Promise<void> {
|
|
if (!(this.state & MapState.NORMAL)) {
|
|
return
|
|
}
|
|
const selfPlayerUI = new SelfPlayerUI()
|
|
selfPlayerUI.on('close', () => {
|
|
this.interfaceManager.remove(selfPlayerUI)
|
|
})
|
|
selfPlayerUI.on('createNodeStart', () => {
|
|
this.addState(MapState.CREATE_NODE)
|
|
this.removeState(MapState.NORMAL)
|
|
})
|
|
this.interfaceManager.push(selfPlayerUI)
|
|
this.selfPlayerUI = selfPlayerUI
|
|
}
|
|
|
|
private isFeatureEnabledMap: Record<string, boolean> = {}
|
|
|
|
async onClickFeature(feature: Feature): Promise<void> {
|
|
if (this.isFeatureEnabledMap[feature.getProperties().type] === undefined) {
|
|
this.isFeatureEnabledMap[feature.getProperties().type] = true
|
|
}
|
|
if (!this.isFeatureEnabledMap[feature.getProperties().type]) {
|
|
return
|
|
}
|
|
this.isFeatureEnabledMap[feature.getProperties().type] = false
|
|
window.setTimeout(() => {
|
|
this.isFeatureEnabledMap[feature.getProperties().type] = true
|
|
}, 100);
|
|
if (feature === this.currentPositionFeature) {
|
|
this.onClickSelf()
|
|
return
|
|
}
|
|
}
|
|
|
|
async onLoginSuccess(): Promise<void> {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 {
|
|
const createNodeObject = new CreateNode(this)
|
|
this.createNodeObject = createNodeObject
|
|
const interfaceManager = new InterfaceManager()
|
|
this.interfaceManager = interfaceManager
|
|
const conquerLogin = new ConquerLogin(interfaceManager)
|
|
conquerLogin.on('login', () => {
|
|
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)
|
|
const conquerContainer = this.conquerContainer
|
|
//layer.on('prerender', (evt) => {
|
|
// // return
|
|
// if (evt.context) {
|
|
// const context = evt.context as CanvasRenderingContext2D
|
|
// context.filter = 'grayscale(80%) invert(100%) '
|
|
// context.globalCompositeOperation = 'source-over'
|
|
// }
|
|
//})
|
|
|
|
//layer.on('postrender', (evt) => {
|
|
// if (evt.context) {
|
|
// const context = evt.context as CanvasRenderingContext2D
|
|
// context.filter = 'none'
|
|
// }
|
|
//})
|
|
olProj.useGeographic()
|
|
const osm = new OSM()
|
|
osm.setUrls([`${window.location.protocol}//${window.location.hostname}:${window.location.port}/conquer/tile/{z}/{x}/{y}.png`])
|
|
this.map = new Map({
|
|
target: conquerContainer,
|
|
layers: [
|
|
new TileLayer({
|
|
source: osm
|
|
})
|
|
],
|
|
view: new View({
|
|
zoom: 19,
|
|
maxZoom: 22,
|
|
}),
|
|
})
|
|
this.setLocationChangeTriggers()
|
|
this.setRotationChangeTriggers()
|
|
}
|
|
|
|
setRotationChangeTriggers(): void {
|
|
if (window.DeviceOrientationEvent) {
|
|
window.addEventListener("deviceorientation", (event) => {
|
|
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
|
|
this.onRotate(event.alpha, event.beta, event.gamma)
|
|
}
|
|
}, true)
|
|
}
|
|
}
|
|
|
|
addCurrentLocationMarkerToMap(currentLatitude: number,
|
|
currentLongitude: number) {
|
|
const currentPositionFeature = new Feature({
|
|
type: 'currentPositionFeature',
|
|
geometry: new Point(this.realCoordinatesToOl(currentLatitude, currentLongitude))
|
|
})
|
|
this.currentPositionFeature = currentPositionFeature
|
|
}
|
|
|
|
processLocation(location: GeolocationPosition) {
|
|
this.currentLatitude = location.coords.latitude
|
|
this.currentLongitude = location. coords.longitude
|
|
if (location.coords.heading !== null && (this.alpha != 0 || this.beta != 0 || this.gamma != 0) && !this.disableSetRotationOffset) {
|
|
this.disableSetRotationOffset = true
|
|
this.heading = location.coords.heading
|
|
this.rotationOffset = this.compassHeading(this.alpha, this.beta, this.gamma) + (location.coords.heading*Math.PI*2)/360
|
|
}
|
|
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
|
|
this.addCurrentLocationMarkerToMap(this.currentLatitude, this.currentLongitude)
|
|
this.refreshLayers()
|
|
}
|
|
|
|
private async refreshLayers(): Promise<void> {
|
|
if (this.currentPositionFeature === null) {
|
|
return
|
|
}
|
|
const styles: StylesInterface = {
|
|
currentPositionFeature: new Style({
|
|
image: new CircleStyle({
|
|
radius: 14,
|
|
fill: new Fill({color: 'white'}),
|
|
stroke: new Stroke({
|
|
color: 'blue',
|
|
width: 2,
|
|
})
|
|
})
|
|
})
|
|
};
|
|
const features = [this.currentPositionFeature]
|
|
for (const key in this.getServerNodes()) {
|
|
styles[key] = this.getServerNodes()[key].getStyle()
|
|
features.push(this.getServerNodes()[key].getFeature())
|
|
}
|
|
const vectorLayer = new VectorLayer<VectorSource>({
|
|
source: new VectorSource({
|
|
features: features
|
|
}),
|
|
})
|
|
if (this.vectorLayer !== null) {
|
|
this.map.removeLayer(this.vectorLayer)
|
|
this.vectorLayer = null;
|
|
}
|
|
vectorLayer.setStyle((feature) => {
|
|
return styles[feature.getProperties().type]
|
|
})
|
|
this.map.addLayer(vectorLayer)
|
|
this.vectorLayer = vectorLayer
|
|
this.map.on('click', (event: MapEvent) => {
|
|
this.onClickMap(event)
|
|
})
|
|
}
|
|
setLocationChangeTriggers(): void {
|
|
window.setInterval(() => {
|
|
this.disableSetRotationOffset = false
|
|
}, 10000)
|
|
this.currentPositionFeature = null
|
|
window.setTimeout(() => {
|
|
window.setInterval(() => {
|
|
navigator.geolocation.getCurrentPosition((location) => {
|
|
this.processLocation(location)
|
|
}, () => {
|
|
return
|
|
}, {
|
|
enableHighAccuracy: true,
|
|
})
|
|
}, 3000)
|
|
}, 1000)
|
|
// const initialLatitude = 37.58237
|
|
//const initialLongitude = -5.96766
|
|
const initialLongitude = 2.500845037550267
|
|
const initialLatitude = 48.81050698635832
|
|
|
|
this.setCenterDisplaced(initialLatitude, initialLongitude)
|
|
this.addCurrentLocationMarkerToMap(initialLatitude, initialLongitude)
|
|
this.refreshLayers()
|
|
navigator.geolocation.watchPosition((location) => {
|
|
this.processLocation(location)
|
|
}, (err) => {
|
|
return
|
|
}, {
|
|
enableHighAccuracy: true,
|
|
})
|
|
}
|
|
realCoordinatesToOl(lat: number, lon: number): number[] {
|
|
return olProj.transform(
|
|
[lon, lat],
|
|
new Projection({ code: "WGS84" }),
|
|
new Projection({ code: "EPSG:900913" }),
|
|
)
|
|
}
|
|
compassHeading(alpha:number, beta:number, gamma:number): number {
|
|
const alphaRad = alpha * (Math.PI / 180)
|
|
return alphaRad
|
|
}
|
|
|
|
logToServer(logValue: string) {
|
|
const urlLog = new URL('/conquer/log', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
|
|
urlLog.searchParams.append('log', logValue)
|
|
fetch(urlLog).then(() => {
|
|
return
|
|
}).catch((error) => {
|
|
console.error(error)
|
|
})
|
|
}
|
|
enabledOnRotate = true
|
|
onRotate(alpha: number, beta: number, gamma: number) {
|
|
if (this.enabledOnRotate) {
|
|
this.alpha = alpha
|
|
this.beta = beta
|
|
this.gamma = gamma
|
|
this.enabledOnRotate = false
|
|
if (this.firstSetRotation || !(this.state & MapState.FREE_ROTATION)) {
|
|
this.map.getView().setRotation((this.compassHeading(alpha, beta, gamma) - this.rotationOffset))
|
|
this.firstSetRotation = false
|
|
}
|
|
|
|
|
|
window.setTimeout(() => {
|
|
this.enabledOnRotate = true
|
|
}, 10)
|
|
}
|
|
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
|
|
}
|
|
constructor(conquerContainer: HTMLDivElement) {
|
|
this.conquerContainer = conquerContainer
|
|
}
|
|
}
|