Skip to content

Commit a5c4cc5

Browse files
committedJul 24, 2011
working version of mapchat
1 parent 96bcbf1 commit a5c4cc5

10 files changed

+406
-135
lines changed
 

‎README

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11

22

3-
This is a simple project to show the power of couchdb and NodeJS in handling Geo Data.
3+
This is a simple project to show the power of CouchDB and Node.js for handling Geo Data.
44

55

6-
In this chat app you all your messages are tied to a location.
6+
Users are shown a map and chatbox. Any chat message they add is added to the map at the
7+
location the map is centered on. Other users can see the map update in real-time.
8+
9+
==== REQUIRES
10+
11+
- expressjs
12+
- cradle
13+
- simplegeo (both the libraary and a simplegeo api key)
14+
- geojs (a simple geospatial lib for node)
15+
- CouchDB (or hosting with iris couch) Needs to include geocouch.

‎app/client.js

-56
This file was deleted.

‎app/index.html

-23
This file was deleted.

‎app/server.js

+106-40
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
HOST = null; // localhost
2-
PORT = 8012;
3-
4-
var fu = require("./fu"),
1+
var express = require("express"),
2+
app = express.createServer(),
3+
cradle = require("cradle"),
54
sys = require("sys"),
6-
url = require("url"),
7-
qs = require("querystring"),
8-
geojs = require("../geojs");
5+
geojs = require("geojs"),
6+
io = require("socket.io"),
7+
settings = require("./settings"),
8+
SimpleGeo = require("simplegeo-client").SimpleGeo;
9+
10+
var connection = new(cradle.Connection)(settings.COUCHDB_HOST, settings.COUCHDB_PORT,
11+
{auth: settings.COUCHDB_AUTH});
12+
var db = connection.database(settings.COUCHDB_DATABASE);
913

14+
var sg = new SimpleGeo(settings.SIMPLEGEO_KEY,
15+
settings.SIMPLEGEO_SECRET);
1016

1117
// keep subscriptions.
1218
// subscriptions have bounding boxes.
@@ -18,18 +24,7 @@ var subscriptions = [];
1824
// callback:<function> or subscription id in socket?
1925
// messages: [<>] might no be needed if socket io does this form me.
2026

21-
22-
23-
//On message: check if it is with anyone bounding box (if so send it to them)
24-
//save it to couchDB, add to message list, and flush message list to calc clustering.
25-
26-
27-
28-
fu.listen(Number(process.env.PORT || PORT), HOST);
29-
30-
31-
32-
socket = fu.socketio()
27+
socket = io.listen(app);
3328

3429
socket.on('connection', function(client){
3530
sys.puts("new client connect");
@@ -44,28 +39,19 @@ socket.on('connection', function(client){
4439
client.on('disconnect', function(){ sys.puts("client disconnect"); })
4540
});
4641

47-
fu.get("/", fu.staticHandler("index.html"));
48-
fu.get("/style.css", fu.staticHandler("style.css"));
49-
fu.get("/client.js", fu.staticHandler("client.js"));
50-
fu.get("/jquery-1.2.6.min.js", fu.staticHandler("jquery-1.2.6.min.js"));
51-
52-
53-
42+
app.get('/', function(req, res){
43+
res.render('index.ejs', { layout: false});
44+
});
45+
app.use('/static', express.static(__dirname + '/static'));
5446

5547

5648
mapchat = {
5749
subscriptions: [],
5850
subscribe:function(client, msg){
5951

60-
//does socket it handle client to server json?
61-
62-
63-
bottomlat = msg.bounds[1][0];
64-
bottomlon = msg.bounds[1][1];
65-
66-
toplatlng = new geojs.latLng(msg.bounds[0][0], msg.bounds[0][1]);
67-
bottomlatlng = new geojs.latLng(msg.bounds[1][0], msg.bounds[1][1]);
68-
bounds = new geojs.bounds(toplatlng, bottomlatlng);
52+
bottomlatlng = new geojs.latLng(msg.bounds[0][1], msg.bounds[0][0]);
53+
toplatlng = new geojs.latLng(msg.bounds[1][1], msg.bounds[1][0]);
54+
bounds = new geojs.bounds(bottomlatlng, toplatlng);
6955

7056
var allReadySubscribed = false;
7157
for(s in mapchat.subscriptions){
@@ -74,35 +60,115 @@ mapchat = {
7460

7561
//Set new bounds.
7662
mapchat.subscriptions[s].bounds = bounds
77-
sys.puts("new bounds set");
7863
break;
7964
}
8065
}
8166
if(!allReadySubscribed){
8267
mapchat.subscriptions.push({client:client,
8368
bounds:bounds});
84-
sys.puts("client with bounds added.");
69+
mapchat.sendChatClusters(client);
8570
}
71+
bbox = bounds.toBoundsArray().join(",");
72+
db.spatial("geo/recentPoints", {"bbox":bbox},
73+
function(er, docs) {
74+
if(er){sys.puts("Error: "+sys.inspect(er)); return;}
75+
for(d in docs){
76+
client.send({"type":"message",
77+
"geometry":docs[d].geometry,
78+
"date":docs[d].value.date,
79+
"message":docs[d].value.message});
80+
}
81+
82+
});
8683

8784
},
8885
message: function(client, msg){
8986

87+
// save message to the database
88+
msg.date = new Date();
89+
db.save(msg, function (err, res) {
90+
if(err){sys.puts("error: "+sys.inspect(err));}
91+
});
92+
9093
for(s in mapchat.subscriptions){
9194
sub = mapchat.subscriptions[s];
9295

9396
//We dont need to send a message to the same client that sent the message.
9497
if(sub.client.sessionId != client.sessionId){
9598

9699
//check see if the bounds match.
97-
point = new geojs.point(new geojs.latLng(msg.point[0], msg.point[1]));
100+
point = new geojs.point(msg.geometry);
98101
if(sub.bounds.contains(point)){
99-
sub.client.send({"type":"message", "point":msg.point, "message":msg.message});
102+
sub.client.send({"type":"message", "geometry":msg.geometry, "message":msg.message});
100103
}else{
101104
sys.puts("not in the box")
102105
}
103106
}
104107
}
105108

106-
}
109+
},
110+
sendChatClusters: function(client){
111+
if(client != undefined){
112+
// Send to just the one client
113+
client.send({"type":"clusters", "clusters":mapchat.clusters});
114+
}else{
115+
// Send to all subscriptions
116+
for(s in mapchat.subscriptions){
117+
sub = mapchat.subscriptions[s];
118+
sub.client.send({"type":"clusters", "clusters":mapchat.clusters});
119+
}
120+
}
121+
},
122+
getChatClusters: function(){
123+
db.spatiallist("geo/proximity-clustering/recentPoints", {"bbox":"-180,-90,180,90",
124+
"sort":"true",
125+
"limit":"5",
126+
"nopoints":"true"},
127+
function(er, docs) {
128+
if(er){sys.puts("Error: "+sys.inspect(er));return;}
129+
130+
var doneFetchingContext = function(docswithcontext){
131+
mapchat.clusters = docswithcontext;
132+
mapchat.sendChatClusters();
133+
setTimeout(mapchat.getChatClusters, (1000*600));
134+
}
135+
count = docs.length;
136+
for(d in docs){
137+
(function(doc){
138+
sg.getContextByLatLng(docs[d].center.coordinates[1],
139+
docs[d].center.coordinates[0],
140+
function(error,data,res){
141+
var city = "",
142+
state = "",
143+
country = "";
144+
for(f in data.features){
145+
if(data.features[f].classifiers[0].category == "National"){
146+
country = data.features[f].name.replace("United States of America", "USA");
147+
}else if(data.features[f].classifiers[0].category == "Subnational"){
148+
state = data.features[f].name;
149+
}else if(data.features[f].classifiers[0].category == "Municipal"){
150+
city = data.features[f].name;
151+
}else if(data.features[f].classifiers[0].category == "Urban Area"){
152+
city = data.features[f].name;
153+
}
154+
}
155+
names = [];
156+
if(city != ""){ names.push(city);}
157+
if(state != ""){ names.push(state);}
158+
if(country != ""){ names.push(country);}
159+
doc.locationName = names.join(", ");
160+
count--;
161+
if(count === 0){doneFetchingContext(docs)};
162+
});})(docs[d]);
107163

164+
}
165+
166+
167+
});
168+
}
108169
};
170+
171+
mapchat.getChatClusters();
172+
173+
174+
app.listen(settings.PORT);

‎app/settings.js.dist

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
settings = exports;
2+
3+
settings.SIMPLEGEO_KEY = "PTtBgGeJhmjq3BdB6jMzyQbHURyRGtcT";
4+
settings.SIMPLEGEO_SECRET = "t2dKATNHTyuJY7w6kexH5rhFNXeyCLza";
5+
settings.PORT = 8010;
6+
settings.COUCHDB_HOST = "localhost";
7+
settings.COUCHDB_PORT = 5984;
8+
settings.COUCHDB_DATABASE = "mapchat";
9+
settings.COUCHDB_AUTH = {username:"", password:""};

‎app/static/client.js

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//var socket = new io.Socket("mapchat.im");
2+
var socket = new io.Socket("localhost");
3+
var map = null;
4+
var chatboxes = [];
5+
socket.connect();
6+
socket.on('connect', function(){ console.log("connect"); });
7+
socket.on('message', function(data){
8+
if(data.type == "message"){
9+
chat.recieveMessage(data);
10+
}else if(data.type == "clusters"){
11+
chat.displayChatClusters(data.clusters);
12+
}
13+
});
14+
socket.on('disconnect', function(){ console.log("disconnect"); });
15+
16+
var chat = {
17+
sendMessage:function(){
18+
chatmsg = $("#message").val()
19+
$("#message").val("");
20+
var latlon = map.getCenter();
21+
var lat = latlon.lat();
22+
if(lat > 90){
23+
lat -= 180;
24+
}
25+
var lon = latlon.lng();
26+
if(lon > 180){
27+
lon -= 360;
28+
}
29+
var point = {"type":"Point", "coordinates":[lon, lat]};
30+
socket.send({action:"message", message:chatmsg, geometry:point});
31+
chat.recieveMessage({message:chatmsg, geometry:point})
32+
},
33+
recieveMessage:function(data){
34+
latlon = new google.maps.LatLng(data.geometry.coordinates[1],data.geometry.coordinates[0]);
35+
var chatbox = new ChatOverlay(latlon, data.message, map, "", "", "http://a3.twimg.com/profile_images/1408706495/at-twitter_bigger_normal.png");
36+
chatbox.show();
37+
chatboxes.push(chatbox);
38+
},
39+
displayChatClusters: function(clusters){
40+
$("div#clusters ul").empty();
41+
for(c in clusters){
42+
center = [clusters[c].center.coordinates[1], clusters[c].center.coordinates[0]].join(",");
43+
image_url = "http://maps.google.com/maps/api/staticmap?center=" +
44+
center + "&zoom=4&size=80x40&sensor=true"
45+
$li = $("<li><img src='"+image_url+"' /> <br /> <div class='location'>"+clusters[c].locationName+"</div></li>");
46+
$("div#clusters ul").append($li);
47+
$li.data("location", center);
48+
$li.click(function(e){
49+
lat =$(this).data("location").split(",")[0];
50+
lon = $(this).data("location").split(",")[1];
51+
map.setCenter(new google.maps.LatLng(lat,lon));
52+
});
53+
}
54+
}
55+
};
56+
57+
58+
$(document).ready(function(){
59+
$("#send").click(chat.sendMessage);
60+
61+
$("textarea#message").keypress(function(e){
62+
var code = (e.keyCode ? e.keyCode : e.which);
63+
if(code == 13) { //Enter keycode
64+
chat.sendMessage();
65+
return false;
66+
}
67+
});
68+
if(navigator.geolocation) {
69+
navigator.geolocation.getCurrentPosition(function(position) {
70+
initialLocation = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
71+
map.setCenter(initialLocation);
72+
});
73+
}
74+
75+
var myLatlng = new google.maps.LatLng(40.397, -104.644);
76+
var myOptions = {
77+
zoom: 8,
78+
center: myLatlng,
79+
mapTypeId: google.maps.MapTypeId.ROADMAP
80+
};
81+
map = new google.maps.Map(jQuery("#map")[0], myOptions);
82+
83+
google.maps.event.addListener(map, 'bounds_changed', function(){
84+
var mapbounds = map.getBounds();
85+
bounds = [[mapbounds.getSouthWest().lng(),
86+
mapbounds.getSouthWest().lat()],
87+
[mapbounds.getNorthEast().lng(),
88+
mapbounds.getNorthEast().lat()]];
89+
socket.send({action:"subscribe", bounds:bounds});
90+
for(c in chatboxes){chatboxes[c].setMap(null);}
91+
chatboxes = [];
92+
});
93+
});
94+
95+
96+
97+
function ChatOverlay(latlon, message, map, name, locationName, profilePic) {
98+
99+
// Now initialize all properties.
100+
this._latlon = latlon;
101+
this._map = map;
102+
this._message = message;
103+
this._name = name;
104+
this._locationName = locationName;
105+
this._profilePic = profilePic
106+
107+
// We define a property to hold the image's
108+
// div. We'll actually create this div
109+
// upon receipt of the add() method so we'll
110+
// leave it null for now.
111+
this._div = null;
112+
113+
// Explicitly call setMap() on this overlay
114+
this.setMap(map);
115+
}
116+
117+
ChatOverlay.prototype = new google.maps.OverlayView();
118+
119+
ChatOverlay.prototype.onAdd = function() {
120+
121+
// Create a new div that we will add to the map.
122+
var chatbox = $("<div class='chatmsg'><div>" + //<img src='"+this._profilePic+"' />" +
123+
"<div class='username'>"+this._name+"</div><br />" +
124+
"<div class='locationname'>"+this._locationName+"</div>"+
125+
"</div><div class='spacer'></div>"+
126+
"<div class='message'> "+this._message+"</div></div>");
127+
chatbox.css("position", "absolute");
128+
129+
// This is the reference to our div.
130+
this._div = chatbox;
131+
// Have to add it to a map pane. in this case the overlay layer.
132+
var panes = this.getPanes();
133+
panes.overlayLayer.appendChild(chatbox[0]);
134+
this._div.fadeIn('fast');
135+
};
136+
137+
ChatOverlay.prototype.draw = function() {
138+
139+
//This function is called when the map is redrawn, such as when the use zooms or moves
140+
141+
// So we can size and position the div we need to get the projection.
142+
var overlayProjection = this.getProjection();
143+
144+
// We will convert our Lat Lon into a pixel position
145+
var point = overlayProjection.fromLatLngToDivPixel(this._latlon);
146+
var div = this._div;
147+
148+
// We dynamically resize the overlay depending on zoom level to make
149+
// showing a lot of them not cover as much of the map
150+
width = 22 *(this.getMap().getZoom()/16)*10;
151+
height = 15 * (this.getMap().getZoom()/16)*10;
152+
div.css("width", width+"px");
153+
div.css("height", height+"px");
154+
155+
// Set the poition of the div.
156+
div.css("left", point.x-(width/2) + 'px');
157+
div.css("top", point.y-height + 'px');
158+
};
159+
160+
ChatOverlay.prototype.onRemove = function() {
161+
this._div.remove();
162+
this._div = null;
163+
};
164+
ChatOverlay.prototype.hide = function() {
165+
if (this._div) {
166+
this._div.hide();
167+
}
168+
};
169+
170+
ChatOverlay.prototype.show = function() {
171+
if (this._div) {
172+
this._div.fadeIn('fast');
173+
}
174+
};
175+
176+
/*ChatOverlay.prototype.toggle = function() {
177+
if (this.div_) {
178+
if (this.div_.style.visibility == "hidden") {
179+
this.show();
180+
} else {
181+
this.hide();
182+
}
183+
}
184+
};
185+
186+
ChatOverlay.prototype.toggleDOM = function() {
187+
if (this.getMap()) {
188+
this.setMap(null);
189+
} else {
190+
this.setMap(this.map_);
191+
}
192+
};*/

‎app/static/map_chat_small.png

15.7 KB
Loading

‎app/static/style.css

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
body{ margin:0px; padding:0px; font-family:arial; font-size:14px;}
2+
div#content{ width:100%; height:100%;}
3+
4+
div#headerwrapper{ position:absolute; top:0px; left:0px;
5+
z-index:10; width:100%;
6+
}
7+
8+
div#header{margin:auto; height:75px;width:70%; border: solid 1px #1a1a1a; border-top:none;
9+
border-bottom-right-radius:5px;
10+
border-bottom-left-radius:5px;
11+
background:#fff; z-index:10; box-shadow: 5px 5px 5px #666;
12+
opacity:0.9;
13+
}
14+
15+
div#logo{ background:#fff url('/static/map_chat_small.png') no-repeat top left; width:300px; padding:5px; height:60px; margin-left:10px;}
16+
17+
div#footerwrapper{ position:absolute; left:0px; bottom:0px; height:70px;
18+
z-index:10; width:100%
19+
}
20+
21+
div#chatsend{margin:auto; height:69px;width:50%; border: solid 1px #1a1a1a; border-bottom:none;
22+
border-top-right-radius:5px;
23+
border-top-left-radius:5px;
24+
background:#fff; z-index:10; box-shadow: 5px 5px 5px #666;
25+
opacity:0.9;
26+
position:relative;
27+
28+
}
29+
30+
div#chatarea{ margin:5px 80px 5px 10px;}
31+
div#chatsend textarea{ background-color:#eee; }
32+
div#send{ width:80px; position:absolute; right:15px; top:5px; width:30px; height:20px; text-align:center;
33+
background:#efefef; border-radius:5px; padding:10px; border:solid 1px #333; cursor:pointer;}
34+
div#send:hover{ background:#ddd;
35+
}
36+
37+
div#map{ height:100%; width:100%; background:#777; position:absolute; top:0px; left:0px;}
38+
39+
textarea#message{ width:100%; height:60px; border:solid 1px #999; }
40+
41+
div.chatmsg{ border-radius:3px; border:solid 1px #111; background-color: rgba(0,0,0,0.7); color:#fff;
42+
position:absolute; padding:3px; min-height:50px; z-index:15; overflow:hidden; display:none;}
43+
div.chatmsg img{ max-width:20px; max-height:20px; float:left;}
44+
div.chatmsg div.username{ float:left; margin-left:2px; font-size:12px; line-height:10px; font-weight:bold;}
45+
div.chatmsg div.locationname{ float:left; margin-left:2px; font-size:10px; line-height:8px; position:relative;
46+
top:-4px; color:#ccc;}
47+
48+
div.chatmsg div.message{ clear:both font-size:12px; border-top:solid 1px #666; padding-top:3px; line-height:14px; overflow:hidden;}
49+
50+
div.spacer{ clear:both;}
51+
52+
div#clusters{float:left; margin-left:310px; margin-top:5px; }
53+
div#clusters p{ margin:0px; padding:0px; font-size:12px; font-weight:bold; position:relative; top:-2px;}
54+
div#clusters ul{margin:0px; padding:0px; }
55+
div#clusters ul li{float:left; width:80px; border:solid 1px #999; height:50px;
56+
overflow:hidden; margin-right:5px; opacity:0.6; cursor:pointer; border-radius:5px; background:#eee;}
57+
div#clusters ul li:hover{ opacity:1;}
58+
div#clusters ul li div.location{font-size:9px; text-align:center;}

‎app/style.css

-14
This file was deleted.

‎app/views/index.ejs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<html>
2+
<head>
3+
<title>Map Chat</title>
4+
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
5+
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
6+
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
7+
<script type="text/javascript" src="/static/client.js"></script>
8+
<link rel="stylesheet" type="text/css" href="/static/style.css" />
9+
</head>
10+
<body>
11+
<div id="content">
12+
<div id="headerwrapper">
13+
<div id="header">
14+
<div id="clusters"><p>Recent Conversations</p><ul></ul></div>
15+
<div id="logo">
16+
</div>
17+
</div>
18+
</div>
19+
<div id="footerwrapper">
20+
<div id="chatsend">
21+
<div id="chatarea"><textarea id="message"></textarea></div>
22+
<div id="send">
23+
Send
24+
</div>
25+
</div>
26+
</div>
27+
<div id="map"></div>
28+
</div>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)
Please sign in to comment.