Merged 'websocket' branch back into 'default'.

This commit is contained in:
Sascha L. Teichmann 2015-03-04 13:20:51 +01:00
commit d0b0ba83ba
7 changed files with 350 additions and 33 deletions

View File

@ -104,7 +104,8 @@ in the background. To start `mtwebmapper` use:
-map=/path/to/your/map \
-web=/path/to/your/static/web \
-redis-host=localhost \
-workers=2
-workers=2 \
-websockets=false
For the `colors=` options applys the same as said above. You can also add
`-transparent=true` for transparency as mentioned above. The `web-host=` is the interface the
@ -125,6 +126,20 @@ give to much ressources to this if you planning to run the mapping webserver on
same machine as the Minetest server. On the other hand assigning more cores to it definitely
helps to boost up the performance.
Setting the `-websockets=true` flag enables websocket support for the server. With this
feature turned on and changing the line (in `web/index.html`) from
var useWebsocket = false; // Set to true if you want websocket support
to
var useWebsocket = true; // Set to true if you want websocket support
the web client gets an extra 'auto update' button. When switched on the server
informs the client if something in the maps has changed. The displayed map will
then update automatically without the need of manual pressing the 'update view'
button. Of cause your browser needs Websocket support, too.
## Configure and restart the Minetest server
Now everything is in place and the only thing left ist to re-configure the Minetest server
@ -138,4 +153,3 @@ backend with a Redis configuration:
You may have to set `redis_port` too if you run `mtredisalize` not on port 6379.
Now we are all done and you can fire your Minetest server up again. :-)

View File

@ -0,0 +1,112 @@
// Copyright 2014 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"github.com/gorilla/websocket"
)
type websocketForwarder struct {
upgrader *websocket.Upgrader
register chan *connection
unregister chan *connection
broadcast chan map[xz]bool
connections map[*connection]bool
}
type connection struct {
ws *websocket.Conn
send chan []byte
}
func newWebsocketForwarder() *websocketForwarder {
upgrader := &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 2048}
return &websocketForwarder{
upgrader: upgrader,
register: make(chan *connection),
unregister: make(chan *connection),
broadcast: make(chan map[xz]bool),
connections: make(map[*connection]bool)}
}
func (wsf *websocketForwarder) run() {
for {
select {
case c := <-wsf.register:
wsf.connections[c] = true
case c := <-wsf.unregister:
if _, ok := wsf.connections[c]; ok {
delete(wsf.connections, c)
close(c.send)
}
case changes := <-wsf.broadcast:
if len(wsf.connections) == 0 {
continue
}
// Do the JSON encoding this late to save
// some CPU cyles if no client is connected.
xzs := make([]xz, 0, len(changes))
for xz := range changes {
xzs = append(xzs, xz)
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
if err := encoder.Encode(xzs); err != nil {
log.Printf("encoding changes failed: %s\n", err)
continue
}
msg := buf.Bytes()
for c := range wsf.connections {
select {
case c.send <- msg:
default:
delete(wsf.connections, c)
close(c.send)
}
}
}
}
}
func (wsf *websocketForwarder) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
ws, err := wsf.upgrader.Upgrade(rw, r, nil)
if err != nil {
log.Printf("Cannot upgrade to websocket: %s\n", err)
return
}
c := &connection{ws: ws, send: make(chan []byte, 8)}
wsf.register <- c
defer func() { wsf.unregister <- c }()
go c.writer()
c.reader()
}
func (wsf *websocketForwarder) BaseTilesUpdated(changes map[xz]bool) {
wsf.broadcast <- changes
}
func (c *connection) writer() {
defer c.ws.Close()
for msg := range c.send {
if c.ws.WriteMessage(websocket.TextMessage, msg) != nil {
break
}
}
}
func (c *connection) reader() {
defer c.ws.Close()
for {
// Just read the message and ignore it.
if _, _, err := c.ws.NextReader(); err != nil {
break
}
}
}

View File

@ -10,32 +10,12 @@ import (
"log"
"net"
"net/http"
"strings"
"bitbucket.org/s_l_teichmann/mtsatellite/common"
"github.com/gorilla/mux"
)
func ipsFromHosts(hosts string) ([]net.IP, error) {
ips := []net.IP{}
if len(hosts) == 0 { // Empty list: allow all hosts.
return ips, nil
}
for _, host := range strings.Split(hosts, ";") {
if hips, err := net.LookupIP(host); err != nil {
return nil, err
} else {
ips = append(ips, hips...)
}
}
return ips, nil
}
func main() {
var (
webPort int
@ -48,6 +28,7 @@ func main() {
workers int
transparent bool
updateHosts string
websockets bool
)
flag.IntVar(&webPort, "web-port", 8808, "port of the web server")
flag.IntVar(&webPort, "p", 8808, "port of the web server (shorthand)")
@ -70,6 +51,8 @@ func main() {
flag.StringVar(&colorsFile, "c", "colors.txt", "colors used to render map tiles (shorthand).")
flag.BoolVar(&transparent, "transparent", false, "Render transparent blocks.")
flag.BoolVar(&transparent, "t", false, "Render transparent blocks (shorthand).")
flag.BoolVar(&websockets, "websockets", false, "Forward tile changes to clients via websockets.")
flag.BoolVar(&websockets, "ws", false, "Forward tile changes to clients via websockets (shorthand).")
flag.Parse()
@ -78,6 +61,15 @@ func main() {
subBaseLine := newSubBaseLine(mapDir)
router.Path("/map/{z:[0-9]+}/{x:[0-9]+}/{y:[0-9]+}.png").Handler(subBaseLine)
var btu baseTilesUpdates
if websockets {
wsf := newWebsocketForwarder()
go wsf.run()
router.Path("/ws").Methods("GET").Handler(wsf)
btu = wsf
}
if redisHost != "" {
var colors *common.Colors
var err error
@ -92,7 +84,13 @@ func main() {
}
tu := newTileUpdater(
mapDir, redisAddress, allowedUpdateIps, colors, transparent, workers)
mapDir,
redisAddress,
allowedUpdateIps,
colors,
transparent,
workers,
btu)
go tu.doUpdates()
router.Path("/update").Methods("POST").Handler(tu)
}

36
cmd/mtwebmapper/misc.go Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2014 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"net"
"strings"
)
func ipsFromHosts(hosts string) ([]net.IP, error) {
ips := []net.IP{}
if len(hosts) == 0 { // Empty list: allow all hosts.
return ips, nil
}
for _, host := range strings.Split(hosts, ";") {
hips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
ips = append(ips, hips...)
}
return ips, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -24,8 +24,13 @@ import (
"bitbucket.org/s_l_teichmann/mtsatellite/common"
)
type baseTilesUpdates interface {
BaseTilesUpdated(map[xz]bool)
}
type tileUpdater struct {
changes map[xz]bool
btu baseTilesUpdates
mapDir string
redisAddress string
ips []net.IP
@ -67,9 +72,11 @@ func newTileUpdater(
ips []net.IP,
colors *common.Colors,
transparent bool,
workers int) *tileUpdater {
workers int,
btu baseTilesUpdates) *tileUpdater {
tu := tileUpdater{
btu: btu,
mapDir: mapDir,
redisAddress: redisAddress,
ips: ips,
@ -166,7 +173,7 @@ func (tu *tileUpdater) doUpdates() {
parentJobs := make(map[xz]uint16)
for c, _ := range changes {
for c := range changes {
//log.Printf("job: %+v", c)
jobs <- c
pxz := c.parent()
@ -191,14 +198,11 @@ func (tu *tileUpdater) doUpdates() {
done.Wait()
parentJobs = ppJobs
}
}
}
func min(a, b int) int {
if a < b {
return a
if tu.btu != nil {
tu.btu.BaseTilesUpdated(changes)
}
}
return b
}
func updatePyramidTiles(level int, baseDir string, jobs chan xzm, done *sync.WaitGroup) {

View File

@ -37,9 +37,11 @@
<script src="js/leaflet.js"></script>
<script src="js/Leaflet.Coordinates-0.1.4.min.js"></script>
<script src="js/easy-button.js"></script>
<script src="js/auto-update.js"></script>
<script type="text/javascript" src="js/leaflet-hash.js"></script>
<script>
var useWebsocket = false; // Set to true if you want websocket support
L.Projection.NoWrap = {
project: function (latlng) {
@ -93,10 +95,21 @@ L.control.coordinates({
useLatLngOrder: true //ordering of labels, default false-> lng-lat
}).addTo(map);
var manualUpdateControl;
if (useWebsocket && 'WebSocket' in window) {
L.autoUpdate('autoUpdate', function(pressed) {
if (pressed) {
manualUpdateControl.getContainer().style = 'visibility: hidden';
}
else {
manualUpdateControl.getContainer().style = 'visibility: visible';
}
});
}
var layersControl = new L.Control.Layers(rasterMaps, overlayMaps, {collapsed: false});
map.addControl(layersControl);
L.easyButton('fa-refresh',
manualUpdateControl = L.easyButton('fa-refresh',
function (){
var tiles = document.getElementsByTagName("img");
for (var i = 0; i < tiles.length; i++) {
@ -114,7 +127,7 @@ L.easyButton('fa-refresh',
//map._resetView(map.getCenter(), map.getZoom(), false);
},
'Update view'
)
);
var hash = new L.Hash(map)
</script>
</body>

View File

@ -0,0 +1,140 @@
L.Control.AutoUpdate = L.Control.extend({
options: {
position: 'topleft',
label: 'Automatic update'
},
pressed: true,
onAdd: function() {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
this.link = L.DomUtil.create('a', 'leaflet-bar-part', container);
this.iconStart = L.DomUtil.create('i', 'fa fa-play', this.link);
this.link.href = '#';
L.DomEvent.on(this.link, 'click', this.cbClick, this);
return container;
},
cbClick: function (e) {
L.DomEvent.stopPropagation(e);
this.intendedFunction(this.pressed);
if (this.pressed) {
this.pressed = false;
this.iconStart.setAttribute('class', 'fa fa-pause');
this.autoUpdate();
return;
}
if (!this.pressed) {
this.pressed = true;
this.iconStart.setAttribute('class', 'fa fa-play');
this.stopUpdate();
return;
}
},
intendedFunction: function() {
alert('no function selected');
},
stopUpdate: function() {
this.socket.close();
},
autoUpdate: function() {
this.socket = new WebSocket('ws://' + window.location.host + '/ws');
this.socket.onmessage = function(evt) {
var invalidate = function(json) {
var invalidateAll = function(x, y, z) { return true; };
if (!(typeof json === "string")) {
return invalidateAll;
}
var tiles;
try {
tiles = JSON.parse(json);
} catch (err) {
return invalidateAll;
}
var pyramid = new Array(9);
var last = new Object();
pyramid[8] = last;
for (var i = 0; i < tiles.length; i++) {
var xz = tiles[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 (prev.hasOwnProperty(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 level.hasOwnProperty(k);
};
} (evt.data);
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.contains('leaflet-tile-loaded')) {
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();
}
}
};
}
});
L.autoUpdate = function(cbLabel, cbFunc, cbMap) {
var control = new L.Control.AutoUpdate();
if (cbLabel) {
control.options.label = cbLabel;
}
if (cbFunc) {
control.intendedFunction = cbFunc;
}
if (cbMap === '') {
return control;
}
else if (cbMap) {
cbMap.addControl(control);
}
else {
map.addControl(control);
}
return control;
};