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 NewTeamUI from '@burguillosinfo/conquer/interface/new-team' import WebSocket from '@burguillosinfo/conquer/websocket' import JsonSerializer from '@burguillosinfo/conquer/serializer'; import ConquerUser from '@burguillosinfo/conquer/user' type StylesInterface = Record 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 | 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 = {} private coordinate_1 = 0; private coordinate_2 = 0; public getServerNodes(): Record { 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 newNodeUI = new NewNodeUI(coordinates) const oldState = this.getState(); newNodeUI.on('close', () => { this.interfaceManager.remove(newNodeUI) this.setState(oldState); }) this.interfaceManager.push(newNodeUI) this.removeState(MapState.SELECT_WHERE_TO_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.refreshLayers() } private isStateFillingFormCreateNode(): boolean { return !!(this.getState() & MapState.FILLING_FORM_CREATE_NODE) } async onClickMap(event: MapEvent): Promise { 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 { if (!(this.state & MapState.NORMAL)) { return } const selfPlayerUI = new SelfPlayerUI(!!(this.getState() & (MapState.FREE_MOVE | MapState.FREE_ROTATION))) selfPlayerUI.on('close', () => { this.interfaceManager.remove(selfPlayerUI) }) selfPlayerUI.on('enable-explorer-mode', () => { this.addState(MapState.FREE_MOVE); this.addState(MapState.FREE_ROTATION); }); selfPlayerUI.on('disable-explorer-mode', () => { this.removeState(MapState.FREE_MOVE); this.removeState(MapState.FREE_ROTATION); }); selfPlayerUI.on('createNodeStart', () => { this.addState(MapState.CREATE_NODE) this.removeState(MapState.NORMAL) }) selfPlayerUI.on('open-create-team', () => { this.onOpenCreateTeam(); }); this.interfaceManager.push(selfPlayerUI) this.selfPlayerUI = selfPlayerUI } private onOpenCreateTeam(): void { const newTeamUI = new NewTeamUI(); newTeamUI.on('close', () => { this.interfaceManager.remove(newTeamUI); }); this.interfaceManager.push(newTeamUI); } private isFeatureEnabledMap: Record = {} async onClickFeature(feature: Feature): Promise { 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); const candidateNode = this.getServerNodes()[feature.getProperties().type]; if (candidateNode !== undefined) { candidateNode.click(this.interfaceManager); return; } if (feature === this.currentPositionFeature) { this.onClickSelf() return } } async onLoginSuccess(): Promise { 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 async getNearbyNodes(): Promise { const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port) let response; try { response = await fetch(urlNodes); } catch (error) { console.error(error); return; } 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 = {}; 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; } node.on('update-nodes', async () => { await this.sendCoordinatesToServer(); this.getNearbyNodes(); }); serverNodes[node.getId()] = node; } this.serverNodes = serverNodes; this.refreshLayers(); } private createIntervalPollNearbyNodes(): void { this.getNearbyNodes(); this.intervalPollNearbyNodes = window.setInterval(() => { this.getNearbyNodes(); }, 40000) } private createIntervalSendCoordinates(): void { this.intervalSendCoordinates = window.setInterval(() => { this.sendCoordinatesToServer(); }, 40000); } private async sendCoordinatesToServer(): Promise { const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port) let res; try { res = await fetch(urlLog, { method: 'POST', body: JSON.stringify([ this.coordinate_1, this.coordinate_2, ])}); } catch (error) { console.error(error) return; } 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); } } 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 { if (this.currentPositionFeature === null) { return } const user = await ConquerUser.getSelfUser() let color = 'white'; if (user !== null) { const team = await user.getTeam(); if (team !== null) { color = team.getColor(); } } const styles: StylesInterface = { currentPositionFeature: new Style({ image: new CircleStyle({ radius: 14, fill: new Fill({color: color}), stroke: new Stroke({ color: 'beige', width: 2, }) }) }) }; const features = [this.currentPositionFeature] for (const key in this.getServerNodes()) { styles[key] = await this.getServerNodes()[key].getStyle() features.push(this.getServerNodes()[key].getFeature()) } const vectorLayer = new VectorLayer({ 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 } }