mirror of
https://bitbucket.org/s_l_teichmann/mtsatellite
synced 2025-06-30 15:10:55 +02:00
Switched to vue-leaflet implementation for web client.
This commit is contained in:
147
cmd/mtwebmapper/client/src/App.vue
Normal file
147
cmd/mtwebmapper/client/src/App.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<v-app dark>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:clipped="true"
|
||||
fixed
|
||||
app
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-action>
|
||||
<v-checkbox v-model="playersLayer"></v-checkbox>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Players</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-app-bar :clipped-left="true" fixed app>
|
||||
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
|
||||
<v-toolbar-title v-text="title" />
|
||||
<v-spacer />
|
||||
<v-btn icon @click.stop="refreshContent">
|
||||
<v-icon v-if="!autoUpdate">fa-refresh</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click.stop="autoUpdate = !autoUpdate">
|
||||
<v-icon v-if="!autoUpdate">fa-play</v-icon>
|
||||
<v-icon v-else>fa-pause</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<MapView />
|
||||
</v-main>
|
||||
<v-footer :absolute="!fixed" app>
|
||||
<span>© {{ new Date().getFullYear() }}</span>
|
||||
<v-spacer />
|
||||
<span>X: {{coordinates[0]}} - Y: {{coordinates[1]}}</span>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MapView from './components/MapView.vue'
|
||||
import {mapState} from 'vuex';
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
MapView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
fixed: false,
|
||||
layerItems: [
|
||||
{
|
||||
icon: 'mdi-apps',
|
||||
title: 'Points of Interest',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-chart-bubble',
|
||||
title: 'Buildings',
|
||||
},
|
||||
],
|
||||
title: 'Minetest - Map of the Real World',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
players: state => state.players,
|
||||
}),
|
||||
autoUpdate: {
|
||||
get() {
|
||||
return this.$store.state.autoUpdate;
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
this.$store.dispatch("runAutoUpdate")
|
||||
}
|
||||
else {
|
||||
this.$store.dispatch("stopAutoUpdate")
|
||||
}
|
||||
this.$store.commit("setAutoUpdate", value);
|
||||
}
|
||||
},
|
||||
coordinates() {
|
||||
return this.$store.state.coordinates;
|
||||
},
|
||||
playersLayer: {
|
||||
get() {
|
||||
return this.$store.state.playersLayer;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("updatePlayersLayer", value)
|
||||
}
|
||||
},
|
||||
buildingsLayer: {
|
||||
get() {
|
||||
return this.$store.state.buildingsLayer;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("updateBuildingsLayer", value)
|
||||
}
|
||||
},
|
||||
poiLayer: {
|
||||
get() {
|
||||
return this.$store.state.poiLayer;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("updatePoiLayer", value)
|
||||
}
|
||||
},
|
||||
travelnetLayer: {
|
||||
get() {
|
||||
return this.$store.state.travelnetLayer;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("updateTravelnetLayer", value)
|
||||
}
|
||||
},
|
||||
otherLayer: {
|
||||
get() {
|
||||
return this.$store.state.otherLayer;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("updateOtherLayer", value)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
fetch("players").then(response => response.json().then((data) => {
|
||||
this.$store.commit("setPlayers", data);
|
||||
}))
|
||||
},
|
||||
methods: {
|
||||
refreshContent() {
|
||||
this.$store.dispatch("manualUpdate");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-navigation-drawer .v-navigation-drawer--fixed{
|
||||
z-index: 1001;
|
||||
}
|
||||
</style>
|
BIN
cmd/mtwebmapper/client/src/assets/logo.png
Normal file
BIN
cmd/mtwebmapper/client/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
BIN
cmd/mtwebmapper/client/src/assets/marker-player.png
Normal file
BIN
cmd/mtwebmapper/client/src/assets/marker-player.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
84
cmd/mtwebmapper/client/src/components/MapView.vue
Normal file
84
cmd/mtwebmapper/client/src/components/MapView.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-container fluid style="height: 100%; padding: 0px">
|
||||
<l-map @mousemove="update" ref="map" style="height: 100%" :zoom="zoom" :center="center" :worldCopyJump="worldCopyJump" :crs="crs" :options="mapOptions">
|
||||
<l-tile-layer ref="baseLayer" :url="url" :attribution="attribution" :tms="tms" continuousWorld="false" minZoom=0 :noWrap="noWrap" :options="layerOptions"></l-tile-layer>
|
||||
<l-marker v-for="p in players" :key="p.name" :lat-lng="flipCoordinates(p.geometry.coordinates)" :icon="iconPlayer">
|
||||
<l-popup>{{p.properties.name}}</l-popup>
|
||||
</l-marker>
|
||||
</l-map>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import L from 'leaflet';
|
||||
import {mapState} from 'vuex';
|
||||
|
||||
L.Projection.NoWrap = {
|
||||
project(latlng) {
|
||||
return new L.Point(latlng.lat, latlng.lng);
|
||||
},
|
||||
unproject(point) {
|
||||
return new L.LatLng(point.x, point.y, true);
|
||||
},
|
||||
bounds: L.bounds(L.point(-30928.0,30928.0),L.point(30928.0, -30928.0))
|
||||
};
|
||||
const ownCRS = L.CRS.Direct = L.Util.extend({}, L.CRS, {
|
||||
code: 'Direct',
|
||||
projection: L.Projection.NoWrap,
|
||||
transformation: new L.Transformation(1.0/65536, 30928.0/65536, -1.0/65536, 34608.0/65536)
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'MapView',
|
||||
data: () => {
|
||||
return {
|
||||
mapOptions: {
|
||||
fadeAnimation: false
|
||||
},
|
||||
layerOptions: {
|
||||
unloadInvisibleTiles: true
|
||||
},
|
||||
socket: null,
|
||||
url: 'map/{z}/{x}/{y}.png',
|
||||
zoom: 3,
|
||||
noWrap: true,
|
||||
center: [0,0],
|
||||
worldCopyJump: false,
|
||||
crs: ownCRS,
|
||||
tms: true,
|
||||
attribution: 'Demo World',
|
||||
features: [],
|
||||
iconPlayer: new L.Icon({
|
||||
iconUrl: require('@/assets/marker-player.png'),
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
}),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
players() {
|
||||
return this.$store.state.players;
|
||||
},
|
||||
...mapState({
|
||||
autoUpdate: state => state.autoUpdate,
|
||||
playersLayer: state => state.playersLayer,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
update(evt) {
|
||||
this.$store.commit('updateCoordinates', [evt.latlng.lat, evt.latlng.lng])
|
||||
},
|
||||
flipCoordinates(pos) {
|
||||
return [pos[1], pos[0]];
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
color: #f5f5f5;
|
||||
background-color: #424242;
|
||||
}
|
||||
</style>
|
14
cmd/mtwebmapper/client/src/main.js
Normal file
14
cmd/mtwebmapper/client/src/main.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import './plugins/leaflet'
|
||||
import '@babel/polyfill'
|
||||
import store from './store'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
vuetify,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
12
cmd/mtwebmapper/client/src/plugins/leaflet.js
Normal file
12
cmd/mtwebmapper/client/src/plugins/leaflet.js
Normal file
@ -0,0 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import { LMap, LTileLayer, LMarker, LPopup } from 'vue2-leaflet';
|
||||
import Vue2LeafletMarkerCluster from 'vue2-leaflet-markercluster'
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||
|
||||
Vue.component('l-map', LMap);
|
||||
Vue.component('l-tile-layer', LTileLayer);
|
||||
Vue.component('l-marker', LMarker);
|
||||
Vue.component('l-popup', LPopup);
|
||||
Vue.component('l-marker-cluster', Vue2LeafletMarkerCluster);
|
27
cmd/mtwebmapper/client/src/plugins/vuetify.js
Normal file
27
cmd/mtwebmapper/client/src/plugins/vuetify.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify';
|
||||
import 'vuetify/dist/vuetify.min.css';
|
||||
import colors from 'vuetify/es5/util/colors'
|
||||
import '@fortawesome/fontawesome-free/css/all.css'
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'fa',
|
||||
},
|
||||
theme: {
|
||||
dark: true,
|
||||
themes: {
|
||||
dark: {
|
||||
primary: colors.blue.darken2,
|
||||
accent: colors.grey.darken3,
|
||||
secondary: colors.amber.darken3,
|
||||
info: colors.teal.lighten1,
|
||||
warning: colors.amber.base,
|
||||
error: colors.deepOrange.accent4,
|
||||
success: colors.green.accent3,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
189
cmd/mtwebmapper/client/src/store/index.js
Normal file
189
cmd/mtwebmapper/client/src/store/index.js
Normal file
@ -0,0 +1,189 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
socket: null,
|
||||
autoUpdate: false,
|
||||
coordinates: [0, 0],
|
||||
playersLayer: true,
|
||||
buildingsLayer: true,
|
||||
poiLayer: true,
|
||||
travelnetLayer: true,
|
||||
otherLayer: true,
|
||||
players: []
|
||||
},
|
||||
|
||||
mutations: {
|
||||
updateCoordinates(state, value) {
|
||||
state.coordinates = value;
|
||||
},
|
||||
setAutoUpdate(state, value) {
|
||||
state.autoUpdate = value
|
||||
},
|
||||
updatePlayersLayer(state, value) {
|
||||
state.playersLayer = value;
|
||||
},
|
||||
updateBuildingsLayer(state, value) {
|
||||
state.buildingsLayer = value;
|
||||
},
|
||||
updatePoiLayer(state, value) {
|
||||
state.poiLayer = value;
|
||||
},
|
||||
updateTravelnetLayer(state, value) {
|
||||
state.travelnetLayer = value;
|
||||
},
|
||||
updateOtherLayer(state, value) {
|
||||
state.otherLayer = value;
|
||||
},
|
||||
setPlayers(state, value) {
|
||||
state.players = value;
|
||||
},
|
||||
setPlayerState(state, value) {
|
||||
const p = state.players.find(p => p.name === value.name)
|
||||
p.online = value.state;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
stopAutoUpdate: function({ state }) {
|
||||
if (this.socket) {
|
||||
var s = state.socket;
|
||||
state.socket = null;
|
||||
s.close();
|
||||
}
|
||||
},
|
||||
|
||||
manualUpdate: function({commit}) {
|
||||
var tiles = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < tiles.length; i++) {
|
||||
var img = tiles[i];
|
||||
var cl = img.getAttribute("class");
|
||||
if (cl.indexOf("leaflet-tile-loaded") >= 0) {
|
||||
var src = img.src;
|
||||
var idx = src.lastIndexOf("#");
|
||||
if (idx >= 0) {
|
||||
src = src.substring(0, idx);
|
||||
}
|
||||
img.src = src + "#" + Math.random();
|
||||
}
|
||||
}
|
||||
fetch("players").then(response => response.json().then((data) => {
|
||||
commit("setPlayers", data);
|
||||
}));
|
||||
},
|
||||
|
||||
runAutoUpdate: function({ commit, state, dispatch}) {
|
||||
var me = this;
|
||||
// TODO: Make the URL to websocket configurable via .env
|
||||
state.socket = new WebSocket('ws://' + window.location.host + '/socket');
|
||||
|
||||
state.socket.onerror = function() {
|
||||
commit('setAutoUpdate', false)
|
||||
dispatch('stopAutoUpdate');
|
||||
};
|
||||
|
||||
state.socket.onclose = function() {
|
||||
commit('setAutoUpdate', false)
|
||||
state.socket = null;
|
||||
}
|
||||
|
||||
state.socket.onopen = function() {
|
||||
// Sending pings every 5 secs to keep connection alive.
|
||||
var heartbeat = function() {
|
||||
if (heartbeat && me.socket) {
|
||||
me.socket.send("PING");
|
||||
setTimeout(heartbeat, 8000);
|
||||
} else {
|
||||
// Prevent sending pings to re-opened sockets.
|
||||
heartbeat = null;
|
||||
}
|
||||
};
|
||||
setTimeout(heartbeat, 8000);
|
||||
};
|
||||
|
||||
state.socket.onmessage = function(evt) {
|
||||
var json = evt.data;
|
||||
if (!(typeof json === "string")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var msg;
|
||||
try {
|
||||
msg = JSON.parse(json);
|
||||
}
|
||||
catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.players) {
|
||||
commit('setPlayers', msg.players);
|
||||
}
|
||||
|
||||
var tilesData = msg.tiles;
|
||||
if (!tilesData) {
|
||||
return;
|
||||
}
|
||||
|
||||
var invalidate = function(td) {
|
||||
var pyramid = new Array(9);
|
||||
var last = new Object();
|
||||
pyramid[8] = last;
|
||||
|
||||
for (var i = 0; i < td.length; i++) {
|
||||
var xz = td[i];
|
||||
last[xz.X + "#" + xz.Z] = xz;
|
||||
}
|
||||
for (var p = 7; p >= 0; p--) {
|
||||
var prev = pyramid[p+1];
|
||||
var curr = new Object();
|
||||
pyramid[p] = curr;
|
||||
for (var k in prev) {
|
||||
if (Object.prototype.hasOwnProperty.call(prev, k)) {
|
||||
var oxz = prev[k];
|
||||
var nxz = { X: oxz.X >> 1, Z: oxz.Z >> 1 };
|
||||
curr[nxz.X + "#" + nxz.Z] = nxz;
|
||||
}
|
||||
}
|
||||
}
|
||||
return function(x, y, z) {
|
||||
if (y > 8) {
|
||||
x >>= y - 8;
|
||||
z >>= y - 8;
|
||||
y = 8;
|
||||
}
|
||||
var level = pyramid[y];
|
||||
var k = x + "#" + z;
|
||||
return Object.prototype.hasOwnProperty.call(level, k);
|
||||
};
|
||||
} (tilesData);
|
||||
var tiles = document.getElementsByTagName('img');
|
||||
var re = /\/map\/([0-9]+)\/([0-9]+)\/([0-9]+).*/;
|
||||
for (var i = 0; i < tiles.length; i++) {
|
||||
var img = tiles[i];
|
||||
var cl = img.getAttribute('class');
|
||||
if (cl.indexOf('leaflet-tile-loaded') < 0) {
|
||||
continue;
|
||||
}
|
||||
var src = img.src;
|
||||
var coord = src.match(re);
|
||||
if (coord == null) {
|
||||
continue;
|
||||
}
|
||||
var y = parseInt(coord[1]);
|
||||
var x = parseInt(coord[2]);
|
||||
var z = parseInt(coord[3]);
|
||||
if (invalidate(x, y, z)) {
|
||||
var idx = src.lastIndexOf('#');
|
||||
if (idx >= 0) {
|
||||
src = src.substring(0, idx);
|
||||
}
|
||||
img.src = src + '#' + Math.random();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user