worldmap v2 poc

This commit is contained in:
Thibault Duplessis 2015-12-23 20:54:48 +07:00
parent 97272e2fd5
commit 88efb2222e
15 changed files with 159 additions and 360 deletions

View file

@ -79,6 +79,7 @@ final class Env(
Env.video,
Env.shutup, // required to load the actor
Env.insight, // required to load the actor
Env.worldMap, // required to load the actor
Env.push // required to load the actor
)
play.api.Logger("boot").info("Preloading complete")

View file

@ -13,9 +13,11 @@ object WorldMap extends LilaController {
Ok(views.html.site.worldMap())
}
def stream = Action {
Ok.chunked(
Env.worldMap.stream.producer &> EventSource()
) as "text/event-stream"
def stream = Action.async {
Env.worldMap.getStream map { stream =>
Ok.chunked(
stream &> EventSource()
) as "text/event-stream"
}
}
}

View file

@ -12,22 +12,9 @@
<body>
<h1><a href="http://lichess.org">lichess<span class="extension">.org</span></a> network</h1>
<div id="worldmap"></div>
<div id="stats">
<div class="wrapL">
<div id="time">time: <span></span></div>
<div id="moves">moves: <span></span></div>
<div id="countries">countries: <span></span></div>
</div>
<div class="wrapL">
<div id="topCountries"></div>
</div>
</div>
@jQueryTag
@jsAt("worldMap/MIDIUtils.js")
@jsAt("worldMap/raphael-min.js")
@jsAt("worldMap/world.js")
@jsAt("worldMap/time.js")
@jsAt("worldMap/app.js")
@jsAt("worldMap/stats.js")
</body>
</html>

View file

@ -268,7 +268,6 @@ playban {
}
worldMap {
geoip = ${geoip}
players.cache_size = 8192
}
push {
collection.device = push_device

View file

@ -13,8 +13,6 @@ trait ActorMap extends Actor {
def mkActor(id: String): Actor
def withEvents = false
def actorMapReceive: Receive = {
case Get(id) => sender ! getOrMake(id)
@ -32,9 +30,7 @@ trait ActorMap extends Actor {
case Terminated(actor) =>
context unwatch actor
actors foreach {
case (id, a) if (a == actor) =>
actors -= id
if (withEvents) self ! ActorMap.Remove(id, a)
case (id, a) if (a == actor) => actors -= id
}
}
@ -44,16 +40,12 @@ trait ActorMap extends Actor {
context.actorOf(Props(mkActor(id)), name = id) ~ { actor =>
actors += (id -> actor)
context watch actor
if (withEvents) self ! ActorMap.Add(id, actor)
}
}
}
object ActorMap {
case class Add(id: String, actor: ActorRef)
case class Remove(id: String, actor: ActorRef)
def apply(make: String => Actor) = new ActorMap {
def mkActor(id: String) = make(id)
def receive = actorMapReceive

View file

@ -3,7 +3,6 @@ package actorApi
import lila.common.LightUser
import akka.actor.ActorRef
import play.api.libs.json._
import play.twirl.api.Html
@ -194,9 +193,11 @@ case class NbRounds(nb: Int)
case class Abort(gameId: String, byColor: String)
case class Berserk(gameId: String, userId: String)
case class IsOnGame(color: chess.Color)
sealed trait DoorEvent
case class Open(gameId: String) extends DoorEvent
case class Close(gameId: String) extends DoorEvent
sealed trait SocketEvent
object SocketEvent {
case class OwnerJoin(gameId: String, color: chess.Color, ip: String) extends SocketEvent
case class Stop(gameId: String) extends SocketEvent
}
}
package evaluation {

View file

@ -54,7 +54,6 @@ final class Env(
lazy val eventHistory = History(db(CollectionHistory)) _
val roundMap = system.actorOf(Props(new lila.hub.ActorMap {
override val withEvents = true
def mkActor(id: String) = new Round(
gameId = id,
messenger = messenger,
@ -72,10 +71,6 @@ final class Env(
case actorApi.GetNbRounds =>
nbRounds = size
hub.socket.lobby ! lila.hub.actorApi.round.NbRounds(nbRounds)
case lila.hub.ActorMap.Add(id, _) =>
system.lilaBus.publish(lila.hub.actorApi.round.Open(id), 'roundDoor)
case lila.hub.ActorMap.Remove(id, _) =>
system.lilaBus.publish(lila.hub.actorApi.round.Close(id), 'roundDoor)
}: Receive) orElse actorMapReceive
}), name = ActorMapName)

View file

@ -78,6 +78,7 @@ private[round] final class Socket(
override def postStop() {
super.postStop()
lilaBus.unsubscribe(self)
lilaBus.publish(lila.hub.actorApi.round.SocketEvent.Stop(gameId), 'roundDoor)
}
private def refreshSubscriptions {
@ -146,6 +147,9 @@ private[round] final class Socket(
playerDo(color, _.ping)
sender ! Connected(enumerator, member)
if (member.userTv.isDefined) refreshSubscriptions
if (member.owner) lilaBus.publish(
lila.hub.actorApi.round.SocketEvent.OwnerJoin(gameId, color, ip),
'roundDoor)
case Nil =>
case eventList: EventList => notify(eventList.events)

View file

@ -2,6 +2,7 @@ package lila.worldMap
import com.typesafe.config.Config
import akka.actor._
import com.sanoma.cda.geoip.MaxMindIpGeo
import lila.common.PimpedConfig._
@ -11,15 +12,19 @@ final class Env(
private val GeoIPFile = config getString "geoip.file"
private val GeoIPCacheTtl = config duration "geoip.cache_ttl"
private val PlayersCacheSize = config getInt "players.cache_size"
lazy val players = new Players(PlayersCacheSize)
private val stream = system.actorOf(
Props(new Stream(
geoIp = MaxMindIpGeo(GeoIPFile, 0),
geoIpCacheTtl = GeoIPCacheTtl)))
lazy val stream = new Stream(
system = system,
players = players,
geoIp = MaxMindIpGeo(GeoIPFile, 0),
geoIpCacheTtl = GeoIPCacheTtl)
def getStream = {
import play.api.libs.iteratee._
import play.api.libs.json._
import akka.pattern.ask
import makeTimeout.short
stream ? Stream.Get mapTo manifest[Enumerator[JsValue]]
}
}
object Env {

View file

@ -1,37 +0,0 @@
package lila.worldMap
import lila.memo.Builder
private[worldMap] final class Players(cacheSize: Int) {
// to each game ID, associate list of player Locations
// there can be 0, 1 or 2 players per game ID,
// but this constraint is not expressed by the cache type :(
private val cache = Builder.size[String, List[Location]](cacheSize)
def getOpponentLocation(gameId: String, myLocation: Location): Option[Location] =
Option(cache getIfPresent gameId) getOrElse Nil match {
// new game ID, store player location
case Nil =>
cache.put(gameId, List(myLocation)); None
// only my location is known
case List(loc) if loc == myLocation => None
// only opponent location is known. Store mine
case List(loc) =>
cache.put(gameId, List(loc, myLocation)); Some(loc)
// both locations are known
case List(l1, l2) if l1 == myLocation => Some(l2)
// both locations are known
case List(l1, l2) if l2 == myLocation => Some(l1)
// both are unknown
case _ => None
}
}

File diff suppressed because one or more lines are too long

View file

@ -81,7 +81,7 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, RM, PRM)
)
lazy val worldMap = project("worldMap", Seq(common, hub, memo)).settings(
lazy val worldMap = project("worldMap", Seq(common, hub, memo, rating)).settings(
libraryDependencies ++= provided(play.api, maxmind)
)

View file

@ -1,64 +0,0 @@
(function() {
var noteMap = {};
var noteNumberMap = [];
var notes = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ];
for(var i = 0; i < 127; i++) {
var index = i,
key = notes[index % 12],
octave = ((index / 12) | 0) - 1; // MIDI scale starts at octave = -1
if(key.length === 1) {
key = key + '-';
}
key += octave;
noteMap[key] = i;
noteNumberMap[i] = key;
}
function getBaseLog(value, base) {
return Math.log(value) / Math.log(base);
}
var MIDIUtils = {
noteNameToNoteNumber: function(name) {
return noteMap[name];
},
noteNumberToFrequency: function(note) {
return 440.0 * Math.pow(2, (note - 69.0) / 12.0);
},
noteNumberToName: function(note) {
return noteNumberMap[note];
},
frequencyToNoteNumber: function(f) {
return Math.round(12.0 * getBaseLog(f / 440.0, 2) + 69);
}
};
// Make it compatible for require.js/AMD loader(s)
if(typeof define === 'function' && define.amd) {
define(function() { return MIDIUtils; });
} else if(typeof module !== 'undefined' && module.exports) {
// And for npm/node.js
module.exports = MIDIUtils;
} else {
this.MIDIUtils = MIDIUtils;
}
}).call(this);

View file

@ -1,67 +1,3 @@
var stats = {
nMoves: 0,
countries: {}
};
var context, gain_node, filter, oscillator, sound = {};
var noteIt = 0;
sound.stop = function() {
try {
oscillator.disconnect();
} catch (e) {}
};
sound.play = function(freq) {
oscillator = context.createOscillator();
oscillator.type = 'sawtooth';
oscillator.connect(filter);
oscillator.frequency.value = freq;
oscillator.start(0);
};
(function init(g) {
try {
context = new(g.AudioContext || g.webkitAudioContext);
gain_node = context.createGain();
gain_node.connect(context.destination);
gain_node.gain.value = 0.1;
filter = context.createBiquadFilter();
filter.type = 'lowpass';
filter.connect(gain_node);
} catch (e) {
console.log('No web audio oscillator support in this browser');
}
}(window));
var soundEnabled = false;
var prevMoves;
var maxPrevMoves = 10;
setInterval(function() {
if (!soundEnabled) return;
sound.stop();
if (stats.nMoves > 0) {
noteIt++;
if (prevMoves) {
var note = Math.min(
11 + 3 * (stats.nMoves - prevMoves),
79);
var freq = MIDIUtils.noteNumberToFrequency(note);
sound.play(freq);
}
filter.frequency.value = 2400 + Math.sin(noteIt / 22) * 2000;
filter.Q.value = 6 + Math.sin(noteIt / 9) * 12;
gain_node.gain.value = 0.3 + -0.08 * Math.sin(noteIt / 22);
prevMoves = stats.nMoves;
}
}, 100);
$('body').prepend($('<button id="sound">').text('SOUND').click(function() {
soundEnabled = !soundEnabled;
prevMoves = null;
if (!soundEnabled) sound.stop();
}));
$(function() {
var worldWith = window.innerWidth - 30,
mapRatio = 0.4,
@ -89,94 +25,66 @@ $(function() {
};
};
// position legend and stats
var worldOffset = $('#worldmap').offset();
$('#stats').offset({
top: worldOffset.top + 250 * scale,
left: worldOffset.left + 7.6 * scale
}).attr({
width: 195 * scale,
height: 172 * scale
});
var randomPoint = function() {
return [
Math.random() * 100 - 50,
Math.random() * 200 - 100
];
};
if (!!window.EventSource) {
var source = new EventSource("/network/stream");
var point2pos = function(point) {
// point = randomPoint();
return world.getXY(
point[0] + Math.random() - 0.5,
point[1] + Math.random() - 0.5);
};
source.addEventListener('message', function(e) {
var raw = e.data.split('|');
var data = {
country: raw[0],
lat: parseFloat(raw[1]),
lon: parseFloat(raw[2]),
oLat: parseFloat(raw[3]),
oLon: parseFloat(raw[4])
// move: raw[5],
// piece: raw[6]
};
data.lat += Math.random() - 0.5;
data.lon += Math.random() - 0.5;
var orig = world.getXY(data.lat, data.lon);
var dot = paper.circle().attr({
fill: "#FE7727",
r: 1.2,
'stroke-width': 0
});
dot.attr(orig);
setTimeout(function() {
dot.remove();
}, 5000);
if (data.oLat) {
var dest = world.getXY(data.oLat, data.oLon);
dest.lat += Math.random() - 0.5;
dest.lon += Math.random() - 0.5;
var str = "M" + orig.cx + "," + orig.cy + "T" + dest.cx + "," + dest.cy;
var lightning = paper.path(str);
lightning.attr({
opacity: 0.2,
stroke: "#fff",
'stroke-width': 0.5
});
setTimeout(function() {
lightning.remove();
var line = paper.path(str);
line.attr({
opacity: 0.35,
stroke: "#FE7727",
'stroke-width': 0.5,
'arrow-end': 'oval-wide-long'
});
setTimeout(function() {
line.remove();
var drag = paper.path(str);
drag.attr({
opacity: 0.2,
stroke: "#FE7727",
'stroke-width': 0.5,
'arrow-end': 'oval-wide-long'
});
setTimeout(function() {
drag.remove();
}, 500);
}, 500);
}, 50);
}
var source = new EventSource("/network/stream");
// moves
stats.nMoves++;
$('#moves > span').text(stats.nMoves);
// top countries
if (data.country in stats.countries) stats.countries[data.country]++;
else stats.countries[data.country] = 1;
}, false);
// source.addEventListener('open', function(e) {
// // Connection was opened.
// // console.log("connection opened");
// }, false);
// source.addEventListener('error', function(e) {
// if (e.readyState == EventSource.CLOSED) {
// // Connection was closed.
// // console.log("connection closed");
// }
// }, false);
var removeFunctions = {};
var drawPoint = function(pos) {
var dot = paper.circle().attr({
opacity: 0.5,
fill: "#fff",
stroke: "#FE7727",
r: 1.5,
'stroke-width': 1
}).attr(pos);
var shadow = paper.circle().attr({
opacity: 0.03,
fill: "#fff",
r: 6
}).attr(pos);
return function() {
dot.remove();
shadow.remove();
};
};
var drawLine = function(pos) {
var str = "M" + pos[0].cx + "," + pos[0].cy + "T" + pos[1].cx + "," + pos[1].cy;
var line = paper.path(str).attr({
opacity: 0.5,
stroke: "#FE7727",
'stroke-width': 1
});
return function() {
line.remove();
};
}
source.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
if (removeFunctions[data.id]) {
removeFunctions[data.id].forEach(function(f) {
f();
});
delete removeFunctions[data.id];
}
if (!data.ps) return;
var pos = data.ps.map(point2pos);
removeFunctions[data.id] = pos.map(drawPoint);
if (data.ps[1]) removeFunctions[data.id].push(drawLine(pos));
}, false);
});

View file

@ -1,23 +0,0 @@
$(function() {
var since = Date.now();
function startTime()
{
var now = Date.now(),
elapsed = new Date(now - since),
h=formatTime(elapsed.getUTCHours()),
m=formatTime(elapsed.getUTCMinutes()),
s=formatTime(elapsed.getUTCSeconds());
$('#time > span').text(h+":"+m+":"+s);
setTimeout(function(){startTime();},500);
}
function formatTime(i)
{
if (i < 10) {
i="0" + i;
}
return i;
}
startTime();
});