├── .backups
└── .gitignore
├── .meteor
├── identifier
├── release
├── .gitignore
├── platforms
├── .id
├── .finished-upgraders
├── packages
└── versions
├── packages
├── csv
│ ├── server.js
│ ├── .gitignore
│ └── package.js
├── analysis
│ ├── .gitignore
│ ├── client
│ │ ├── indivPerformance.html
│ │ ├── graphing.coffee
│ │ ├── tagging.html
│ │ ├── groupScatter.html
│ │ ├── indivPerformance.coffee
│ │ ├── viz.styl
│ │ ├── groupPerformance.html
│ │ ├── viz.html
│ │ ├── groupSlices.html
│ │ ├── routes.coffee
│ │ ├── tagging.coffee
│ │ ├── overview.coffee
│ │ ├── groupSlices.coffee
│ │ ├── groupScatter.coffee
│ │ └── overview.html
│ ├── generate_all.sh
│ ├── rpc.coffee
│ ├── analysis.py
│ ├── common.coffee
│ ├── package.js
│ ├── util.coffee
│ ├── scoring.py
│ └── tagging_biclustering.py
└── .gitignore
├── restore-db.sh
├── client
├── css
│ ├── meta.styl
│ ├── index.styl
│ ├── compact.styl
│ ├── chat.styl
│ ├── map.styl
│ ├── datastream.styl
│ ├── events.styl
│ └── mapper.styl
├── lib
│ ├── OpenLayersExtensions.coffee
│ └── mapper.coffee
├── helpers.coffee
├── views
│ ├── datastream.html
│ ├── notifications.html
│ ├── map.html
│ ├── notifications.coffee
│ ├── docs.html
│ ├── datastream.coffee
│ ├── common.html
│ ├── docs.coffee
│ ├── chat.html
│ ├── common.coffee
│ ├── events.html
│ └── chat.coffee
├── admin.html
├── meta
│ ├── exitsurvey.coffee
│ └── exitsurvey.html
├── admin.coffee
├── index.html
├── tutorial
│ └── tutorial.coffee
└── index.coffee
├── public
└── images
│ └── map
│ ├── fire-red.png
│ ├── fire-cyan.png
│ ├── fire-green.png
│ ├── flood-cyan.png
│ ├── flood-red.png
│ ├── skull-cyan.png
│ ├── skull-red.png
│ ├── fire-yellow.png
│ ├── flood-green.png
│ ├── flood-yellow.png
│ ├── skull-green.png
│ ├── skull-yellow.png
│ ├── tornado-2-red.png
│ ├── treedown-cyan.png
│ ├── treedown-red.png
│ ├── tsunami-cyan.png
│ ├── tsunami-green.png
│ ├── tsunami-red.png
│ ├── avalanche1-cyan.png
│ ├── avalanche1-red.png
│ ├── tornado-2-cyan.png
│ ├── tornado-2-green.png
│ ├── treedown-green.png
│ ├── treedown-yellow.png
│ ├── tsunami-yellow.png
│ ├── avalanche1-green.png
│ ├── avalanche1-yellow.png
│ ├── earthquake-3-cyan.png
│ ├── earthquake-3-green.png
│ ├── earthquake-3-red.png
│ ├── shark-export-cyan.png
│ ├── shark-export-green.png
│ ├── shark-export-red.png
│ ├── tornado-2-yellow.png
│ ├── earthquake-3-yellow.png
│ ├── shark-export-yellow.png
│ ├── zombie-outbreak1-red.png
│ ├── zombie-outbreak1-cyan.png
│ ├── zombie-outbreak1-green.png
│ └── zombie-outbreak1-yellow.png
├── deploy_demo.sh
├── restore-db-files.sh
├── dump-db.sh
├── dump-db-files.sh
├── start-production.sh
├── .gitignore
├── server
├── events_server.coffee
├── lib
│ └── mapper_utils.coffee
├── experiment_init.coffee
├── server.coffee
├── chat_server.coffee
└── firstrun.coffee
├── package.json
├── private
├── tutorial.csv
├── README.md
├── seed-instructions.txt
└── fields-pablo.json
├── settings.json
├── smart.json
└── README.md
/.backups/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/.meteor/identifier:
--------------------------------------------------------------------------------
1 | ikva1s1f9xydy12o3721
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.3.5.1
2 |
--------------------------------------------------------------------------------
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | dev_bundle
2 | local
3 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/packages/csv/server.js:
--------------------------------------------------------------------------------
1 | csv = Npm.require('csv');
2 |
--------------------------------------------------------------------------------
/packages/csv/.gitignore:
--------------------------------------------------------------------------------
1 | versions.json
2 |
3 | .build*
4 | .npm
5 |
--------------------------------------------------------------------------------
/restore-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mongorestore --host localhost:3001 --drop $@
3 |
--------------------------------------------------------------------------------
/client/css/meta.styl:
--------------------------------------------------------------------------------
1 | form.survey textarea
2 | min-width: 50%
3 | min-height: 120px
4 |
--------------------------------------------------------------------------------
/public/images/map/fire-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/fire-red.png
--------------------------------------------------------------------------------
/public/images/map/fire-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/fire-cyan.png
--------------------------------------------------------------------------------
/public/images/map/fire-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/fire-green.png
--------------------------------------------------------------------------------
/public/images/map/flood-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/flood-cyan.png
--------------------------------------------------------------------------------
/public/images/map/flood-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/flood-red.png
--------------------------------------------------------------------------------
/public/images/map/skull-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/skull-cyan.png
--------------------------------------------------------------------------------
/public/images/map/skull-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/skull-red.png
--------------------------------------------------------------------------------
/deploy_demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | meteor deploy --password --release 0.6.5-rc13 --settings settings.json crowdmapper.meteor.com
3 |
--------------------------------------------------------------------------------
/public/images/map/fire-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/fire-yellow.png
--------------------------------------------------------------------------------
/public/images/map/flood-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/flood-green.png
--------------------------------------------------------------------------------
/public/images/map/flood-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/flood-yellow.png
--------------------------------------------------------------------------------
/public/images/map/skull-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/skull-green.png
--------------------------------------------------------------------------------
/public/images/map/skull-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/skull-yellow.png
--------------------------------------------------------------------------------
/public/images/map/tornado-2-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tornado-2-red.png
--------------------------------------------------------------------------------
/public/images/map/treedown-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/treedown-cyan.png
--------------------------------------------------------------------------------
/public/images/map/treedown-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/treedown-red.png
--------------------------------------------------------------------------------
/public/images/map/tsunami-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tsunami-cyan.png
--------------------------------------------------------------------------------
/public/images/map/tsunami-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tsunami-green.png
--------------------------------------------------------------------------------
/public/images/map/tsunami-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tsunami-red.png
--------------------------------------------------------------------------------
/public/images/map/avalanche1-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/avalanche1-cyan.png
--------------------------------------------------------------------------------
/public/images/map/avalanche1-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/avalanche1-red.png
--------------------------------------------------------------------------------
/public/images/map/tornado-2-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tornado-2-cyan.png
--------------------------------------------------------------------------------
/public/images/map/tornado-2-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tornado-2-green.png
--------------------------------------------------------------------------------
/public/images/map/treedown-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/treedown-green.png
--------------------------------------------------------------------------------
/public/images/map/treedown-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/treedown-yellow.png
--------------------------------------------------------------------------------
/public/images/map/tsunami-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tsunami-yellow.png
--------------------------------------------------------------------------------
/packages/analysis/.gitignore:
--------------------------------------------------------------------------------
1 | .build*
2 | .npm
3 |
4 | versions.json
5 |
6 | # Don't store generated analysis output
7 | *.png
8 |
9 |
--------------------------------------------------------------------------------
/public/images/map/avalanche1-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/avalanche1-green.png
--------------------------------------------------------------------------------
/public/images/map/avalanche1-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/avalanche1-yellow.png
--------------------------------------------------------------------------------
/public/images/map/earthquake-3-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/earthquake-3-cyan.png
--------------------------------------------------------------------------------
/public/images/map/earthquake-3-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/earthquake-3-green.png
--------------------------------------------------------------------------------
/public/images/map/earthquake-3-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/earthquake-3-red.png
--------------------------------------------------------------------------------
/public/images/map/shark-export-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/shark-export-cyan.png
--------------------------------------------------------------------------------
/public/images/map/shark-export-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/shark-export-green.png
--------------------------------------------------------------------------------
/public/images/map/shark-export-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/shark-export-red.png
--------------------------------------------------------------------------------
/public/images/map/tornado-2-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/tornado-2-yellow.png
--------------------------------------------------------------------------------
/public/images/map/earthquake-3-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/earthquake-3-yellow.png
--------------------------------------------------------------------------------
/public/images/map/shark-export-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/shark-export-yellow.png
--------------------------------------------------------------------------------
/public/images/map/zombie-outbreak1-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/zombie-outbreak1-red.png
--------------------------------------------------------------------------------
/restore-db-files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | rm -rf .backups/cmdump
3 | tar xjf $@
4 | mongorestore --dbpath .meteor/local/db --drop .backups/cmdump
5 |
--------------------------------------------------------------------------------
/public/images/map/zombie-outbreak1-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/zombie-outbreak1-cyan.png
--------------------------------------------------------------------------------
/public/images/map/zombie-outbreak1-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/zombie-outbreak1-green.png
--------------------------------------------------------------------------------
/public/images/map/zombie-outbreak1-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurkServer/CrowdMapper/HEAD/public/images/map/zombie-outbreak1-yellow.png
--------------------------------------------------------------------------------
/dump-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | rm -rf .backups/cmdump
3 | mongodump --host localhost:3001 -o .backups/cmdump
4 | tar cjvf .backups/cmdump.tar.bz2 .backups/cmdump/
5 |
--------------------------------------------------------------------------------
/dump-db-files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | rm -rf .backups/cmdump
3 | mongodump --dbpath .meteor/local/db -o .backups/cmdump
4 | tar cjvf .backups/cmdump.tar.bz2 .backups/cmdump/
5 |
--------------------------------------------------------------------------------
/start-production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export HTTP_FORWARDED_COUNT=1
3 | # Don't listen on public interface of port 3000.
4 | meteor --port=localhost:3000 --settings settings-private.json --production
5 |
--------------------------------------------------------------------------------
/packages/csv/package.js:
--------------------------------------------------------------------------------
1 | Npm.depends({
2 | csv: "0.3.5"
3 | });
4 |
5 | Package.on_use(function (api) {
6 | api.add_files('server.js', 'server');
7 |
8 | api.export('csv');
9 | });
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | smart.lock
3 | dump.rdb
4 | settings-private.json
5 | settings-sandbox.json
6 |
7 | lib-cov
8 | *.seed
9 | *.log
10 | # *.csv
11 | *.dat
12 | *.out
13 | *.pid
14 | *.gz
15 |
16 | pids
17 | logs
18 | results
19 |
20 | npm-debug.log
21 | node_modules
22 |
--------------------------------------------------------------------------------
/packages/analysis/client/indivPerformance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Effort per Person-Hour
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/analysis/client/graphing.coffee:
--------------------------------------------------------------------------------
1 | # Helper functions for d3 graphs and visualizations
2 | colors = d3.scale.category10().domain( [0...10] )
3 |
4 | Util.groupColor = (size) -> colors(Math.log(size) / Math.LN2)
5 |
6 | Template.registerHelper "sizeColor", (size) ->
7 | size ?= @nominalSize
8 | return Util.groupColor(size)
9 |
--------------------------------------------------------------------------------
/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 1js89sbtwtctgmw94ch
8 |
--------------------------------------------------------------------------------
/packages/analysis/client/tagging.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Show only tweets tagged more than once
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/.gitignore:
--------------------------------------------------------------------------------
1 | /mizzao:user-status
2 | /matb33:collection-hooks
3 | /mizzao:partitioner
4 | /mizzao:turkserver
5 | /mizzao:sharejs
6 | /mizzao:autocomplete
7 | /mizzao:tutorials
8 | /mizzao:timesync
9 | /mizzao:jquery-ui
10 | /mizzao:openlayers
11 | /mrt:bootstrap-3
12 | /mrt:moment
13 | /natestrauser:x-editable-bootstrap
14 | /mizzao:bootstrap-3
15 | /mizzao:build-fetcher
16 | /mizzao:sharejs-ace
17 | /mizzao:animated-each
18 |
--------------------------------------------------------------------------------
/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.3.5-remove-old-dev-bundle-link
15 |
--------------------------------------------------------------------------------
/server/events_server.coffee:
--------------------------------------------------------------------------------
1 | # Publish all event fields
2 | Meteor.publish "eventFieldData", ->
3 | sub = this
4 |
5 | EventFields.find().forEach (doc) ->
6 | sub.added("eventfields", doc._id, doc)
7 |
8 | sub.ready()
9 |
10 | # Create an index on events to delete editors who piss off
11 | # No need to index non-editor events
12 | Events._ensureIndex {editor: 1}, {sparse: true}
13 |
14 | TurkServer.onDisconnect ->
15 | Events.update { editor: @userId },
16 | $unset: { editor: null }
17 | , multi: true
18 |
--------------------------------------------------------------------------------
/packages/analysis/generate_all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python tagging_biclustering.py 100
3 | python tagging_biclustering.py 100 100
4 | python tagging_biclustering.py 100 75
5 | python tagging_biclustering.py 100 50
6 | python tagging_biclustering.py 100 40
7 | python tagging_biclustering.py 100 30
8 | python tagging_biclustering.py 100 20
9 | python tagging_biclustering.py 100 10
10 |
11 | python tagging_biclustering.py 140 30
12 | python tagging_biclustering.py 120 30
13 | python tagging_biclustering.py 80 30
14 | python tagging_biclustering.py 60 30
15 | python tagging_biclustering.py 40 30
16 | python tagging_biclustering.py 20 30
17 |
--------------------------------------------------------------------------------
/server/lib/mapper_utils.coffee:
--------------------------------------------------------------------------------
1 | @Mapper ?= {}
2 |
3 | Mapper.loadCSVTweets = (file, limit) ->
4 | # csv is exported by the csv package
5 |
6 | Assets.getText file, (err, res) ->
7 | throw err if err
8 |
9 | csv()
10 | .from.string(res, {
11 | columns: true
12 | trim: true
13 | })
14 | .to.array Meteor.bindEnvironment ( arr, count ) ->
15 |
16 | i = 0
17 | while i < limit and i < arr.length
18 | Datastream.insert
19 | num: i+1 # Indexed from 1
20 | text: arr[i].text
21 | i++
22 | # console.log(i + " tweets inserted")
23 |
24 | , (e) ->
25 | Meteor._debug "Exception while reading CSV:", e
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CrowdMapper",
3 | "version": "0.0.0",
4 | "description": "Crisis mapping and collective intelligence experiment",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/TurkServer/CrowdMapper.git"
15 | },
16 | "author": "Andrew Mao",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/TurkServer/CrowdMapper/issues"
20 | },
21 | "homepage": "https://github.com/TurkServer/CrowdMapper",
22 | "devDependencies": {
23 | "zerorpc": "^0.9.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/analysis/rpc.coffee:
--------------------------------------------------------------------------------
1 | Meteor.defer ->
2 | console.log "Trying setup for python RPC analysis server..."
3 |
4 | try
5 | zerorpc = Npm.require("zerorpc")
6 |
7 | client = new zerorpc.Client()
8 | # Try to talk to localhost. If the python service hasn't started, an error
9 | # will be thrown later.
10 | client.connect("tcp://127.0.0.1:4242")
11 |
12 | # Print out any errors that might be encountered
13 | client.on "error", (err) -> console.error("RPC client error: ", err)
14 |
15 | Analysis.invokeRPC = Meteor.wrapAsync(client.invoke, client)
16 |
17 | response = Analysis.invokeRPC("maxMatching", [ [0, 0.5], [1, 0.5] ])
18 | console.log("Got python response (expect 1.5): ", response)
19 | catch e
20 | console.error("RPC error: ", e)
21 |
--------------------------------------------------------------------------------
/client/lib/OpenLayersExtensions.coffee:
--------------------------------------------------------------------------------
1 | Meteor.startup ->
2 | # adapted from dev.openlayers.org/releases/OpenLayers-2.13.1/examples/click.html
3 | OpenLayers.Control.Click = OpenLayers.Class OpenLayers.Control,
4 | defaultHandlerOptions: {
5 | 'single': true
6 | 'double': false
7 | 'pixelTolerance': 0
8 | 'stopSingle': false
9 | 'stopDouble': false
10 | }
11 | initialize: (options) ->
12 | @handlerOptions = OpenLayers.Util.extend({}, @defaultHandlerOptions)
13 | OpenLayers.Control::initialize.apply(@, arguments)
14 | @handler = new OpenLayers.Handler.Click(@, {'click': @trigger}, @handlerOptions)
15 | return # This is super important or it breaks
16 | # Give this an explicit name so we can use its active class: .olControlClickActive
17 | CLASS_NAME: "OpenLayers.Control.Click"
18 |
--------------------------------------------------------------------------------
/packages/analysis/analysis.py:
--------------------------------------------------------------------------------
1 | # RPC server using the method described at
2 | # http://ianhinsdale.com/code/2013/12/08/communicating-between-nodejs-and-python/
3 |
4 | import zerorpc
5 | import logging
6 |
7 | from munkres import Munkres
8 |
9 | logging.basicConfig()
10 |
11 | class AnalysisRPC(object):
12 | def __init__(self):
13 | self.m = Munkres()
14 |
15 | def hello(self, name):
16 | print "Hello called with: %s" % name
17 | return "Hello, %s" % name
18 |
19 | # mat is an nxm list of lists
20 | # representing a weight matching matrix
21 | # satisfying 0 < mat(x,y) < 1
22 | def maxMatching(self, mat):
23 | result = self.m.compute(mat)
24 | return sum([1 - mat[row][column] for row, column in result])
25 |
26 | s = zerorpc.Server(AnalysisRPC())
27 | s.bind("tcp://127.0.0.1:4242")
28 |
29 | print "Starting RPC server..."
30 | s.run()
31 |
--------------------------------------------------------------------------------
/client/helpers.coffee:
--------------------------------------------------------------------------------
1 | Handlebars.registerHelper "debug", ->
2 | console.log arguments
3 |
4 | Meteor.Spinner.options =
5 | lines: 13 # The number of lines to draw
6 | length: 13 # The length of each line
7 | width: 5 # The line thickness
8 | radius: 12 # The radius of the inner circle
9 | corners: 1 # Corner roundness (0..1)
10 | rotate: 0 # The rotation offset
11 | direction: 1 # 1: clockwise, -1: counterclockwise
12 | color: "#000" # #rgb or #rrggbb or array of colors
13 | speed: 1 # Rounds per second
14 | trail: 60 # Afterglow percentage
15 | shadow: false # Whether to render a shadow
16 | hwaccel: false # Whether to use hardware acceleration
17 | className: "spinner" # The CSS class to assign to the spinner
18 | zIndex: 2e9 # The z-index (defaults to 2000000000)
19 | top: "50%" # Top position relative to parent in px
20 | left: "50%" # Left position relative to parent in px
21 |
--------------------------------------------------------------------------------
/server/experiment_init.coffee:
--------------------------------------------------------------------------------
1 | TurkServer.initialize ->
2 | return if Datastream.find().count() > 0
3 |
4 | if @instance.treatment().tutorialEnabled
5 | Mapper.loadCSVTweets("tutorial.csv", 10)
6 | else
7 | # Load initial tweets on first start
8 | # Meta-cleaned version has 1567 tweets
9 | Mapper.loadCSVTweets("PabloPh_UN_cm.csv", 2000)
10 | # Create a seed instructions document for the app
11 | docId = Documents.insert
12 | title: "Instructions"
13 |
14 | Assets.getText "seed-instructions.txt", (err, res) ->
15 | if err?
16 | console.log "Error getting document"
17 | return
18 | ShareJS.initializeDoc(docId, res)
19 |
20 | TurkServer.onConnect ->
21 | if @instance.treatment().tutorialEnabled
22 | # Help the poor folks who shot themselves in the foot
23 | # TODO do a more generalized restore
24 | Datastream.update({}, {$unset: hidden: null}, {multi: true})
25 |
--------------------------------------------------------------------------------
/private/tutorial.csv:
--------------------------------------------------------------------------------
1 | tweetid,text,date,username,userid
2.76E+17, now that #PabloPh's leaving CDO i have the every right to just CHILL on my bed and have a peace of mind #cuddleweather http://t.co/tpaqkxhi, Wed Dec 05 11:56:59 CET 2012, imArielleLu,147559729
2.76E+17, Just landed! Hello Davao! Will be covering typhoon #PabloPH. May God bless us all. (@ Davao Int'l Airport Runway) http://t.co/n6BR8jZY, Wed Dec 05 14:42:53 CET 2012, hadjirieta,64646127
2.76E+17, CATEEL DAVAO ORIENTAL badly needs clothing food bottled water medicines and other basic needs #PabloPH #HelpCateel http://t.co/gjdwyi79, Wed Dec 05 15:01:03 CET 2012, highreaching,10781252
2.76E+17, Mati and Caraga Davao Oriental need help. Food water urgent needs. #PabloPH #ReliefPH @dswdserves @ANCALES @gmanews http://t.co/YQ6x7AlA, Wed Dec 05 15:49:49 CET 2012, highreaching,10781252
2.76E+17, Negros Oriental still without power - NORECO #PabloPH http://t.co/0KOZWBGc, Wed Dec 05 16:05:25 CET 2012, gmanews,39453212
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "public": {
3 | "map": {
4 | "extent": [ 12800000, 525000, 14500000, 2450000 ],
5 | "bingAPIKey": "AtsCXPry0QFxHVXRBJDXPVVy88GhE6tTwtW61SNJoVl8AYwcNce_UsO3VZ3lGT3Q"
6 | },
7 | "turkserver": {
8 | "watchRoute": "/mapper"
9 | }
10 | },
11 | "turkserver": {
12 | "adminPassword": "test",
13 | "mturk": {
14 | "sandbox": true,
15 | "accessKeyId": "FILL_ME_IN",
16 | "secretAccessKey": "FILL_ME_IN_TOO",
17 | "externalUrl": "http://localhost:3000/",
18 | "frameHeight": 800
19 | }
20 | },
21 | "sharejs": {
22 | "options": {
23 | "accounts_auth": {
24 | "authenticate": {
25 | "collection": "users",
26 | "token_validations": {
27 | "_id": "is_equal"
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/views/datastream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if loaded}}
4 | {{!-- again, a workaround to allow the inner divs to be dragged out --}}
5 | {{> dataList}}
6 | {{else}}
7 | {{> spinner}}
8 | {{/if}}
9 |
10 |
11 |
12 |
13 |
14 | {{#each data}}
15 | {{> dataItem}}
16 | {{/each}}
17 |
18 |
19 |
20 |
21 |
22 | {{num}}
23 | {{{replaceURLs text}}}
24 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/client/views/notifications.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Notifications
5 | {{#with notificationCount}}
6 | {{this}}
7 | {{/with}}
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
24 | {{username}} invited you to {{roomname}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{username}} mentioned you in {{roomname}}
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/packages/analysis/client/groupScatter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Group Scatterplot
4 |
5 |
Y-axis
6 |
7 | {{#each labels}}
8 |
9 | {{value}}
10 |
11 | {{/each}}
12 |
13 |
14 |
X-axis
15 |
16 | {{#each labels}}
17 |
18 | {{value}}
19 |
20 | {{/each}}
21 |
22 |
23 |
24 |
25 | Boxplot
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client/css/index.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | body
4 | // padding: 20px 20px 10px;
5 | padding-bottom: 5px
6 | overflow: hidden // Don't expand height/width when shit being dragged off screen
7 |
8 | blockquote, q
9 | quotes: none;
10 |
11 | blockquote:before, blockquote:after, q:before, q:after
12 | content: '';
13 | content: none;
14 |
15 | html, body, .fill-parent
16 | height: 100%
17 | width: 100%
18 |
19 | .flex
20 | display: flex
21 |
22 | .inherit-height
23 | height: inherit
24 |
25 | .fill-height
26 | height: 100%
27 |
28 | .fill-width
29 | width: 100%
30 |
31 | .carrier
32 | position: relative
33 |
34 | .scroll-vertical
35 | overflow-x: hidden
36 | overflow-y: auto
37 |
38 | .vertical-table
39 | display: table
40 | border-spacing: 0px 8px
41 |
42 | .vertical-table-row
43 | display: table-row
44 |
45 | .vertical-table-cell
46 | display: table-cell
47 |
48 | // Tweaks to bootstrap classes
49 |
50 | .label-inverse
51 | background-color: #000000
52 |
53 | .form-control.input-wide
54 | width: 300px
55 |
56 | .dropdown-info // For non-links in dropdowns
57 | padding: 3px 20px
58 | min-width: 240px
59 |
--------------------------------------------------------------------------------
/client/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Show deleted data
8 |
9 |
10 |
11 |
12 |
13 | Remaining Data
14 | {{remainingData}}
15 | Hidden Data
16 | {{hiddenData}}
17 | Attached Data
18 | {{attachedData}}
19 |
20 |
21 |
22 |
23 | Events Created
24 | {{createdEvents}}
25 | Events Deleted
26 | {{deletedEvents}}
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/client/css/compact.styl:
--------------------------------------------------------------------------------
1 | /*
2 | Compact styling modifications for bootstrap 2.3.2
3 | */
4 |
5 | form.compact
6 | margin: 0px
7 |
8 | h1.compact, h2.compact, h3.compact, h4.compact, h5.compact, h6.compact
9 | margin: 0px
10 |
11 | p.compact
12 | margin: 0 0 3px
13 |
14 | td.text-center
15 | text-align: center
16 |
17 | .container-fluid.compact
18 | padding-left: 5px
19 | padding-right: 5px
20 |
21 | .nav.nav-stacked.compact
22 | margin-bottom: 0px
23 |
24 | .nav.compact > li > a
25 | padding: 5px 8px
26 |
27 | .nav-pills.compact:before, .nav-pills.compact:after
28 | content: none
29 |
30 | .navbar.compact
31 | margin-bottom: 5px
32 |
33 | .navbar.compact .nav > li > a
34 | padding-left: 8px
35 | padding-right: 8px
36 |
37 | .navbar-inner.compact
38 | padding-left: 8px
39 | padding-right: 8px
40 |
41 | .row-fluid.compact:before, .row-fluid.compact:after
42 | content: none
43 |
44 | .table.compact th
45 | font-size: 12px // Smaller text = less horizontal space
46 | font-weight: bold
47 | line-height: 19px // Height of the sorting chevrons
48 |
49 | .well.well-nopad
50 | padding: 0
51 | margin: 0
52 |
53 | .well.well-skinny
54 | padding: 3px
55 | margin: 0px
56 |
--------------------------------------------------------------------------------
/client/views/map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{#with eventRecord}}
8 | {{! float right before other stuff makes it on the same line }}
9 |
10 |
11 | ({{{formatLocation}}})
12 | {{num}}
13 | {{dereference "type" type}}
14 |
15 |
16 |
{{description}}
17 |
18 |
19 | {{dereference "region" region}}
20 | {{dereference "province" province}}
21 |
22 |
23 |
24 | {{#each sources}}
25 | {{> tweetIcon findTweet}}
26 | {{/each}}
27 |
28 |
29 | {{> editCell }}
30 |
31 | Unmap
32 |
33 | {{/with}}
34 |
35 |
36 |
--------------------------------------------------------------------------------
/client/views/notifications.coffee:
--------------------------------------------------------------------------------
1 | Template.notifications.helpers
2 | notifications: ->
3 | return Notifications.find {},
4 | # show most recent first
5 | sort: {timestamp: -1}
6 |
7 | glowClass: ->
8 | if Notifications.find().count() > 0 then "glowing" else ""
9 |
10 | notificationCount: ->
11 | return Notifications.find().count()
12 |
13 | notificationTemplate: ->
14 | switch @type
15 | when "invite" then Template._inviteNotification
16 | when "mention" then Template._mentionNotification
17 | else null
18 |
19 | notifyEvents =
20 | 'click a': (e) ->
21 | e.preventDefault()
22 | Session.set("room", this.room)
23 | Meteor.call "readNotification", this._id
24 |
25 | Template._inviteNotification.events(notifyEvents)
26 | Template._mentionNotification.events(notifyEvents)
27 |
28 | notifyUsername = ->
29 | Meteor.users.findOne(@sender)?.username
30 |
31 | notifyRoomname = ->
32 | ChatRooms.findOne(@room)?.name
33 |
34 | notificationHelpers = {
35 | username: notifyUsername
36 | roomName: notifyRoomname
37 | }
38 |
39 | Template._inviteNotification.helpers(notificationHelpers)
40 | Template._mentionNotification.helpers(notificationHelpers)
41 |
--------------------------------------------------------------------------------
/private/README.md:
--------------------------------------------------------------------------------
1 | # Description of files
2 |
3 | - `PabloPh_UN_cm.csv`: filtered set of original data used by SBTF for Typhoon Pablo in 2012.
4 |
5 | - `tutorial.csv`: small set of data used for the interactive tutorial.
6 |
7 | - `fields-pablo.json`: configuration for the data entry fields used for the Pablo data.
8 |
9 | - `seed-instructions.txt`: initial document loaded as instructions for groups working together.
10 |
11 | - `groundtruth-pablo.json`: gold-standard set of events constructed from groups working together and corroborated with derived SBTF generated maps, found
12 | [here](http://www.arcgis.com/home/webmap/viewer.html?webmap=1e606f1a7cf74a599ccec9d0d5893fb0&extent=115.1752,4.4788,133.5663,13.6042)
13 | and
14 | [here](http://www.arcgis.com/home/webmap/viewer.html?webmap=fa64e3f0b09b4d61b0b907f8644cc272&extent=115.5322,4.5144,135.6152,16.1232).
15 | Some "events" in this set do not have locations and represent data that may have been relevant but were not specific enough to tag. Only events that were tagged with a location were used for evaluation. Note also that many links may have broken since this dataset was generated, and as such it may be harder to verify; however, it was produced from a best effort using what was available at the time.
16 |
--------------------------------------------------------------------------------
/client/css/chat.styl:
--------------------------------------------------------------------------------
1 | .chat-overview
2 | height: 172px
3 | max-height: 172px
4 |
5 | .chat-rooms, .room-users
6 | height: 150px
7 | max-height: 150px
8 | overflow: auto
9 |
10 | .room-item.deleted
11 | opacity: 0.4
12 |
13 | .action-room-enter
14 | overflow: hidden
15 | white-space: nowrap
16 | text-overflow: ellipsis
17 |
18 | .roomScreen
19 | margin: 0px
20 |
21 | .roomScreen form
22 | margin: 0px
23 |
24 | .chat-header
25 | position: absolute
26 | width: 100%
27 |
28 | .messages
29 | position: absolute
30 | width: 100%
31 | top: 30px
32 | bottom: 35px
33 | margin: 0
34 | padding: 0px
35 |
36 | .messages-body
37 | list-style: none
38 | margin: 0
39 | padding: 5px
40 | height: 100%
41 | width: 100%
42 |
43 | .chat-messaging > form
44 | position: absolute
45 | width: 100%
46 | height: auto
47 | bottom: 0
48 |
49 | .chat-button-container
50 | float: right
51 |
52 | .chat-input-container
53 | margin-right: 45px
54 | position: relative
55 |
56 | .chat-input.form-control
57 | width: 100%
58 | padding-right: 20px // So chat isn't covered by help icon
59 |
60 | .chat-help
61 | position: absolute
62 | top: 0
63 | bottom: 0
64 | right: 5px
65 | height: auto
66 | line-height: 36px
67 |
68 |
--------------------------------------------------------------------------------
/smart.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | "mizzao:user-status": {
4 | "path": "../mizzao:user-status"
5 | },
6 | "mizzao:build-fetcher": {
7 | "path": "../mizzao:build-fetcher"
8 | },
9 | "matb33:collection-hooks": {
10 | "path": "../matb33:collection-hooks"
11 | },
12 | "mizzao:partitioner": {
13 | "path": "../mizzao:partitioner"
14 | },
15 | "mizzao:turkserver": {
16 | "path": "../mizzao:turkserver"
17 | },
18 | "mizzao:sharejs": {
19 | "path": "../mizzao:sharejs/sharejs-base"
20 | },
21 | "mizzao:sharejs-ace": {
22 | "path": "../mizzao:sharejs/sharejs-ace"
23 | },
24 | "mizzao:animated-each": {
25 | "path": "../mizzao:animated-each"
26 | },
27 | "mizzao:autocomplete": {
28 | "path": "../mizzao:autocomplete"
29 | },
30 | "mizzao:tutorials": {
31 | "path": "../mizzao:tutorials"
32 | },
33 | "mizzao:timesync": {
34 | "path": "../mizzao:timesync"
35 | },
36 | "mizzao:jquery-ui": {
37 | "path": "../mizzao:jquery-ui"
38 | },
39 | "mizzao:openlayers": {
40 | "path": "../mizzao:openlayers"
41 | },
42 | "natestrauser:x-editable-bootstrap": {
43 | "path": "../natestrauser:x-editable-bootstrap"
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/css/map.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | // Fix for bootstrap crapping on openlayers
4 | #map img
5 | max-width: none
6 |
7 | .olControlClickActive
8 | cursor: crosshair
9 |
10 | .olControlLayerSwitcher .layersDiv
11 | border-radius: 10px 0 0 10px;
12 | opacity: 0.75;
13 |
14 | .olControlLayerSwitcher .layersDiv label
15 | display: inline // Cancel bootstrap styling
16 |
17 | .olControlMousePosition
18 | color: #b94a48
19 | font-size: larger !important
20 | font-weight: bold
21 |
22 | // Fixes hover events on vector layer in IE
23 | .olLayerDiv > svg
24 | pointer-events: all
25 |
26 | // HACK: remove the explicit visibility attribute on the vector layer - causes display issues in Firefox
27 | .olLayerDiv > svg > g
28 | visibility: inherit !important
29 |
30 | // Allow tweet tags to extend outside popups
31 | .olPopup
32 | overflow: visible !important
33 | border-radius: 5px
34 | box-shadow 0px 0px 3px 3px rgba(0, 0, 0, 0.3);
35 |
36 | .olPopup > div
37 | overflow: visible !important
38 | // position: static !important
39 |
40 | // One of the hacks below is needed to allow popovers to show outside of popups
41 | .olPopupContent
42 | overflow: visible !important
43 | position: static !important
44 | display: block
45 | max-width: 300px // Stop the popups from rendering too wide
46 |
--------------------------------------------------------------------------------
/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | #
3 | # 'meteor add' and 'meteor remove' will edit this file for you,
4 | # but you can also edit it by hand.
5 |
6 | meteor-base
7 | mobile-experience
8 | mongo
9 | blaze-html-templates
10 | session
11 | tracker
12 | logging
13 | reload
14 | random
15 | ejson
16 | spacebars
17 | check
18 | coffeescript
19 | jquery
20 | stylus
21 | accounts-ui
22 | webapp
23 |
24 | # Published, versioned shit
25 | iron:router
26 | mizzao:bootboxjs@4.4.0
27 | sacha:spin@2.3.1
28 | mizzao:animated-each@0.2.0
29 |
30 | d3js:d3@3.5.5
31 | twbs:bootstrap@3.3.5
32 | mizzao:jquery-ui@1.11.4
33 | natestrauser:x-editable-bootstrap@1.5.2_2
34 |
35 | # Pulls in the openlayers code and images
36 | mizzao:openlayers@2.13.1
37 |
38 | # DavidSichau's 0.10.0 breaks mongo connectivity due to some bugs in Meteor 1.4
39 | # So we'll leave this for now and change it in the future if necessary
40 | mizzao:sharejs@0.9.0
41 | mizzao:sharejs-ace
42 |
43 | # Development packages
44 | mizzao:user-status
45 | mizzao:autocomplete
46 | mizzao:tutorials
47 | mizzao:partitioner # So we can use the APIs directly
48 | mizzao:turkserver
49 | mizzao:timesync
50 |
51 | # Local - For parsing input files
52 | csv
53 | analysis
54 | aslagle:reactive-table
55 | standard-minifier-css
56 | standard-minifier-js
57 |
--------------------------------------------------------------------------------
/client/css/datastream.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .datastream
4 | width: 100%
5 | height: 100%
6 |
7 | .data-body
8 | padding: 4px
9 | margin: 0px
10 |
11 | .data-filters
12 | width: 100%
13 |
14 | .data-filters > .btn
15 | padding: 4px 0px
16 | width: 33.33%
17 |
18 | .data-cell
19 | position: relative
20 | width: 100%
21 | overflow: hidden
22 | // border: 1px dotted #E08E79
23 | margin-bottom: 3px
24 | border: 1px solid #CCCCCC
25 |
26 | .data-cell,
27 | .data-dragging
28 | background: white
29 | border-radius: 5px
30 |
31 | /*
32 | .data-cell.bolded
33 | background: white
34 | border: 1px solid #69D2E7
35 | */
36 | .data-cell.data-cell-hidden // Show deleted data to admins
37 | opacity: 0.4
38 | border: 1px dotted #CCCCCC
39 |
40 | .data-cell .label // full-width header bar
41 | cursor: move
42 | display: block
43 | text-align: left
44 |
45 | .data-cell.selected
46 | background: white
47 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 1) // A rounded outline
48 |
49 | .data-cell.selected .label
50 | background-color: #333333
51 |
52 | .data-cell:hover,
53 | .data-dragging
54 | border: 1px solid #000000
55 |
56 | .data-cell:hover,
57 | .data-dragging,
58 | .tweet-icon-container:hover,
59 | .tweet-icon-container.ui-draggable-dragging
60 | box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4)
61 |
62 | .data-cell-icon
63 | position: absolute
64 | bottom: 1px
65 | right: 1px
66 |
67 |
68 |
--------------------------------------------------------------------------------
/client/views/docs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if loaded}}
5 | {{> docTabs}}
6 | {{else}}
7 | {{> spinner}}
8 | {{/if}}
9 |
10 |
11 | {{> docCurrent}}
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{#each documents}}
19 | {{> docTab}}
20 | {{/each}}
21 | {{#if noDocuments}}
22 |
23 | No docs yet. Create one!
24 |
25 | {{/if}}
26 |
27 | New Document
28 |
29 |
30 |
31 |
32 |
33 | {{title}}
34 |
35 |
36 |
37 |
38 |
39 | {{#with document}}
40 | {{> docTitle title}}
41 | Delete this document
42 | {{> sharejsAce docid=this id="editor" onRender=config onConnect=checkAdmin }}
43 | {{/with}}
44 |
45 |
46 |
47 | {{this}}
48 |
49 |
--------------------------------------------------------------------------------
/client/meta/exitsurvey.coffee:
--------------------------------------------------------------------------------
1 | Template.exitsurvey.helpers
2 | surveyTemplate: ->
3 | # TODO generalize this based on batch
4 | treatments = TurkServer.batch()?.treatments
5 | if _.indexOf(treatments, "recruiting") >= 0
6 | Template.tutorialSurvey
7 | else if _.indexOf(treatments, "parallel_worlds") >= 0
8 | Template.postTaskSurvey
9 | else
10 | Template.loadingSurvey
11 |
12 | Template.tutorialSurvey.events
13 | "submit form": (e, tmpl) ->
14 | e.preventDefault()
15 |
16 | results =
17 | comprehension: tmpl.find("textarea[name=comprehension]").value
18 | prepared: tmpl.find("textarea[name=prepared]").value
19 | bugs: tmpl.find("textarea[name=bugs]").value
20 |
21 | panel =
22 | contact: tmpl.find("input[name=contact]").checked
23 | times: [
24 | tmpl.find("select[name=pickTime1]").value
25 | tmpl.find("select[name=pickTime2]").value
26 | tmpl.find("select[name=pickTime3]").value
27 | ]
28 |
29 | tmpl.find("button[type=submit]").disabled = true # Prevent multiple submissions
30 |
31 | TurkServer.submitExitSurvey(results, panel)
32 |
33 | Template.postTaskSurvey.events
34 | "submit form": (e, tmpl) ->
35 | e.preventDefault()
36 |
37 | results = {}
38 | results.age = tmpl.find("input[name=age]").value
39 | results.gender = tmpl.find("select[name=gender]").value
40 |
41 | fields = [ "approach", "specialize", "teamwork", "workwith", "leadership", "misc" ]
42 |
43 | for field in fields
44 | results[field] = tmpl.find("textarea[name=#{field}]").value
45 |
46 | tmpl.find("button[type=submit]").disabled = true # Prevent multiple submissions
47 |
48 | TurkServer.submitExitSurvey(results)
49 |
--------------------------------------------------------------------------------
/client/css/events.styl:
--------------------------------------------------------------------------------
1 | #mapper-events table
2 | border-collapse: collapse;
3 | border-spacing: 0;
4 | table-layout: fixed
5 | // word-wrap: normal
6 | word-wrap: break-word
7 |
8 | .events-header.table, .events-body > .table
9 | margin-bottom: 0
10 |
11 | // Don't allow table headers to wrap - They are also sized small to help with this
12 | .events-header th
13 | white-space: nowrap
14 |
15 | .header-num
16 | width: 45px
17 |
18 | .header-sources
19 | width: 170px // Allows four-digit tweets to display 3 to a row.
20 |
21 | .header-buttons, .header-location
22 | width: 71px
23 |
24 | .data-body
25 | list-style: none
26 |
27 | .events-body, .data-body
28 | display: block
29 | height: 100%
30 | width: 100%;
31 |
32 | /*
33 | Table row CSS
34 | */
35 |
36 | shadow-color = rgba(0,0,0,0.5)
37 | shadow = 14px
38 | -shadow = - shadow
39 | blur = 4px
40 | spread = -10px
41 |
42 | tr.event-record.deleted
43 | opacity: 0.4
44 |
45 | tr.event-record.selected > td
46 | box-shadow:
47 | 0 shadow blur spread shadow-color inset,
48 | 0 -shadow blur spread shadow-color inset
49 |
50 | // Since we have to, make the top left and bottom right corners the dark overlapping ones
51 | tr.event-record.selected > td:first-child
52 | box-shadow:
53 | shadow -shadow blur spread shadow-color inset,
54 | 0 shadow blur spread shadow-color inset
55 |
56 | tr.event-record.selected > td:last-child
57 | box-shadow:
58 | 0 -shadow blur spread shadow-color inset,
59 | -shadow shadow blur spread shadow-color inset
60 |
61 | // Make the selected event black
62 | tr.event-record.selected .label.label-danger
63 | background-color: #333333
64 |
65 | .table-condensed th.vertical-nopad
66 | padding-top: 0px
67 | padding-bottom: 0px
68 |
69 | .editable-coord
70 | display: block
71 | margin-bottom: 5px
72 |
--------------------------------------------------------------------------------
/packages/analysis/common.coffee:
--------------------------------------------------------------------------------
1 | # Collections for analysis
2 | Analysis.People = new Meteor.Collection("analysis.people")
3 | ###
4 | Fields:
5 | - userId
6 | - age
7 | - dropped (whether this user quit)
8 | - gender
9 | - exitSurveyWords
10 | - groupSize (nominalSize of group)
11 | - instanceId
12 | - treated
13 | - tutorialMins
14 | - tutorialWords
15 | ###
16 |
17 | # Nominal world stats that don't change over time
18 | Analysis.Worlds = new Meteor.Collection("analysis.worlds")
19 | ###
20 | Fields:
21 | - batchId
22 | - completed (whether at least one person submitted)
23 | - endTime
24 | - fracFemale
25 | - nominalSize
26 | - startTime
27 | - users
28 | - treated (valid treatment for analysis)
29 | - treatments
30 | ###
31 |
32 | # Stats computed in sliced world states
33 | Analysis.Stats = new Meteor.Collection("analysis.stats")
34 | ###
35 | Fields:
36 | - instanceId
37 | - chatFrac / chatWeight
38 | - chatWordCount / chatWordEntropy
39 | - avgIndivEntropy
40 | - binaryScore
41 | - classifyFrac / classifyWeight
42 | - effortEntropy (equality across users)
43 | - eventContention (maybe)
44 | - filterFrac / filterWeight
45 | - fractionalScore (rounded)
46 | - groupEntropy (distribution in group)
47 | - personTime
48 | - precision
49 | - recall
50 | - totalEffort
51 | - verifyFrac / verifyWeight
52 | - wallTime
53 | ###
54 |
55 | ###
56 | People fields
57 | - chatWordCount / chatWordFrac
58 | - dropped (whether dropout happened yet)
59 | - effort
60 | - time (activeTime)
61 | - tutorialMins / tutorialWords
62 | - wallTime (of group)
63 | - userId
64 | ###
65 |
66 | if Meteor.isServer
67 | Analysis.People._ensureIndex({instanceId: 1, userId: 1})
68 |
69 | # Index stats collection by world/user, then by time
70 | Analysis.Stats._ensureIndex({instanceId: 1, wallTime: 1}, {sparse: true})
71 | Analysis.Stats._ensureIndex({userId: 1, wallTime: 1}, {sparse: true})
72 |
--------------------------------------------------------------------------------
/packages/analysis/package.js:
--------------------------------------------------------------------------------
1 | // This is used to send RPC requests over ZeroMQ to Python
2 | Npm.depends({
3 | json2csv: "2.2.1",
4 | "simple-statistics": "0.9.0",
5 | });
6 |
7 | Package.on_use(function (api) {
8 | api.use("coffeescript");
9 | api.use("stylus");
10 | api.use("templating");
11 | api.use("tracker");
12 | api.use("reactive-dict");
13 |
14 | api.use("underscore");
15 |
16 | // use versions of these specified in main project
17 | api.use("mizzao:turkserver");
18 | api.use("mizzao:jquery-ui");
19 | api.use("iron:router");
20 | api.use("d3js:d3");
21 | api.use("aslagle:reactive-table");
22 |
23 | // Use local csv package to generate stuff
24 | api.use("csv");
25 |
26 | // Basic stats in browser
27 | api.addFiles('.npm/package/node_modules/simple-statistics/src/simple_statistics.js', 'client');
28 |
29 | api.addFiles("util.coffee");
30 | api.addFiles("common.coffee");
31 |
32 | api.addFiles([
33 | "client/viz.styl",
34 | "client/routes.coffee",
35 | "client/graphing.coffee",
36 | "client/viz.html",
37 | "client/viz.coffee",
38 | "client/overview.html",
39 | "client/overview.coffee",
40 | "client/tagging.html",
41 | "client/tagging.coffee",
42 | "client/box.js", // from http://bl.ocks.org/jensgrubert/7789216
43 | "client/groupScatter.html",
44 | "client/groupScatter.coffee",
45 | "client/groupPerformance.html",
46 | "client/groupPerformance.coffee",
47 | "client/groupSlices.html",
48 | "client/groupSlices.coffee",
49 | "client/indivPerformance.html",
50 | "client/indivPerformance.coffee"
51 | ], "client");
52 |
53 | api.addFiles('rpc.coffee', 'server');
54 |
55 | api.addFiles('replay.coffee', 'server');
56 | api.addFiles('aggregation.coffee', 'server');
57 | api.addFiles('analysis.coffee', 'server');
58 |
59 | // Exports
60 | api.export('Analysis');
61 | api.export('ReplayHandler', 'server');
62 |
63 | api.export('AdminController', 'client');
64 | // Make global available only in this package
65 | api.export('Util', {testOnly: true});
66 | });
67 |
68 |
--------------------------------------------------------------------------------
/client/views/datastream.coffee:
--------------------------------------------------------------------------------
1 | Template.datastream.helpers
2 | loaded: -> Session.equals("dataSubReady", true)
3 |
4 | # We want events to exist and events.length > 0 to display
5 | # So we get all docs where events either doesn't exist or it's of size 0
6 | dataSelector = {
7 | $or: [
8 | { events: {$exists: false} },
9 | { events: {$size: 0} }
10 | ]
11 | }
12 |
13 | Template.dataList.helpers
14 | data: ->
15 | selector = if TurkServer.isAdmin() and Session.equals("adminShowDeleted", true)
16 | # Ignoring just tagged events values
17 | dataSelector
18 | else
19 | _.extend({}, dataSelector, { hidden: {$exists: false} })
20 | return Datastream.find(selector, sort: {num: 1}) # Sort in increasing insertion order
21 |
22 | Template.dataList.rendered = ->
23 | AnimatedEach.attachHooks(@firstNode)
24 |
25 | dragProps =
26 | # Adding classes is okay because we activate on mouseover
27 | # addClasses: false
28 | # containment: "window"
29 | cursorAt: { top: 0, left: 0 }
30 | distance: 5
31 | # Temporarily disabled, see below
32 | # handle: ".label" # the header
33 | helper: Mapper.tweetDragHelper
34 | revert: "invalid"
35 | scroll: false
36 | # Make it really obvious where to drop these
37 | start: Mapper.highlightEvents
38 | drag: Mapper.tweetDragScroll
39 | stop: Mapper.unhighlightEvents
40 | zIndex: 1000
41 |
42 | Template.dataList.events
43 | "click .data-cell": (e, t) -> Mapper.selectData(@_id)
44 |
45 | # Debounce multi-click data hides
46 | "click .action-data-hide": _.debounce( (e) ->
47 | # Somehow it's still possible to click hide on missing stuff. Slow machines?
48 | unless @_id?
49 | bootbox.alert("Couldn't hide that data. If this persists, try reloading the app.")
50 | return
51 | Meteor.call "dataHide", @_id
52 | , 750, true)
53 |
54 | # Enable draggable when entering a tweet cell
55 | "mouseenter .data-cell:not(.ui-draggable-dragging)": (e) ->
56 | cell = $(e.target)
57 |
58 | # TODO remove temporary fix for jquery UI 1.11.0
59 | # http://bugs.jqueryui.com/ticket/10212
60 | cell.draggable(
61 | $.extend({
62 | handle: cell.find(".label")
63 | }, dragProps)
64 | )
65 |
66 | # TODO sometimes this throws an error. Why?
67 | cell.one("mouseleave", -> cell.draggable("destroy") )
68 |
--------------------------------------------------------------------------------
/packages/analysis/client/indivPerformance.coffee:
--------------------------------------------------------------------------------
1 | Template.overviewIndivPerformance.rendered = ->
2 | peopleData = Analysis.People.find().fetch()
3 |
4 | margin = {top: 30, right: 50, bottom: 70, left: 50}
5 |
6 | svg = @find("svg")
7 |
8 | accessor = (d) -> d.effort / d.time
9 | min = d3.min(peopleData, accessor)
10 | max = d3.max(peopleData, accessor)
11 |
12 | nest = d3.nest()
13 | .key( (d) -> d.groupSize )
14 | .sortKeys(d3.ascending)
15 | .rollup( (leaves) -> leaves.map(accessor) )
16 | .entries(peopleData)
17 |
18 | # Get this shit into the [0][1] format for rows
19 | data = nest.map( (o) -> [o.key, o.values] )
20 |
21 | width = $(svg).width() - margin.left - margin.right
22 | height = $(svg).height() - margin.top - margin.bottom
23 |
24 | chart = d3.box()
25 | .whiskers(Util.iqrFun(1.5))
26 | .height(height)
27 | .domain([min, max])
28 | .showLabels(true)
29 |
30 | # Switch svg over to d3 g element
31 | svg = d3.select(svg)
32 | .attr("class", "viz box")
33 | .append("g")
34 | .attr("transform", "translate(#{margin.left},#{margin.top})")
35 |
36 | # the x-axis
37 | x = d3.scale.ordinal()
38 | .domain([1, 2, 4, 8, 16, 32])
39 | .rangeRoundBands([0, width], 0.6, 0.3)
40 |
41 | xAxis = d3.svg.axis().scale(x).orient("bottom")
42 |
43 | # the y-axis
44 | y = d3.scale.linear()
45 | .domain([min, max])
46 | .range([height + margin.top, 0 + margin.top])
47 |
48 | yAxis = d3.svg.axis().scale(y).orient("left")
49 |
50 | # draw the boxplots
51 | svg.selectAll(".box")
52 | .data(data)
53 | .enter()
54 | .append("g")
55 | .attr("transform", (d) -> "translate(" + x(d[0]) + "," + margin.top + ")")
56 | .call chart.width(x.rangeBand())
57 |
58 | # draw y axis
59 | svg.append("g")
60 | .attr("class", "y axis")
61 | .call(yAxis)
62 | .append("text")
63 | .attr("transform", "rotate(-90)")
64 | .attr("y", 6)
65 | .attr("dy", ".71em")
66 | .style("text-anchor", "end")
67 | .text("Normalized Effort")
68 |
69 | # draw x axis
70 | # text label for the x axis
71 | svg.append("g")
72 | .attr("class", "x axis")
73 | .attr("transform", "translate(0," + (height + margin.top + 10) + ")")
74 | .call(xAxis)
75 | .append("text")
76 | .attr("x", (width / 2))
77 | .attr("y", 10)
78 | .attr("dy", ".71em")
79 | .style("text-anchor", "middle")
80 | .text("Group Size")
81 |
--------------------------------------------------------------------------------
/packages/analysis/client/viz.styl:
--------------------------------------------------------------------------------
1 | .legend div
2 | color: white
3 | padding: 4px
4 |
5 | // Box plots - janky CSS
6 | .box line, .box rect, .box circle
7 | fill: steelblue;
8 | stroke: #777;
9 | stroke-width: 1px;
10 |
11 | .box .center
12 | stroke-dasharray: 3,3;
13 |
14 | .box .outlier
15 | fill: none;
16 | stroke: #000;
17 |
18 | svg.viz
19 | // Charting stuff
20 | .grid .tick
21 | stroke: grey
22 | stroke-dasharray: 2, 2
23 | opacity: 0.7
24 |
25 | .line
26 | fill: none
27 | stroke: steelblue
28 | stroke-width: 1.5px
29 |
30 | .line.regression
31 | stroke: maroon;
32 |
33 | .axis text
34 | font-size: 150%
35 | text-anchor: middle
36 |
37 | // Graphing stuff
38 | .node
39 | stroke: #fff;
40 | stroke-width: 1.5px;
41 |
42 | .link
43 | stroke: #999;
44 | stroke-opacity: .6;
45 |
46 | // Visualizer
47 | .band
48 | fill: #dddddd
49 | stroke: #aaaaaa
50 |
51 | .pie .action
52 | fill-opacity: 0.50
53 |
54 | // The root node, showing the user's main activity
55 | .pie .action.root
56 | fill-opacity: 0.3
57 |
58 | .pie .action.type
59 | fill-opacity: 1.0
60 |
61 | // Filtering actions
62 | .action.filter, .action.data-hide
63 | fill: #d62728
64 |
65 | .action.data-link
66 | fill: #9467bd
67 |
68 | // Classification actions
69 | .action.event-edit
70 | fill: #1f77b4
71 |
72 | .action.classify, .action.event-create, .action.event-update
73 | fill: #bcbd22
74 |
75 | // Location updates
76 | .action.event-update.location, .action.event-update.province, .action.event-update.region
77 | fill: #17becf
78 |
79 | .action.event-save
80 | fill: #2ca02c
81 |
82 | // Verification actions
83 | .action.verify, .action.event-vote
84 | fill: #ff7f0e
85 |
86 | .action.data-move, .action.event-unvote
87 | fill: #8c564b
88 |
89 | .action.data-unlink, .action.event-delete
90 | fill: #7f7f7f
91 |
92 | // Chat actions
93 | .chat
94 | fill: #e377c2
95 |
96 | .chat.tagged
97 | stroke: #000000
98 |
99 | .chat.tagged, .chat.undirected
100 | fill-opacity: 0.50
101 |
102 | path
103 | fill-rule: evenodd
104 | stroke: #ffffff
105 |
106 | path.untreated
107 | stroke-dasharray: 5, 5
108 |
109 | path.pseudo
110 | stroke-dasharray: 3, 3
111 |
112 | .outline
113 | fill: #EEEEEE
114 |
115 | .brush .extent
116 | stroke: #000;
117 | fill-opacity: .250;
118 | shape-rendering: crispEdges;
119 |
--------------------------------------------------------------------------------
/server/server.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | TurkServer-ed publications
3 | All of these publications check for grouping, except notifications
4 | ###
5 |
6 | userFields = {
7 | fields: {
8 | username: 1
9 | status: 1
10 | }
11 | }
12 |
13 | # User status and username
14 | Meteor.publish "userStatus", ->
15 | ###
16 | The status field below should really be "status.online" to not publish random other status fields
17 | But we need to leave it at status because otherwise we will be missing fields on the merge.
18 | https://github.com/meteor/meteor/issues/998
19 | ###
20 | Meteor.users.find({}, userFields) # All users (in my group)
21 |
22 | # Publish all events, docs, and events, including deleted - filtering is done on the client
23 | # This means admins can see deleted items easily, and they still work in chat
24 |
25 | Meteor.publish "datastream", -> Datastream.find()
26 |
27 | Meteor.publish "docs", -> Documents.find()
28 |
29 | Meteor.publish "events", -> Events.find()
30 |
31 | Meteor.publish 'notifications', ->
32 | # Only publish unread notifications for this user (in this instance)
33 | Notifications.find
34 | user: this.userId
35 | read: {$exists: false}
36 |
37 | # Alternate admin publication for watching, that does not block TurkServer
38 | Meteor.publish "adminWatch", (instance) ->
39 | return [] unless Meteor.users.findOne(@userId)?.admin
40 | check(instance, String)
41 |
42 | # Hack to make sure we get both current users and past ones
43 | # TODO does not update as group is filling
44 | exp = Experiments.findOne(instance)
45 | treatments = exp?.treatments || []
46 | users = exp?.users || []
47 |
48 | return Partitioner.directOperation ->
49 | [
50 | # Group and treatment data
51 | Experiments.find(instance),
52 | Treatments.find({name: $in : treatments})
53 | # Experiment data
54 | ChatRooms.find({_groupId: instance}),
55 | Meteor.users.find({$or: [
56 | { _id: $in: users },
57 | { group: instance }
58 | ]}, userFields),
59 | Datastream.find({_groupId: instance}),
60 | Documents.find({_groupId: instance}),
61 | Events.find({_groupId: instance}),
62 | ]
63 |
64 | ###
65 | Methods
66 | ###
67 |
68 | Meteor.methods
69 | "finishTutorial": ->
70 | exp = TurkServer.Instance.currentInstance()
71 | # If finish button is mashed, this may not exist.
72 | unless exp?
73 | Meteor._debug("Finish tutorial: may have already finished for ", Meteor.userId())
74 | return
75 |
76 | if exp.treatment()?.tutorialEnabled
77 | # Don't accidentally teardown something that isn't the tutorial
78 | exp.teardown()
79 |
80 | return
81 |
82 |
83 |
--------------------------------------------------------------------------------
/client/css/mapper.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .stack > .item:not(.active)
4 | display: none
5 |
6 | .stack > .pages:not(.active)
7 | visibility: hidden
8 |
9 | .pages
10 | position: absolute
11 | top: 57px // 52px + 5px bottom margin on the navbar
12 | bottom: 0px
13 | left: 0px
14 | right: 0px
15 |
16 | @keyframes glow {
17 | to {
18 | text-shadow: 0 0 10px red;
19 | }
20 | }
21 |
22 | .notification > a.glowing
23 | animation: glow .5s infinite alternate;
24 |
25 | .bigger-labels .label
26 | font-size: 85%
27 |
28 | .data-labels .label
29 | // http://stackoverflow.com/q/11126685/586086
30 | display: inline-block
31 | padding: 3px 5px
32 |
33 | .user-list
34 | display: block
35 | height: 80px
36 | max-height: 80px
37 |
38 | .user-pill-container, .tweet-icon-container
39 | border-radius: .25em // To match the label inside, so shadow is round
40 | display: inline-block
41 | margin-bottom: 1px
42 |
43 | .tweet-icon-container .label
44 | cursor: move
45 |
46 | .tweet-icon-container .popover
47 | max-width: 250px
48 |
49 | .shadow-outline, #editor
50 | box-shadow 0px 0px 5px 5px rgba(0, 0, 0, 0.2);
51 |
52 | .shadow-outline.highlighted
53 | box-shadow 0px 0px 150px 100px rgba(0, 0, 0, 0.5);
54 |
55 | .guidanceMessage
56 | color: #FFFFFF
57 | position: absolute
58 | top: 0
59 | width: 100%
60 | height: auto
61 | z-index: 1000
62 |
63 | #editor
64 | position: absolute
65 | width: auto
66 | top: 30px
67 | bottom: 0
68 | left: 15px
69 | right: 15px
70 |
71 | #map, #events
72 | display: block
73 | border-radius: 4px
74 |
75 | newdoc-height = 34px
76 |
77 | #doc-list
78 | position: absolute
79 | top: 0px
80 | bottom: newdoc-height + 10px
81 | left: 0px
82 | right: 0px
83 |
84 | .doc-item.deleted
85 | opacity: 0.4
86 |
87 | #events
88 | position: absolute
89 | top: 32px // 27px header height
90 | bottom: 35px // 30px button height
91 | left: 0px
92 | right: 0px
93 |
94 | .event-create
95 | position: absolute
96 | height: 30px
97 | left: 0px
98 | bottom: 0px
99 | right: 0px
100 |
101 | .action-document-new
102 | position: absolute
103 | height: newdoc-height
104 | bottom: 0px
105 |
106 | .chopped
107 | display: inline-block
108 | font-size: 14px // Esp. necessary to cancel an input-append inheritance
109 | max-width: 160px
110 | overflow: hidden
111 | white-space: nowrap
112 | text-overflow: ellipsis
113 |
114 | .center
115 | float: none
116 | display: block
117 | margin-left: auto
118 | margin-right: auto
119 |
120 | .clickme
121 | cursor: pointer
122 |
--------------------------------------------------------------------------------
/packages/analysis/client/groupPerformance.html:
--------------------------------------------------------------------------------
1 |
2 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/client/admin.coffee:
--------------------------------------------------------------------------------
1 | # Admin Routes
2 | Router.map ->
3 | # Multi-watching capable route
4 | @route 'watch',
5 | path: '/watch/:instance'
6 | template: 'mapper',
7 | controller: AdminController
8 | waitOn: ->
9 | return unless @params.instance
10 | # Need to set all these session variables to true for it to work
11 | Meteor.subscribe "adminWatch", @params.instance, ->
12 | Session.set("userSubReady", true)
13 | Session.set("chatSubReady", true)
14 | Session.set("dataSubReady", true)
15 | Session.set("docSubReady", true)
16 | Session.set("eventSubReady", true)
17 |
18 | # Route to re-play a given crisis mapping instantiation
19 | @route 'replay',
20 | path: '/replay/:instance/:speed?/:scroll?'
21 | template: 'mapper',
22 | controller: AdminController
23 | waitOn: ->
24 | speed = parseFloat(this.params.speed) || 20
25 | Meteor.subscribe "replay", this.params.instance, speed, ->
26 | Session.set("userSubReady", true)
27 | Session.set("dataSubReady", true)
28 | # Session.set("docSubReady", true)
29 | Session.set("eventSubReady", true)
30 |
31 | Session.set("chatSubReady", true)
32 | Session.set("chatRoomReady", true)
33 |
34 | onAfterAction: ->
35 | # Set displayed chat room upon arrival of any message.
36 | # This just filters the chat collection based on the room, and shouldn't
37 | # be too slow because most worlds only had one room.
38 | @chatWatcher = ChatMessages.find().observeChanges
39 | added: (id, fields) -> Session.set("room", fields.room)
40 |
41 | return unless this.params.scroll
42 | # turn on auto scrolling for new events and hidden tweets
43 | @dataWatcher = Datastream.find({hidden: $exists: false}).observeChanges
44 | removed: (id) -> Mapper.scrollToData(id, 100)
45 |
46 | @eventWatcher = Events.find().observeChanges
47 | added: (id) ->
48 | Deps.afterFlush -> Mapper.scrollToEvent(id, 100)
49 |
50 | onStop: ->
51 | @chatWatcher?.stop()
52 | @dataWatcher?.stop()
53 | @eventWatcher?.stop()
54 |
55 | Template.adminControls.events
56 | "change input": (e, t) ->
57 | Session.set("adminShowDeleted", e.target.checked)
58 |
59 | Template.adminControls.helpers
60 | showDeleted: -> Session.equals("adminShowDeleted", true)
61 | remainingData: -> Datastream.find({
62 | $or: [ { events: null }, { events: {$size: 0} } ],
63 | hidden: null
64 | }).count()
65 |
66 | hiddenData: -> Datastream.find({ hidden: true }).count()
67 |
68 | attachedData: -> Datastream.find({
69 | "events.0": $exists: true
70 | }).count()
71 |
72 | createdEvents: -> Events.find({deleted: null}).count()
73 | deletedEvents: -> Events.find({deleted: true}).count()
74 |
--------------------------------------------------------------------------------
/packages/analysis/util.coffee:
--------------------------------------------------------------------------------
1 | # Define analysis global for export
2 | Analysis = {}
3 |
4 | bisectors = {}
5 |
6 | Util =
7 | # Overall classification of log actions
8 | logActionType: (entry) -> Util.actionCategory(entry.action)
9 |
10 | actionCategory: (action) ->
11 | switch action
12 | when "data-hide", "data-link" then "filter"
13 | when "event-create", "event-edit", "event-update", "event-save" then "classify"
14 | when "event-vote", "event-unvote", "event-unmap", "event-delete", "data-move", "data-unlink", \
15 | "event-edit-verify", "event-update-verify", "event-save-verify" then "verify"
16 | when "chat" then "chat"
17 | when "chat-create", "chat-rename", "room-enter", "chat-delete", \
18 | "document-create", "document-rename", "document-delete", "document-open"
19 | # explicitly ignored
20 | null
21 | else
22 | throw new Meteor.Error(500, "Unrecognized action")
23 |
24 | typeFields: [ "filter", "classify", "verify", "chat", "" ]
25 |
26 | # Convenience function for binning log or chat actions
27 | actionType: (item) ->
28 | if item.timestamp? then "chat" else Util.logActionType(item)
29 |
30 | weightOf: (item, weights) ->
31 | # Chat entry
32 | return weights.chat if item.timestamp?
33 | # Log entry
34 | return weights[item.action] || 0
35 |
36 | # Returns a function to compute the interquartile range.
37 | iqrFun: (k) ->
38 | (d, i) ->
39 | q1 = d.quartiles[0]
40 | q3 = d.quartiles[2]
41 | iqr = (q3 - q1) * k
42 | i = -1
43 | j = d.length
44 | while (d[++i] < q1 - iqr)
45 | null
46 | while (d[--j] > q3 + iqr)
47 | null
48 | return [ i, j ]
49 |
50 | # Abbreviations used in progress array
51 | fieldAbbr:
52 | partialCreditScore: "ps"
53 | fullCreditScore: "ss"
54 | totalEffort: "ef"
55 | wallTime: "wt"
56 | personTime: "mt"
57 | precision: "p"
58 | recall: "r"
59 |
60 | # Memoize bisectors so they aren't constructed each time.
61 | getBisector: (key) ->
62 | return bisectors[key] ?= d3.bisector( (d) -> d[key] )
63 |
64 | # compute a linearly interpolated value for a field in a progress array.
65 | interpolateArray: (progress, xField, yField, xVal) ->
66 | return if progress[progress.length - 1][xField] < xVal
67 | bisector = Util.getBisector(xField)
68 |
69 | i = bisector.right(progress, xVal)
70 |
71 | lower = progress[i - 1] || progress[0]
72 | upper = progress[i]
73 |
74 | l = (xVal - lower[xField]) / (upper[xField] - lower[xField])
75 | return l * upper[yField] + (1-l) * lower[yField]
76 |
77 | # Compute entropy of an array of probabilities that sum to 1.
78 | entropy: (probArr) ->
79 | e = 0
80 | for p in probArr
81 | continue if p is 0 # 0 log 0 = 0
82 | e -= p * Math.log(p) / Math.LN2
83 | return e
84 |
--------------------------------------------------------------------------------
/client/views/common.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if loaded}}
4 | Users:
5 | {{! Weirdness: see discussion at http://stackoverflow.com/q/23918439/586086 }}
6 | {{#each users}}{{> userPill}} {{/each}}
7 | {{else}}
8 | {{> spinner}}
9 | {{/if}}
10 |
11 |
12 |
13 |
14 | {{username}}
15 |
16 |
17 |
18 | Invite to Chat
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 | {{num}}
33 |
34 | {{{replaceURLs text}}}
35 |
36 |
37 |
38 |
39 | {{{replaceURLs text}}}
40 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 | {{num}}
56 |
57 | {{text}}
58 |
59 |
60 |
61 | {{num}}
63 |
64 |
65 |
66 | {{num}}
67 | {{description}}
68 |
69 |
70 |
71 |
72 |
74 |
75 |
76 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/client/views/docs.coffee:
--------------------------------------------------------------------------------
1 | Meteor.startup ->
2 | # No session document by default
3 | Session.setDefault("document", undefined)
4 |
5 | Template.docs.helpers
6 | loaded: -> Session.equals("docSubReady", true)
7 |
8 | Template.docTabs.helpers
9 | documents: ->
10 | selector = if TurkServer.isAdmin() and Session.equals("adminShowDeleted", true) then {}
11 | else { deleted: {$exists: false} }
12 | Documents.find(selector)
13 |
14 | noDocuments: ->
15 | Documents.find(deleted: {$exists: false}).count() is 0
16 |
17 | Template.docTabs.events =
18 | "click .action-document-new": ->
19 | bootbox.prompt "Name the document", (docName) ->
20 | return unless !!docName
21 |
22 | Meteor.call "createDocument", docName, (err, id) ->
23 | return unless id
24 | Session.set("document", id)
25 |
26 | "click a": (e) ->
27 | e.preventDefault()
28 | Session.set("document", @_id)
29 |
30 | TurkServer.log
31 | action: "document-open"
32 | docId: @_id
33 |
34 | Template.docTab.helpers
35 | active: -> if Session.equals("document", @_id) then "active" else ""
36 | deleted: -> if @deleted then "deleted" else ""
37 |
38 | # TODO: make sure this doesn't cause thrashing of the currently open document.
39 | Template.docCurrent.helpers
40 | document: ->
41 | id = Session.get("document")
42 | # Can't stay in a document if someone deletes it, unless we're admin
43 | selector = {_id: id}
44 | selector.deleted = {$exists: false} unless TurkServer.isAdmin()
45 |
46 | if Documents.findOne(selector)
47 | return id
48 | else
49 | return undefined
50 |
51 | title: -> Documents.findOne(""+@)?.title
52 |
53 | Template.docTitle.rendered = ->
54 | tmplInst = this
55 |
56 | this.autorun ->
57 | # Trigger this whenever title changes
58 | title = Blaze.getData()
59 | # Destroy old editable if it exists
60 | tmplInst.$(".editable").editable("destroy").editable
61 | display: ->
62 | success: (response, newValue) ->
63 | docId = Session.get("document")
64 | return unless document
65 | Meteor.call "renameDocument", docId, newValue
66 |
67 | Template.docCurrent.events =
68 | "click .action-document-delete": ->
69 | bootbox.confirm "Deleting this document will kick out all other editors! Are you sure?", (res) ->
70 | return unless res
71 | id = Session.get("document")
72 | Meteor.call "deleteDocument", id
73 | Session.set("document", undefined)
74 |
75 | aceConfig = (ace) ->
76 | # Set some reasonable options on the editor
77 | ace.setShowPrintMargin(false)
78 | # ace.renderer.setShowGutter(false)
79 | ace.session.setUseWrapMode(true)
80 | ace.session.setMode("ace/mode/markdown")
81 |
82 | aceCheckAdmin = (ace) ->
83 | ace.setReadOnly(true) if TurkServer.isAdmin()
84 |
85 | Template.docCurrent.helpers
86 | config: -> aceConfig
87 | checkAdmin: -> aceCheckAdmin
88 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.8
2 | accounts-password@1.1.13
3 | accounts-ui@1.1.9
4 | accounts-ui-unstyled@1.1.12
5 | aldeed:template-extension@3.4.3
6 | allow-deny@1.0.5
7 | analysis@0.0.0
8 | anti:i18n@0.4.3
9 | aslagle:reactive-table@0.8.12
10 | autoupdate@1.2.11
11 | babel-compiler@6.8.5
12 | babel-runtime@0.1.9_1
13 | base64@1.0.9
14 | binary-heap@1.0.9
15 | blaze@2.1.8
16 | blaze-html-templates@1.0.4
17 | blaze-tools@1.0.9
18 | boilerplate-generator@1.0.9
19 | caching-compiler@1.0.6
20 | caching-html-compiler@1.0.6
21 | callback-hook@1.0.9
22 | check@1.2.3
23 | coffeescript@1.1.4
24 | csv@0.0.0
25 | d3js:d3@3.5.5
26 | dandv:caret-position@2.1.1
27 | ddp@1.2.5
28 | ddp-client@1.2.9
29 | ddp-common@1.2.6
30 | ddp-rate-limiter@1.0.5
31 | ddp-server@1.2.10
32 | deps@1.0.12
33 | diff-sequence@1.0.6
34 | ecmascript@0.4.8
35 | ecmascript-runtime@0.2.12
36 | ejson@1.0.12
37 | email@1.0.16
38 | facts@1.0.8
39 | fastclick@1.0.12
40 | geojson-utils@1.0.9
41 | handlebars@1.0.7
42 | hot-code-push@1.0.4
43 | html-tools@1.0.10
44 | htmljs@1.0.10
45 | http@1.1.8
46 | id-map@1.0.8
47 | iron:controller@1.0.12
48 | iron:core@1.0.11
49 | iron:dynamic-template@1.0.12
50 | iron:layout@1.0.12
51 | iron:location@1.0.11
52 | iron:middleware-stack@1.1.0
53 | iron:router@1.0.12
54 | iron:url@1.0.11
55 | jquery@1.11.9
56 | launch-screen@1.0.12
57 | less@2.6.5
58 | livedata@1.0.18
59 | localstorage@1.0.11
60 | logging@1.0.14
61 | matb33:collection-hooks@0.7.15
62 | meteor@1.1.16
63 | meteor-base@1.0.4
64 | minifier-css@1.1.13
65 | minifier-js@1.1.13
66 | minimongo@1.0.17
67 | mizzao:animated-each@0.2.0
68 | mizzao:autocomplete@0.5.1
69 | mizzao:bootboxjs@4.4.0
70 | mizzao:build-fetcher@0.3.2
71 | mizzao:jquery-ui@1.11.4
72 | mizzao:openlayers@2.13.1_3
73 | mizzao:partitioner@0.5.9
74 | mizzao:sharejs@0.9.0
75 | mizzao:sharejs-ace@1.4.1
76 | mizzao:timesync@0.4.0
77 | mizzao:turkserver@0.5.0
78 | mizzao:tutorials@0.6.7
79 | mizzao:user-status@0.6.6
80 | mobile-experience@1.0.4
81 | mobile-status-bar@1.0.12
82 | modules@0.6.5
83 | modules-runtime@0.6.5
84 | momentjs:moment@2.10.6
85 | mongo@1.1.9_1
86 | mongo-id@1.0.5
87 | mongo-livedata@1.0.12
88 | natestrauser:x-editable-bootstrap@1.5.2_3
89 | npm-bcrypt@0.8.6_3
90 | npm-mongo@1.4.45
91 | observe-sequence@1.0.12
92 | ordered-dict@1.0.8
93 | promise@0.7.3
94 | random@1.0.10
95 | rate-limit@1.0.5
96 | reactive-dict@1.1.8
97 | reactive-var@1.0.10
98 | reload@1.1.10
99 | retry@1.0.8
100 | routepolicy@1.0.11
101 | sacha:spin@2.3.1
102 | service-configuration@1.0.10
103 | session@1.1.6
104 | sha@1.0.8
105 | spacebars@1.0.12
106 | spacebars-compiler@1.0.12
107 | srp@1.0.9
108 | standard-minifier-css@1.0.8
109 | standard-minifier-js@1.0.8
110 | stylus@2.512.5
111 | templating@1.1.14
112 | templating-tools@1.0.4
113 | tracker@1.0.15
114 | twbs:bootstrap@3.3.5
115 | ui@1.0.11
116 | underscore@1.0.9
117 | url@1.0.10
118 | webapp@1.2.11
119 | webapp-hashing@1.0.9
120 |
--------------------------------------------------------------------------------
/packages/analysis/client/viz.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
17 |
18 | {{> controls}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{! user bands }}
29 |
30 |
31 | {{! log and chat nodes }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{! XXX: Should read rendered radio button settings from parent template}}
45 |
46 | Sunburst Weights
47 |
48 |
49 | Scaled
50 |
51 |
52 | Equal
53 |
54 |
55 | Sunburst Layout
56 |
57 |
58 | Force Cluster
59 |
60 |
61 | Communication Network
62 |
63 |
64 | Collaboration Network
65 |
66 |
67 | By Contribution
68 |
69 |
70 | Fixed
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/private/seed-instructions.txt:
--------------------------------------------------------------------------------
1 | These instructions briefly recap the content you went through in the tutorial. This document is editable, so you should edit and improve the instructions below to help other mappers.
2 |
3 | You and your team members will be working actual Twitter reports from Typhoon Pablo (also known as Bopha), which hit the Phillippines in December 2012. Your team must figure out which reports describe relevant crisis events, and record them with the type of event, its description, and its location. By effectively solving this problem and creating an accurate crisis map with your teammates, you can help us learn how to better respond to future natural disasters.
4 |
5 | # Data reports
6 |
7 | - Twitter reports show up on the left of the screen.
8 | - Click on links to see what they refer to.
9 | - Click the red X to hide tweets that are irrelevant. Hidden tweets are hidden for everyone.
10 |
11 | # Event Records
12 |
13 | - Event records are the main way of recording crisis events.
14 | - Drag a tweet by its blue title bar to attach it as a reference to an event.
15 | - Drag tweets between events to reorganize them.
16 | - Move your mouse over a tweet to see its content.
17 | - Click edit to start editing the fields of an event.
18 | - When editing, click a field (underlined) to select or type a new value.
19 | - Click save when you are done editing so that someone else can edit.
20 | - You can edit the location field to type in a longitude and latitude directly, or use the Locate button to place it on the map visually.
21 | - Mouse over the check box on the right to verify an event if you have checked it.
22 |
23 | # Map
24 |
25 | - Drag, pan, and zoom on the map to navigate.
26 | - Mouse over an event on the map to see a popup of its information.
27 | - Click to select events on the map, which will keep the popover open.
28 | - Drag events on the map to change their location.
29 | - Switch between a satellite and political map using the '+' at the top right.
30 |
31 | # Documents
32 |
33 | - Create documents to share information with other users that does not fit in the events, map, or chat.
34 | - Documents can be edited simultaneously.
35 |
36 | # Users and Chat
37 |
38 | - The user list at the top right shows all users that are on your team.
39 | - You can join a chat room by clicking on it, or switch to another chat room.
40 | - You can only be in one room at a time.
41 | - When you are in a room, you can hover over an online user's chat to send them an invite to your room.
42 | - Invites will show up as notifications at the top of the screen.
43 | - When you mention a user in chat, a notification will also appear for them.
44 | - Click notifications to go to the room that they came from.
45 | - Use symbols to tag tweets, events, and users in the chat. Hover over the '?' in the chat input for more info.
46 |
47 | # Payment
48 |
49 | - You will be paid for the amount of time you spend on the task, plus a bonus for how well your team does.
50 | - If you do not do anything for several minutes, you will be put in an idle mode.
51 | - You will not earn anything for the time you spend while idle.
52 |
--------------------------------------------------------------------------------
/packages/analysis/scoring.py:
--------------------------------------------------------------------------------
1 |
2 | # Script to score groups using a min-cost max-flow matching algorithm
3 | # based on the ground truth created from clustering
4 |
5 | import argparse
6 |
7 | parser = argparse.ArgumentParser(description='Score groups using the gold standard.')
8 |
9 | parser.add_argument('goldStandardId', help='The world to use as the benchmark.')
10 |
11 | parser.add_argument('--groupId', '-g', help='A specific group to run on. If unspecified, runs on all groups.')
12 |
13 | parser.add_argument('--write', '-w', action='store_const', const=True, default=False,
14 | help='Write results to database.')
15 |
16 | args = parser.parse_args()
17 |
18 | from munkres import Munkres, make_cost_matrix
19 | from math import sqrt, log10
20 | from pymongo import MongoClient
21 |
22 | m = Munkres()
23 |
24 | client = MongoClient('localhost', 3001)
25 | db = client.meteor
26 |
27 | events = db['events']
28 | worlds = db['analysis.worlds']
29 |
30 | world_list = list(
31 | worlds.find() if not args.groupId else worlds.find({_id: worlds.groupId}) )
32 |
33 | gs_events = list( events.find({
34 | '_groupId': args.goldStandardId,
35 | 'deleted': { '$exists': False },
36 | # Some events in gold standard don't have location:
37 | # They are just being used to hold data, so ignore them.
38 | 'location': { '$exists': True }
39 | }) )
40 |
41 | # Scoring function for an event. Current scheme is:
42 | # 0.25 to type, 0.25 to region, 0.25 to province,
43 | # 0.25 for within 10km to 0 beyond 100km
44 | def score(event, goldst):
45 | s = 0
46 | for field in ["type", "region", "province"]:
47 | if field in event and event[field] == goldst[field]:
48 | s += 0.25
49 |
50 | if "location" in event:
51 | dist_meters = 0.1 + sqrt(
52 | sum( (a - b)**2 for a, b in zip(event["location"], goldst["location"])))
53 |
54 | s += 0.25 * (1 - max(0, min(1, (log10(dist_meters) - 4))) )
55 |
56 | # Flip so it's a cost matrix
57 | return 1 - s
58 |
59 | # 0.33 = up to 1 field wrong and ~20km away
60 | # < 0.24 = just errors in the location
61 |
62 | errorThresh = 0.33
63 |
64 | for world in world_list:
65 | worldId = world["_id"]
66 | world_events = list( events.find({
67 | '_groupId': worldId,
68 | 'deleted': { '$exists': False }
69 | }) )
70 |
71 | # Build matrix for Munkres
72 | mat = []
73 | for event in world_events:
74 | mat.append([score(event, gs) for gs in gs_events])
75 |
76 | partialCredit = sum([1 - mat[row][column] for row, column in m.compute(mat)])
77 |
78 | # Clamp matrix values for a threshold
79 | for row in mat:
80 | for j in range(len(row)):
81 | row[j] = 0 if row[j] < errorThresh else 1
82 |
83 | fullCredit = sum([1 - mat[row][column] for row, column in m.compute(mat)])
84 |
85 | print 'world %s, size %d, partial %.02f, full %.02f' % (
86 | worldId, world["nominalSize"], partialCredit, fullCredit)
87 |
88 | if args.write:
89 | world["partialCreditScore"] = partialCredit
90 | world["fullCreditScore"] = fullCredit
91 | worlds.save(world)
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CrowdMapper
2 | ===========
3 |
4 | Real-time collaborative application for tagging streams of geospatial data, built on top of the Meteor Javascript platform. Built with the humanitarian goal of crisis mapping in mind.
5 |
6 | [](https://www.youtube.com/watch?v=xJYq_kh6NlI)
7 |
8 | This project uses [TurkServer](https://github.com/HarvardEconCS/turkserver-meteor) to study how people can organize to do crisis mapping. It was used to run the experiment and generate the data for the following paper:
9 |
10 | > [Mao A, Mason W, Suri S, Watts DJ (2016) An Experimental Study of Team Size and Performance on a Complex Task. PLoS ONE 11(4): e0153048.][cm-plos]
11 |
12 | [cm-plos]: http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0153048
13 |
14 | ## Running the app
15 |
16 | [Install Meteor]. Then, clone this repository and run the following command:
17 |
18 | ```
19 | meteor --settings settings.json
20 | ```
21 |
22 | If that works, then you should be able to view the data from the experiment. Using this software to run another experiment is a bit more complicated, as it's not that well documented. However, the code is well-commented.
23 |
24 | [install meteor]: https://www.meteor.com/install
25 |
26 | ## Viewing the data
27 |
28 | CrowdMapper was designed to both facilitate a teamwork task as well as log the interactions for further analysis. Using the data from the experiment, you can access visualizations and replays with the following instructions:
29 |
30 | 1. Start Meteor in development mode as above.
31 | 2. Unzip the MongoDB dump to a local directory, e.g. `tar xjvf data.tar.bz2`. Usually, you want to make sure this is in a folder preceded by `.` so that Meteor doesn't try to read it, e.g. `.backups/data`.
32 | 3. Replace the database `mongorestore --host localhost:3001 --drop .backups/data`. Once this finishes, you will have a copy of the database from after the experiment.
33 | 4. You will need to restart Meteor to reset the admin password. Then, you can access the visualizations and replays at http://127.0.0.1:3000/overview.
34 |
35 | To do further analysis on the data, please consult the instructions below, and the source code.
36 |
37 | ## Additional dependencies
38 |
39 | If you are replicating the original analysis from the paper, this project has several dependencies that can't be installed by Meteor.
40 |
41 | The data analysis uses [libzmq](https://github.com/zeromq/libzmq) for Node.js to make RPC calls to algorithms implemented in Python, as outlined in [this blog post](http://ianhinsdale.com/code/2013/12/08/communicating-between-nodejs-and-python/). If you don't have ZeroMQ installed on your system, you will see an error when starting the Meteor app (it should still start) and you won't be able to start the Python computation process or run any of the data analysis methods.
42 |
43 | To make the analysis algorithms available, first install the `libzmq` and `libevent` libraries, using whatever is appropriate for your system. For example, on Ubuntu:
44 |
45 | ```
46 | apt-get install libzmq-dev
47 | apt-get install libevent
48 | ```
49 |
50 | If all the above works, Meteor should be able to install the `zerorpc` npm package as part of the app without any issues.
51 |
52 | In addition, install the python dependencies for ZeroRPC, and Munkres (the Hungarian algorithm):
53 |
54 | ```
55 | pip install pyzmq
56 | pip install zerorpc
57 | pip install munkres
58 | ```
59 |
--------------------------------------------------------------------------------
/packages/analysis/client/groupSlices.html:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
52 |
53 |
0
54 |
{{settingOf "sliceMax"}}
55 |
56 |
57 |
58 |
59 |
60 | {{#each points}}
61 | {{> groupSlicePoint}}
62 | {{/each}}
63 |
64 |
65 |
66 |
67 |
68 | Group Size
69 |
70 |
71 |
72 | {{settingOf "yLabel"}}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {{_id}}
82 |
83 |
84 |
--------------------------------------------------------------------------------
/client/lib/mapper.coffee:
--------------------------------------------------------------------------------
1 | # The map needs to load first or openlayers complains
2 | @Mapper = @Mapper || {}
3 |
4 | @ChatUsers = new Mongo.Collection("chatusers")
5 |
6 | # Allow bing map code to be loaded by OpenLayers
7 | UI._allowJavascriptUrls()
8 |
9 | Mapper.events = new EventEmitter()
10 |
11 | Mapper.switchTab = (page) ->
12 | return unless page is "docs" or page is "events" or page is "map"
13 | # Simulate click on navbar in index.coffee
14 | $("a[data-target='#{page}']").trigger("click")
15 | return
16 |
17 | scrollPos = (element, parent) ->
18 | scrollTop:
19 | parent.scrollTop() + element.position().top - parent.height() / 2 + element.height() / 2
20 |
21 | Mapper.selectData = (id) ->
22 | $(".data-cell").removeClass("selected")
23 | $("#data-#{id}").addClass("selected") if id?
24 | return
25 |
26 | Mapper.scrollToData = (id, speed = "slow") ->
27 | parent = $(".scroll-vertical.data-body")
28 | element = $("#data-#{id}")
29 | return unless element.length # Can't scroll to things that aren't in datastream
30 | parent.animate(scrollPos(element, parent), speed)
31 | return
32 |
33 | Mapper.selectEvent = (id) ->
34 | Mapper.mapSelectEvent?(id)
35 | $(".events-body tr").removeClass("selected")
36 | $("#event-#{id}").addClass("selected") if id?
37 | return
38 |
39 | Mapper.scrollToEvent = (id, speed = "slow") ->
40 | parent = $(".scroll-vertical.events-body")
41 | element = $("#event-#{id}")
42 | return unless element.length
43 | parent.animate(scrollPos(element, parent), speed)
44 | return
45 |
46 | Mapper.tweetDragHelper = (e) ->
47 | # Get width of current item, if dragging from datastream
48 | currentWidth = Math.max $(this).width(), 200
49 | data = Blaze.getData(this)
50 |
51 | # Grab tweetId either from datastream object or event tweet array
52 | tweetId = data?._id || data
53 | helper = $ Blaze.toHTMLWithData Template.tweetDragHelper, Datastream.findOne(tweetId)
54 |
55 | # Make a clone of just the text of the same width
56 | # Append to events-body, so it can be scrolled while dragging (see below).
57 | return helper.appendTo(".events-body").width(currentWidth)
58 |
59 | # jQuery UI's scrolling doesn't quite work. So we roll our own.
60 | scrollSensitivity = 80
61 | scrollSpeed = 25
62 |
63 | Mapper.tweetDragScroll = (event, ui) ->
64 | scrollParent = $(".events-body")
65 | overflowOffset = scrollParent.offset()
66 |
67 | # Don't scroll if outside the x-bounds of the event window
68 | return unless overflowOffset.left < event.pageX < overflowOffset.left + scrollParent[0].offsetWidth
69 |
70 | # Adapted from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/draggable.js
71 | if ((overflowOffset.top + scrollParent[0].offsetHeight) - event.pageY < scrollSensitivity)
72 | scrollParent[0].scrollTop = scrollParent[0].scrollTop + scrollSpeed;
73 | else if (event.pageY - overflowOffset.top < scrollSensitivity)
74 | scrollParent[0].scrollTop = scrollParent[0].scrollTop - scrollSpeed;
75 |
76 | Mapper.highlightEvents = ->
77 | Mapper.switchTab("events")
78 | $("#events").addClass("highlighted")
79 | Session.set("guidanceMessage", "Drop on an event below to attach this tweet.")
80 |
81 | Mapper.unhighlightEvents = ->
82 | $("#events").removeClass("highlighted")
83 | Session.set("guidanceMessage", undefined)
84 |
85 | # Highlighting and unhighlighting map can run automatically from a placing event
86 | Deps.autorun ->
87 | id = Session.get("placingEvent")
88 | if id
89 | $("#map").addClass("highlighted")
90 | Session.set("guidanceMessage", "Click to map a location for this event.")
91 | else
92 | $("#map").removeClass("highlighted")
93 | Session.set("guidanceMessage", undefined)
94 |
95 | Mapper.extent = Meteor.settings.public.map.extent
96 |
97 |
--------------------------------------------------------------------------------
/packages/analysis/client/routes.coffee:
--------------------------------------------------------------------------------
1 | adminRedirectURL = null
2 |
3 | # This controller handles the behavior of all admin templates
4 | class AdminController extends RouteController
5 | onRun: ->
6 | unless TurkServer.isAdmin()
7 | # Redirect to turkserver admin login
8 | adminRedirectURL = Router.current().url
9 | Router.go("/turkserver")
10 | else
11 | this.next()
12 |
13 | # Redirect to the appropriate path after login, if it was set; then remove.
14 | Tracker.autorun ->
15 | if Meteor.userId() and TurkServer.isAdmin() and adminRedirectURL?
16 | Router.go(adminRedirectURL)
17 | adminRedirectURL = null
18 |
19 | # TODO fix hack below that is used to avoid hitting the server twice
20 | # https://github.com/EventedMind/iron-router/issues/1011 and related
21 | class AdminDataController extends AdminController
22 | waitOn: ->
23 | return if @data()?
24 |
25 | args = @route.options.methodArgs.call(this)
26 | console.log "getting data"
27 |
28 | # Tack on callback argument
29 | args.push (err, res) =>
30 | bootbox.alert(err) if err
31 | console.log "got data"
32 | @state.set("data", res)
33 |
34 | Meteor.call.apply(null, args)
35 |
36 | return => @data()?
37 |
38 | data: -> @state.get("data")
39 |
40 | Router.map ->
41 | # Single-instance visualization templates
42 | @route 'viz',
43 | path: 'viz/:groupId/:type?/:layout?'
44 | controller: AdminDataController
45 | methodArgs: -> [ "cm-get-viz-data", this.params.groupId ]
46 |
47 | # Overview route, with access to experiments and stuff
48 | @route 'overview',
49 | controller: AdminController
50 | layoutTemplate: "overviewLayout"
51 |
52 | @route 'overviewExperiments',
53 | path: 'overview/experiments'
54 | controller: AdminController
55 | layoutTemplate: "overviewLayout"
56 | waitOn: ->
57 | Meteor.subscribe("cm-analysis-worlds", {
58 | pseudo: null,
59 | synthetic: null
60 | })
61 |
62 | @route 'overviewPeople',
63 | path: 'overview/people'
64 | controller: AdminController
65 | layoutTemplate: "overviewLayout"
66 | waitOn: -> Meteor.subscribe("cm-analysis-people")
67 |
68 | @route 'overviewTagging',
69 | path: 'overview/tagging'
70 | controller: AdminDataController
71 | layoutTemplate: "overviewLayout"
72 | methodArgs: -> [ "cm-get-group-cooccurences" ]
73 |
74 | @route 'overviewStats',
75 | path: 'overview/stats'
76 | controller: AdminDataController
77 | layoutTemplate: "overviewLayout"
78 | methodArgs: -> [ "cm-get-action-weights" ]
79 |
80 | @route 'overviewGroupScatter',
81 | path: 'overview/groupScatter'
82 | controller: AdminController
83 | layoutTemplate: "overviewLayout"
84 | waitOn: ->
85 | Meteor.subscribe("cm-analysis-worlds", {
86 | pseudo: null,
87 | synthetic: null
88 | })
89 |
90 | @route 'overviewGroupPerformance',
91 | path: 'overview/groupPerformance'
92 | controller: AdminController
93 | layoutTemplate: "overviewLayout"
94 | waitOn: ->
95 | Meteor.subscribe("cm-analysis-worlds")
96 |
97 | @route 'overviewGroupSlices',
98 | path: 'overview/groupSlices'
99 | controller: AdminController
100 | layoutTemplate: "overviewLayout"
101 | waitOn: ->
102 | Meteor.subscribe("cm-analysis-worlds", {
103 | pseudo: null,
104 | synthetic: null
105 | })
106 |
107 | @route 'overviewIndivPerformance',
108 | path: 'overview/indivPerformance'
109 | controller: AdminController
110 | layoutTemplate: "overviewLayout"
111 | waitOn: -> Meteor.subscribe("cm-analysis-people")
112 |
--------------------------------------------------------------------------------
/private/fields-pablo.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "key": "type",
4 | "name": "Type",
5 | "order": 1,
6 | "type": "dropdown",
7 | "choices": [
8 | "Damaged bridges",
9 | "Damaged crops",
10 | "Damaged hospitals/health facilities",
11 | "Damaged housing",
12 | "Damaged roads",
13 | "Damaged schools",
14 | "Damaged vehicles",
15 | "Damaged infrastructure (other)",
16 | "Death(s) reported",
17 | "Displaced population",
18 | "Evacuation center",
19 | "Flooding"
20 | ]
21 | },
22 | {
23 | "key": "description",
24 | "name": "Description",
25 | "order": 2,
26 | "type": "text"
27 | },
28 | {
29 | "key": "region",
30 | "name": "Region",
31 | "order": 3,
32 | "type": "dropdown",
33 | "choices": [
34 | "ARMM - Autonomous Region in Muslim Mindanao",
35 | "CAR - Cordillera Administrative Region",
36 | "NCR - National Capital Region",
37 | "REGION I (Ilocos Region)",
38 | "REGION II (Cagayan Valley)",
39 | "REGION III (Central Luzon)",
40 | "REGION IV-A (Calabarzon)",
41 | "REGION IV-B (Mimaropa)",
42 | "REGION V (Bicol Region)",
43 | "REGION VI (Western Visayas)",
44 | "REGION VII (Central Visayas)",
45 | "REGION VIII (Eastern Visayas)",
46 | "REGION IX (Zamboanga Peninsula)",
47 | "REGION X (Northern Mindanao)",
48 | "REGION XI (Davao Region)",
49 | "REGION XII (Soccsksargen)",
50 | "REGION XIII (Caraga)"
51 | ]
52 | },
53 | {
54 | "key": "province",
55 | "name": "Province",
56 | "order": 4,
57 | "type": "dropdown",
58 | "choices": [
59 | "Abra",
60 | "Agusan del Norte",
61 | "Agusan del Sur",
62 | "Aklan",
63 | "Albay",
64 | "Antique",
65 | "Apayao",
66 | "Aurora",
67 | "Basilan",
68 | "Bataan",
69 | "Batanes",
70 | "Batangas",
71 | "Benguet",
72 | "Biliran",
73 | "Bohol",
74 | "Bukidnon",
75 | "Bulacan",
76 | "Cagayan",
77 | "Camarines Norte",
78 | "Camarines Sur",
79 | "Camiguin",
80 | "Capiz",
81 | "Catanduanes",
82 | "Cavite",
83 | "Cebu",
84 | "Compostela Valley",
85 | "Cotabato",
86 | "Davao del Norte",
87 | "Davao del Sur",
88 | "Davao Oriental",
89 | "Dinagat Islands",
90 | "Eastern Samar",
91 | "Guimaras",
92 | "Ifugao",
93 | "Ilocos Norte",
94 | "Ilocos Sur",
95 | "Iloilo",
96 | "Isabela",
97 | "Kalinga",
98 | "La Union",
99 | "Laguna",
100 | "Lanao del Norte",
101 | "Lanao del Sur",
102 | "Leyte",
103 | "Maguindanao",
104 | "Marinduque",
105 | "Masbate",
106 | "Metro Manila",
107 | "Misamis Occidental",
108 | "Misamis Oriental",
109 | "Mountain Province",
110 | "Negros Occidental",
111 | "Negros Oriental",
112 | "Northern Samar",
113 | "Nueva Ecija",
114 | "Nueva Vizcaya",
115 | "Occidental Mindoro",
116 | "Oriental Mindoro",
117 | "Palawan",
118 | "Pampanga",
119 | "Pangasinan",
120 | "Quezon",
121 | "Quirino",
122 | "Rizal",
123 | "Romblon",
124 | "Samar",
125 | "Sarangani",
126 | "Siquijor",
127 | "Sorsogon",
128 | "South Cotabato",
129 | "Southern Leyte",
130 | "Sultan Kudarat",
131 | "Sulu",
132 | "Surigao del Norte",
133 | "Surigao del Sur",
134 | "Tarlac",
135 | "Tawi-Tawi",
136 | "Zambales",
137 | "Zamboanga del Norte",
138 | "Zamboanga del Sur",
139 | "Zamboanga Sibugay"
140 | ]
141 | }
142 | ]
143 |
--------------------------------------------------------------------------------
/packages/analysis/client/tagging.coffee:
--------------------------------------------------------------------------------
1 | skipEventSize = 20
2 |
3 | computeGraph = (occurrences, filter) ->
4 | occurrences = _.filter(occurrences, (o) -> o.length <= skipEventSize)
5 |
6 | # compute number of occurrences for each tweet
7 | nodes = d3.nest()
8 | .key(Object)
9 | .rollup( (leaves) -> leaves.length )
10 | .entries($.map(occurrences, Object))
11 |
12 | # Only consider tweets that were tagged at least once
13 | nodes = _.filter(nodes, (e) -> e.values > 1) if filter
14 |
15 | # Create a map of nodes for the next step (may be helpful later too)
16 | indices = {}
17 | for obj, i in nodes
18 | indices[obj.key] = i
19 |
20 | console.log "#{nodes.length} nodes"
21 |
22 | # Compute co-occurrences for each pair
23 |
24 | # Create a temporary 2-level associative array, indexing higher numbers first
25 | linkMap = {}
26 |
27 | for arr in occurrences
28 |
29 | for x, i in arr
30 | continue unless indices[x]
31 |
32 | j = i+1
33 |
34 | while j < arr.length
35 | y = arr[j]
36 | j++
37 | continue unless indices[y]
38 |
39 | [first, second] = if parseInt(x) > parseInt(y) then [x, y] else [y, x]
40 |
41 | linkMap[first] ?= {}
42 | linkMap[first][second] ?= { count: 0 }
43 | linkMap[first][second].count++
44 |
45 | links = []
46 |
47 | for k1, map of linkMap
48 | for k2, val of map
49 | links.push
50 | source: indices[k1]
51 | target: indices[k2]
52 | value: val.count
53 |
54 | console.log "#{links.length} edges"
55 |
56 | return [nodes, links]
57 |
58 | minNodeRadius = 3
59 | minCharge = -15
60 |
61 | Template.overviewTagging.rendered = ->
62 | tmpl = this
63 |
64 | occurrences = this.data.occurrences
65 | tweetText = this.data.tweetText
66 |
67 | console.log "#{occurrences.length} events"
68 |
69 | svg = @find("svg")
70 | width = $(svg).width()
71 | height = $(svg).height()
72 |
73 | gEdges = d3.select(svg).append("g")
74 | gNodes = d3.select(svg).append("g")
75 |
76 | strengthScale = d3.scale.pow().exponent(3)
77 |
78 | force = d3.layout.force()
79 | .size([width, height])
80 | # Bigger nodes should repel harder, but not too much harder...
81 | .charge( (d) -> minCharge * d.values )
82 | .linkDistance(15)
83 |
84 | @redrawGraph = (filterNodes) ->
85 | [nodes, links] = computeGraph(occurrences, filterNodes)
86 |
87 | maxStr = d3.max(nodes, (d) -> d.values)
88 |
89 | # The co-occurrence strength needs to be nonlinear
90 | # and rapidly increasing with for smaller values
91 | force.linkStrength( (e) -> 1 - strengthScale(1 - e.value / maxStr) )
92 | .nodes(nodes)
93 | .links(links)
94 |
95 | linkEls = gEdges.selectAll(".link")
96 | .data(links, (l) -> "#{l.source},#{l.target}" )
97 |
98 | linkEls.enter().append("line")
99 | .attr("class", "link")
100 | .style("stroke-width", (d) -> Math.sqrt(d.value) )
101 |
102 | linkEls.exit().remove()
103 |
104 | nodeEls = gNodes.selectAll(".node")
105 | .data(nodes, (n) -> n.key)
106 |
107 | nodeEls.enter().append("circle")
108 | .attr("class": "node")
109 | .attr("r", (d) -> minNodeRadius * Math.sqrt(d.values) )
110 | # .style("fill", function(d) { return color(d.group); })
111 | .call(force.drag)
112 | .append("title")
113 | .text( (d) -> "#{d.key} #{tweetText[parseInt(d.key)]}" )
114 |
115 | nodeEls.exit().remove()
116 |
117 | tmpl.link = linkEls
118 | tmpl.node = nodeEls
119 |
120 | force.start()
121 |
122 | # Redraw events
123 | force.on "tick", ->
124 | tmpl.link
125 | .attr("x1", (d) -> d.source.x )
126 | .attr("y1", (d) -> d.source.y )
127 | .attr("x2", (d) -> d.target.x )
128 | .attr("y2", (d) -> d.target.y )
129 | tmpl.node
130 | .attr("cx", (d) -> d.x)
131 | .attr("cy", (d) -> d.y)
132 |
133 | @redrawGraph(false) # With checked box
134 |
135 | Template.overviewTagging.events
136 | "change input[type=checkbox]": (e, t) ->
137 | t.redrawGraph(e.target.checked)
138 |
--------------------------------------------------------------------------------
/client/views/chat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Chat Rooms:
5 | New room
6 |
7 |
8 | {{> rooms}}
9 |
10 |
11 |
12 |
13 | {{#if currentRoom}}
14 | {{> roomUsers}}
15 | {{/if}}
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{#with currentRoom}}
23 | {{> roomHeader nameDoc }}
24 | {{> messageBox}}
25 | {{> chatInput}}
26 | {{/with}}
27 |
28 |
29 |
30 |
31 | {{#if loaded}}
32 |
33 | {{#each availableRooms}}
34 | {{> roomItem}}
35 | {{/each}}
36 |
37 | {{else}}
38 | {{> spinner}}
39 | {{/if}}
40 |
41 |
42 |
43 |
44 |
45 | {{users}}
46 | {{#if empty}}
47 |
48 |
49 |
50 | {{/if}}
51 | {{name}}
52 |
53 |
54 |
55 |
56 |
57 | In room:
58 |
59 | {{#each users}}{{> userPill findUser}} {{/each}}
60 |
61 |
62 |
63 |
64 |
70 |
71 |
72 |
73 |
74 | {{#if loaded}}
75 |
76 | {{#each messages}}
77 | {{> messageItem}}
78 | {{/each}}
79 |
80 | {{else}}
81 | {{> spinner}}
82 | {{/if}}
83 |
84 |
85 |
86 |
87 |
88 | {{#if text}}
89 | {{username}} : {{{renderText}}}
90 | {{else}}
91 | {{eventText}}
92 | {{/if}}
93 |
94 |
95 |
96 |
97 |
111 |
112 |
113 |
114 |
115 |
These special symbols create clickable references in chat:
116 |
Use @ to tag and mention a user .
117 |
Use ~ (tilde) to tag a tweet .
118 |
Use # to tag an event .
119 |
120 |
121 |
--------------------------------------------------------------------------------
/server/chat_server.coffee:
--------------------------------------------------------------------------------
1 | # Don't persist the contents of this collection
2 | @ChatUsers = new Mongo.Collection("chatusers") #, {connection: null})
3 |
4 | # Because it is in the DB, we can have this index
5 | ChatUsers._ensureIndex({roomId: 1})
6 |
7 | # Index chat messages by room and then by timestamp.
8 | # It will not be partitioned by TurkServer.
9 | ChatMessages._ensureIndex({ room: 1, timestamp: 1})
10 |
11 | # Managed by TurkServer; publish all chatrooms
12 | # Deleted chatrooms are filtered on the client
13 | Meteor.publish "chatrooms", -> ChatRooms.find()
14 |
15 | # Generalize what we are doing below
16 |
17 | enterRoom = (sessionId, roomId, userId) ->
18 | ChatUsers.upsert sessionId,
19 | $set: { userId, roomId }
20 |
21 | ChatRooms.update roomId,
22 | $inc: { users: 1 }
23 |
24 | TurkServer.log
25 | action: "room-enter"
26 | room: roomId
27 |
28 | leaveRoom = (sessionId, roomId, userId) ->
29 | # Remove the chatuser record unless they changed rooms
30 | ChatUsers.remove
31 | _id: sessionId
32 | roomId: roomId
33 |
34 | ChatRooms.update roomId,
35 | $inc: { users: -1 }
36 |
37 | # Because this is no longer a null connection, clear it on startup
38 | Meteor.startup ->
39 | ChatUsers.remove({})
40 |
41 | # Clear all users stored in chatrooms on start
42 | TurkServer.startup ->
43 | ChatRooms.update {},
44 | $set:
45 | {users: 0}
46 | , multi: true
47 |
48 | # Clean up any chat state when a user disconnects
49 | UserStatus.events.on "connectionLogout", (doc) ->
50 | # No groupId used here because ChatUsers and ChatRooms are not partitioned
51 | if (existing = ChatUsers.findOne(doc.sessionId))?
52 | leaveRoom(doc.sessionId, existing.roomId, doc.userId)
53 |
54 | # publish messages and users for a room
55 | Meteor.publish "chatstate", (room) ->
56 | userId = @userId
57 | return [] unless userId? # No chat for unauthenticated users
58 | sessionId = @_session.id
59 |
60 | # Don't update room state for admin
61 | unless Meteor.users.findOne(userId)?.admin
62 | # don't try to enter room if no room specified
63 | return [] unless room
64 |
65 | # Update room state - except for admin
66 | enterRoom(sessionId, room, userId)
67 |
68 | this.onStop ->
69 | # Leave this room when the subscription is stopped
70 | leaveRoom(sessionId, room, userId)
71 | else
72 | # Don't publish arbitrary rooms to admin
73 | return [] unless room
74 |
75 | return [
76 | ChatUsers.find(roomId: room),
77 | ChatMessages.find(room: room)
78 | ]
79 |
80 | userRegex = new RegExp('(^|\\b|\\s)(@[\\w.]+)($|\\b|\\s)','g')
81 |
82 | unreadNotificationExists = (user, sender, room, type) ->
83 | return Notifications.findOne({user, sender, room, type, read: {$exists: false} })?
84 |
85 | Meteor.methods
86 | inviteChat: (userId, roomId) ->
87 | TurkServer.checkNotAdmin()
88 | check(userId, String)
89 | check(roomId, String)
90 |
91 | myId = Meteor.userId()
92 | return unless myId?
93 | # Don't invite if user is already in the same room
94 | return if ChatUsers.findOne(userId: userId)?.roomId is roomId
95 |
96 | # Skip invite if this user already has an outstanding invitee to the other user to this room
97 | return if unreadNotificationExists(userId, myId, roomId, "invite")
98 |
99 | Notifications.insert
100 | user: userId
101 | sender: myId
102 | type: "invite"
103 | room: roomId
104 | timestamp: new Date()
105 |
106 | # No need to log this, we have it as a notification
107 | return
108 |
109 | sendChat: (roomId, message) ->
110 | TurkServer.checkNotAdmin()
111 | check(roomId, String)
112 | check(message, String)
113 |
114 | userId = Meteor.userId()
115 | return unless userId?
116 |
117 | chatTime = new Date()
118 |
119 | obj =
120 | room: roomId
121 | userId: userId
122 | text: message
123 | timestamp: chatTime # Attach server-side timestamps to chat messages
124 |
125 | ChatMessages.insert(obj)
126 | @unblock()
127 |
128 | # Parse and generate any notifications from this chat, using this regex ability
129 | message.replace userRegex, (_, p1, p2) ->
130 | targetUser = Meteor.users.findOne({username: p2.substring(1)})
131 | return unless targetUser?
132 | # Don't notify if user is already in the same room
133 | return if ChatUsers.findOne(userId: targetUser._id)?.roomId is roomId
134 |
135 | return if unreadNotificationExists(targetUser._id, userId, roomId, "mention")
136 |
137 | Notifications.insert
138 | user: targetUser._id
139 | sender: userId
140 | type: "mention"
141 | room: roomId
142 | timestamp: chatTime
143 |
--------------------------------------------------------------------------------
/packages/analysis/tagging_biclustering.py:
--------------------------------------------------------------------------------
1 | # print(__doc__)
2 |
3 | # Script to use scikit-learn for spectral co-clustering. Run this
4 | # after generating the analysis datasets from experimental data
5 |
6 | import sys
7 | import argparse
8 |
9 | parser = argparse.ArgumentParser(description='Run co-clustering from local database.')
10 |
11 | # We expect about this many events from Pablo
12 | parser.add_argument('--clusters', '-c', type=int, default=100,
13 | help='number of clusters')
14 |
15 | # Ignore events with too much stuff, no signal
16 | parser.add_argument('--threshold', '-t', type=int, default=None,
17 | help='threshold above which events are ignored')
18 |
19 | # Whether to write the clusters back to the database
20 | parser.add_argument('--write', '-w', action='store_const', const=True, default=False,
21 | help='write results to db')
22 |
23 | args = parser.parse_args()
24 |
25 | skip_thresh = args.threshold
26 | n_clusters = args.clusters
27 |
28 | import numpy as np
29 | from matplotlib import pyplot as plt
30 | import pymongo
31 | from pymongo import MongoClient
32 |
33 | from sklearn.cluster.bicluster import SpectralCoclustering
34 | # from sklearn.metrics import consensus_score
35 |
36 | client = MongoClient('localhost', 3001)
37 | db = client.meteor
38 |
39 | events = db['analysis.events']
40 | datastream = db['analysis.datastream']
41 |
42 | identifier = "i%d_c%d" % (skip_thresh, n_clusters) if skip_thresh else "c%d" % (n_clusters)
43 |
44 | # Build array of relationships between events and tweets
45 |
46 | n_rows = datastream.find().count()
47 | n_cols = events.find().count()
48 | shape = (n_rows, n_cols)
49 |
50 | # Should we really use float64 here?
51 | data = np.ones(shape, dtype=np.float64) * 0.001
52 |
53 | # Map tweets to a contiguous list, for now
54 | data_list = list(datastream.find().sort('num', pymongo.ASCENDING))
55 | events_list = list(events.find())
56 |
57 | row_lookup = {}
58 | for i, tweet in enumerate(data_list):
59 | row_lookup[tweet['num']] = i
60 |
61 | for j, event in enumerate(events_list):
62 | sources = event['sources']
63 | # Skip empty lists
64 | if not sources:
65 | continue
66 |
67 | # TODO hack: skip very long lists
68 | if skip_thresh and len(sources) > skip_thresh:
69 | continue
70 |
71 | # All events have numbered tweets
72 | rowSelector = np.array([row_lookup[source] for source in sources])
73 | data[rowSelector, j] = 1
74 |
75 | plt.matshow(data, cmap=plt.cm.Blues)
76 | plt.title("Original dataset")
77 |
78 | plt.savefig('%s_original.png' % (identifier), bbox_inches='tight')
79 |
80 | model = SpectralCoclustering(n_clusters=n_clusters, random_state=0)
81 | model.fit(data)
82 |
83 | fit_data = data[np.argsort(model.row_labels_)]
84 | fit_data = fit_data[:, np.argsort(model.column_labels_)]
85 |
86 | plt.matshow(fit_data, cmap=plt.cm.Blues)
87 | plt.title("After biclustering; rearranged")
88 |
89 | plt.savefig('%s_clustered.png' % (identifier), bbox_inches='tight')
90 |
91 | avg_data = np.copy(data)
92 |
93 | # Compute average value in each co-cluster for display purposes
94 | for c in range(n_clusters):
95 | for d in range(n_clusters):
96 | row_ind = np.nonzero(model.rows_[c])
97 | col_ind = np.nonzero(model.columns_[d])
98 | # print row_ind, col_ind
99 |
100 | row_sel = np.tile(row_ind, (col_ind[0].size, 1))
101 | col_sel = np.tile(col_ind, (row_ind[0].size, 1)).transpose()
102 | # print row_sel, col_sel
103 |
104 | avg_data[row_sel, col_sel] = np.average(data[row_sel, col_sel])
105 |
106 | avg_data = avg_data[np.argsort(model.row_labels_)]
107 | avg_data = avg_data[:, np.argsort(model.column_labels_)]
108 |
109 | plt.matshow(avg_data, cmap=plt.cm.Blues)
110 | plt.title("Average cluster intensity")
111 |
112 | plt.savefig('%s_averaged.png' % (identifier), bbox_inches='tight')
113 |
114 | if args.write:
115 | print "Writing clusters to database."
116 | # No need to clean up here, just overwrite by _id.
117 | for c in range(n_clusters):
118 | (nr, nc) = model.get_shape(c)
119 | (row_ind, col_ind) = model.get_indices(c)
120 |
121 | cluster_val = None
122 | if nr > 25 or nc > 50:
123 | print "Nulling cluster %d: shape (%d, %d)" % (c, nr, nc)
124 | else:
125 | cluster_val = c
126 |
127 | for ri in row_ind:
128 | data_list[ri]['cluster'] = cluster_val
129 | datastream.save(data_list[ri])
130 | for ci in col_ind:
131 | events_list[ci]['cluster'] = cluster_val
132 | events.save(events_list[ci])
133 |
134 | # plt.show()
135 |
--------------------------------------------------------------------------------
/packages/analysis/client/overview.coffee:
--------------------------------------------------------------------------------
1 | Template.overview.events
2 | "click .cm-analysis": (e) ->
3 | method = $(e.target).data("method")
4 |
5 | dialog = bootbox.dialog
6 | closeButton: false
7 | message: "Working... "
8 |
9 | Meteor.call method, (err, res) ->
10 | dialog.modal("hide")
11 | if err
12 | bootbox.alert(err)
13 | else
14 | bootbox.alert("done")
15 |
16 | "click .cm-download": (e) ->
17 | $target = $(e.target)
18 | Meteor.call $target.data("method"), $target.data("arg1"), $target.data("arg2"), (err, res) ->
19 | if err
20 | bootbox.alert(err)
21 | else
22 | # http://stackoverflow.com/a/18197511/586086
23 | pom = document.createElement('a')
24 | pom.setAttribute("href", "data:text/csv," + encodeURIComponent(res))
25 | pom.setAttribute("download", "data.csv")
26 | pom.click()
27 |
28 | Template.analysisExpLinks.helpers
29 | # Get the groupId associated with an analysis.world or analysis.person.
30 | id: -> @instanceId || @_id
31 |
32 | Template.overviewExperiments.helpers
33 | settings: {
34 | collection: Analysis.Worlds
35 | rowsPerPage: 100
36 | fields: [
37 | {
38 | key: "nominalSize"
39 | label: "nominal size"
40 | sort: 'descending' # Default sort order
41 | },
42 | {
43 | key: "wallTime"
44 | label: "wall time"
45 | fn: (v) -> v.toFixed(2)
46 | sortByValue: true
47 | },
48 | {
49 | key: "personTime"
50 | label: "person-time"
51 | fn: (v) -> v.toFixed(2)
52 | sortByValue: true
53 | },
54 | {
55 | key: "totalEffort"
56 | label: "effort-time"
57 | fn: (v) -> v.toFixed(2)
58 | sortByValue: true
59 | },
60 | {
61 | key: "personEffort"
62 | label: "effort/person"
63 | fn: (v, o) ->
64 | # Return a number so value is properly sorted
65 | +(o.totalEffort / o.personTime).toFixed(2)
66 | },
67 | {
68 | key: "treated"
69 | label: "valid treatment"
70 | },
71 | {
72 | key: "partialCreditScore"
73 | label: "partial score"
74 | fn: (v) -> v.toFixed(3)
75 | sortByValue: true
76 | },
77 | {
78 | key: "fullCreditScore"
79 | label: "0-1 score"
80 | },
81 | {
82 | key: "avgIndivEntropy"
83 | label: "mean indiv. entropy"
84 | fn: (v) -> v.toFixed(3)
85 | sortByValue: true
86 | },
87 | {
88 | key: "groupEntropy"
89 | label: "collective entropy"
90 | fn: (v) -> v.toFixed(3)
91 | sortByValue: true
92 | },
93 | {
94 | key: "links"
95 | label: "links"
96 | tmpl: Template.analysisExpLinks
97 | }
98 | ]
99 | }
100 |
101 | Template.overviewPeople.helpers
102 | settings: {
103 | collection: Analysis.People
104 | rowsPerPage: 100
105 | fields: [
106 | {
107 | key: "age"
108 | label: "age"
109 | },
110 | {
111 | key: "gender"
112 | label: "gender"
113 | },
114 | {
115 | key: "groupSize"
116 | label: "group size"
117 | },
118 | {
119 | key: "time"
120 | label: "active time"
121 | fn: (v) -> v.toFixed(2)
122 | sortByValue: true
123 | },
124 | {
125 | key: "effort"
126 | label: "effort-time"
127 | fn: (v) -> v.toFixed(2)
128 | sortByValue: true
129 | },
130 | {
131 | key: "normalizedEffort"
132 | label: "effort/time"
133 | fn: (v, o) ->
134 | # Return a number so value is properly sorted
135 | +(o.effort / o.time).toFixed(2)
136 | },
137 | {
138 | key: "treated"
139 | label: "valid treatment"
140 | },
141 | {
142 | key: "tutorialWords"
143 | label: "tut. response words"
144 | },
145 | {
146 | key: "tutorialMins"
147 | label: "tut. time mins"
148 | fn: (v) -> v.toFixed(2)
149 | sortByValue: true
150 | },
151 | {
152 | key: "exitSurveyWords"
153 | label: "exit survey words"
154 | },
155 | {
156 | key: "links"
157 | label: "links"
158 | tmpl: Template.analysisExpLinks
159 | }
160 |
161 | ]
162 | }
163 |
164 | Template.overviewStats.helpers
165 | actionArray: -> ({action: k, time: v} for k, v of this)
166 | settings: {
167 | rowsPerPage: 50
168 | fields: [
169 | {
170 | key: "action"
171 | label: "action"
172 | },
173 | {
174 | key: "time"
175 | label: "mean time since previous action (s)"
176 | fn: (v) -> (v / 1000).toFixed(2)
177 | sortByValue: true
178 | }
179 | ]
180 | }
181 |
182 | Template.sizeLegend.helpers
183 | color: Util.groupColor
184 |
--------------------------------------------------------------------------------
/server/firstrun.coffee:
--------------------------------------------------------------------------------
1 | Meteor.startup ->
2 | return if EventFields.find().count() > 0
3 |
4 | pabloFields = Assets.getText("fields-pablo.json")
5 |
6 | EventFields.insert(field) for field in JSON.parse(pabloFields)
7 |
8 | # Set up treatments
9 | Meteor.startup ->
10 | TurkServer.ensureTreatmentExists
11 | name: "tutorial"
12 | tutorial: "pre_task"
13 | tutorialEnabled: true
14 | payment: 1.00
15 |
16 | TurkServer.ensureTreatmentExists
17 | name: "recruiting"
18 | tutorial: "recruiting"
19 | tutorialEnabled: true
20 | payment: 1.00
21 |
22 | TurkServer.ensureTreatmentExists
23 | name: "parallel_worlds"
24 | wage: 6.00
25 | bonus: 9.00
26 |
27 | # Create Assigner on recruiting batch, if it exists
28 | if (batch = Batches.findOne(treatments: $in: [ "recruiting" ]))?
29 | # Enable re-attempts on recruiting batch if returned
30 | Batches.update batch._id,
31 | $set: allowReturns: true
32 |
33 | TurkServer.Batch.getBatch(batch._id).setAssigner(new TurkServer.Assigners.SimpleAssigner)
34 | console.log "Set up assigner on recruiting batch"
35 |
36 | # Ensure we have a hit type on this batch
37 | HITTypes.upsert {
38 | batchId: batch._id,
39 | Title: "Complete a tutorial for the Crisis Mapping Project"
40 | }, {
41 | $setOnInsert: {
42 | Description: "Complete a tutorial for the Crisis Mapping Project, which takes about 10 minutes. After you complete this, you will be qualified to participate in collaborative Crisis Mapping sessions, which will pay from $6 to $15 per hour. You may see some disturbing content from natural disasters.
43 |
44 | You cannot do this HIT if you've done it before. If you accept it again, you will be asked to return it."
45 | Keywords: "crisis mapping, tutorial, collaborative"
46 | Reward: 1.00
47 | QualificationRequirement: [
48 | Qualifications.findOne({ # 95%
49 | QualificationTypeId: "000000000000000000L0"
50 | Comparator: "GreaterThanOrEqualTo"
51 | IntegerValue: "95"
52 | })._id
53 | Qualifications.findOne({ # 100 HITs
54 | QualificationTypeId: "00000000000000000040"
55 | Comparator: "GreaterThan"
56 | IntegerValue: "100"
57 | })._id
58 | Qualifications.findOne({ # US Worker
59 | QualificationTypeId: "00000000000000000071"
60 | Comparator: "EqualTo"
61 | LocaleValue: "US"
62 | })._id
63 | Qualifications.findOne({ # Adult Worker
64 | QualificationTypeId: "00000000000000000060"
65 | Comparator: "EqualTo"
66 | IntegerValue: "1"
67 | })._id
68 | ]
69 | AssignmentDurationInSeconds: 43200
70 | AutoApprovalDelayInSeconds: 86400
71 | }
72 | }
73 |
74 | # Set up pilot testing batch - currently disabled
75 | if Meteor.settings.pilot
76 |
77 | TurkServer.ensureBatchExists
78 | name: "pilot testing"
79 |
80 | pilotBatchId = Batches.findOne(name: "pilot testing")._id
81 |
82 | Batches.upsert pilotBatchId,
83 | $addToSet: { treatments: "parallel_worlds" }
84 |
85 | pilotBatch = TurkServer.Batch.getBatch(pilotBatchId)
86 | pilotBatch.setAssigner new TurkServer.Assigners.TutorialGroupAssigner(
87 | [ "tutorial" ], [ "parallel_worlds" ]
88 | )
89 | console.log "Set up pilot testing assigner"
90 |
91 | ###
92 | Set up group size experiments! Yay!
93 |
94 | Old group size batch was "group sizes" which we are discarding, because no
95 | one got through it during this server crash.
96 |
97 | This is the new one.
98 | ###
99 | groupSizeBatchName = "group sizes redux"
100 |
101 | TurkServer.ensureBatchExists
102 | name: groupSizeBatchName
103 |
104 | groupSizeBatchId = Batches.findOne(name: groupSizeBatchName)._id
105 |
106 | # Needed to trigger the right preview/exit survey
107 | Batches.upsert groupSizeBatchId,
108 | $addToSet: { treatments: "parallel_worlds" }
109 |
110 | groupBatch = TurkServer.Batch.getBatch(groupSizeBatchId)
111 | # 8x1, 4x2, 2x4, 1x8, 1x16, 1x32
112 | groupArray = [
113 | 1, 1, 1, 1, 1, 1, 1, 1,
114 | 2, 2, 2, 2,
115 | 4, 4,
116 | 8, 16, 32
117 | ]
118 |
119 | groupAssigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner(
120 | [ "tutorial" ], [ "parallel_worlds" ], groupArray)
121 |
122 | groupBatch.setAssigner(groupAssigner)
123 |
124 | Meteor._debug "Set up group size assigner"
125 |
126 | Meteor.methods
127 | "cm-delete-world-data": (worldId) ->
128 | TurkServer.checkAdmin()
129 |
130 | if Experiments.remove(worldId)
131 | Partitioner.bindGroup worldId, ->
132 | Events.remove({})
133 | Datastream.remove({})
134 |
135 | return
136 |
137 | # Load gold standard data if it exists
138 | tryImport = (worldName) ->
139 | return if Experiments.findOne(worldName)?
140 | result = JSON.parse Assets.getText("#{worldName}.json")
141 |
142 | Experiments.upsert({worldName}, $set: { treatments: [ "editable" ] })
143 |
144 | for event in result.events
145 | event._groupId = worldName
146 | Events.direct.insert(event)
147 |
148 | for data in result.datastream
149 | data._groupId = worldName
150 | Datastream.direct.insert(data)
151 |
152 | console.log "Imported #{worldName}; events: #{result.events.length}, datastream: #{result.datastream.length}"
153 |
154 | Meteor.startup -> tryImport("groundtruth-pablo")
155 | Meteor.startup -> tryImport("sbtf-pablo")
156 |
--------------------------------------------------------------------------------
/client/meta/exitsurvey.html:
--------------------------------------------------------------------------------
1 |
2 | {{> surveyTemplate}}
3 |
4 |
5 |
6 | Loading survey...
7 |
8 |
9 |
10 |
61 |
62 |
63 |
64 |
65 | Thank you for your work!
66 |
67 | Please briefly answer the questions below. Your feedback will help us improve crisis mapping and learn how to better respond to natural disasters in the future.
68 |
69 |
70 | Your age:
71 |
72 |
73 |
74 |
75 | Your gender:
76 |
77 |
78 | Male
79 | Female
80 |
81 |
82 |
83 |
84 | What was your general impression of the task, and how did you approach the overall crisis mapping problem?
85 |
86 |
87 |
88 |
89 | During the task, did you specialize in any particular type of work?
90 |
91 |
92 |
93 | If you were the only person on your team (a team of one), enter N/A for the next three questions and skip to the last question.
94 |
95 |
96 | Did your team adopt any strategies for tackling the problem? Were these strategies effective?
97 |
98 |
99 |
100 |
101 | Did you work with anyone else specifically on certain tasks?
102 |
103 |
104 |
105 |
106 | Did you or anyone else take charge of organizing other team members?
107 |
108 |
109 |
110 |
111 | Any other comments? If you ran into any bugs, please describe them here.
112 |
113 | If you encountered any bugs, please include your operating system and browser in the description.
114 |
115 |
116 |
117 | We ask that you please do not discuss the specifics of the task in online forums, so that we can maintain the integrity of our research and improve crisis mapping for both you and future participants. If you have any questions about this project, please feel free to e-mail us directly.
118 |
119 |
120 | Submit HIT
121 |
122 |
123 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 | CrowdMapper
3 |
4 |
5 |
6 |
7 |
8 | {{> landingTemplate}}
9 |
10 |
11 |
12 |
13 |
14 | Waiting for preview to load...
15 |
16 |
17 |
18 | This is the task for the Crisis Mapping Project.
19 |
20 | Accept the HIT to participate in crisis mapping. You will first complete a quick tutorial, as in the previous qualification task. Then, you will create a crisis map with actual crisis data, and you will earn $6 to $15 an hour based on your work.
21 |
22 |
23 |
24 | {{> tut_welcome_recruiting}}
25 |
26 | After completing this HIT, you will gain a qualification for crisis mapping tasks where you will collaborate with other workers and earn from $6 to $15 an hour.
27 |
28 | Accept the HIT to complete the tutorial, which will take about 10 minutes.
29 |
30 |
31 |
32 |
33 | {{> yield}}
34 |
35 |
36 |
37 |
38 |
39 |
Waiting for login...
40 |
41 |
42 |
43 |
44 |
45 |
Error loading the task. Please try reloading this HIT from your dashboard.
46 |
47 |
48 |
49 |
50 | {{> turkserverPulldown include=adminControls }}
51 | {{> mapperTutorial}}
52 |
53 |
54 |
55 |
56 | {{> datastream}}
57 |
58 |
59 | {{> guidance}}
60 | {{> pageNav}}
61 |
62 | {{> docs}}
63 |
64 |
65 | {{> eventRecords}}
66 |
67 |
68 | {{> map}}
69 |
70 |
71 |
72 | {{> sidebar}}
73 |
74 |
75 |
76 |
77 |
78 |
79 | {{#if tutorialEnabled}}
80 | {{> tutorial options}}
81 | {{/if}}
82 |
83 |
84 |
85 | {{#with message}}
86 |
87 |
{{this}}
88 |
89 | {{/with}}
90 |
91 |
92 |
93 |
94 |
105 |
106 |
107 |
108 | {{> notifications}}
109 |
110 | {{> payment treatment}}
111 | {{> help treatment }}
112 |
113 |
114 |
115 |
116 |
117 | Tutorial: {{amount}}
118 |
119 |
120 |
121 |
122 |
123 | Payment: {{amount}}
124 |
125 |
126 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Help
138 |
139 |
140 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | {{> userList}}
158 |
159 |
160 |
161 |
162 | {{> chat}}
163 |
164 |
165 |
166 |
167 | {{> currentChatroom}}
168 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/client/views/common.coffee:
--------------------------------------------------------------------------------
1 | urlExp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig
2 |
3 | UI.registerHelper "replaceURLs", (text) ->
4 | # Shim so old links aren't broken, can be removed later for speed
5 | return text if text.indexOf("target='_blank'>") > -1
6 | text.replace(urlExp, "$1 ")
7 |
8 | Template.userList.helpers
9 | loaded: -> Session.equals("userSubReady", true)
10 | users: -> Meteor.users.find()
11 |
12 | Template.userPill.helpers
13 | labelClass: ->
14 | if @_id is Meteor.userId()
15 | "inverse"
16 | else if @status?.online
17 | "success"
18 | else "default"
19 |
20 | Template.userPill.events =
21 | "click .action-chat-invite": (e) ->
22 | myId = Meteor.userId()
23 | unless myId?
24 | bootbox.alert("You must be logged in to invite others to chat.")
25 | return
26 |
27 | myRoom = Session.get("room")
28 | unless myRoom?
29 | bootbox.alert("Join a chat room first to invite someone to chat with you.")
30 | return
31 |
32 | user = Blaze.getData(e.target)
33 |
34 | if ChatUsers.findOne(userId: user._id)?
35 | bootbox.alert("You and #{user.username} are already in the same room.")
36 | return
37 |
38 | bootbox.confirm "Invite #{user.username} to join you in " + ChatRooms.findOne(myRoom).name + " ?"
39 | , (result) ->
40 | Meteor.call "inviteChat", user._id, myRoom if result
41 |
42 | # String conversion needed: https://github.com/meteor/meteor/issues/1447
43 | Handlebars.registerHelper "findTweet", -> Datastream.findOne(""+@)
44 |
45 | Handlebars.registerHelper "lookupUser", -> Meteor.users.findOne(""+@)
46 |
47 | ###
48 | XXX tweet icon mouseover and dragging is handled at the global page level and the
49 | event page level respectively
50 | ###
51 |
52 | Template.tweetIcon.events =
53 | "click .action-unlink-tweet": (e) ->
54 | # This needs to work on both events and map
55 | # both the table row and the popup are .event-record
56 | tweet = Blaze.getData(e.target)
57 | event = Blaze.getData $(e.target).closest(".event-record")[0]
58 |
59 | # The unlink function will also hide this if it's not tagged somewhere
60 | # TODO: the unlink-hide process causes an unwanted scroll adjustment
61 | Meteor.call "dataUnlink", tweet._id, event._id
62 |
63 | # Mapping helpers
64 | epsg4326 = new OpenLayers.Projection("EPSG:4326")
65 | epsg900913 = new OpenLayers.Projection("EPSG:900913")
66 |
67 | transformLocation = (location) ->
68 | point = new OpenLayers.Geometry.Point(location[0], location[1])
69 | point.transform(epsg900913, epsg4326)
70 | return [point.x, point.y]
71 |
72 | formatLocation = (location) ->
73 | return "" unless location
74 | [x, y] = transformLocation(location)
75 | return x.toFixed(2) + ", " + y.toFixed(2)
76 |
77 | Handlebars.registerHelper "formatLocation", -> formatLocation(@location)
78 |
79 | transformLongLat = (longlat) ->
80 | point = new OpenLayers.Geometry.Point(longlat[0], longlat[1])
81 | point.transform(epsg4326, epsg900913)
82 | return [point.x, point.y]
83 |
84 | minLongLat = transformLocation([Mapper.extent[0], Mapper.extent[1]])
85 | maxLongLat = transformLocation([Mapper.extent[2], Mapper.extent[3]])
86 |
87 | entryPrecision = 3
88 |
89 | Template.longLatEntry.helpers
90 | minLong: minLongLat[0].toFixed(entryPrecision)
91 | maxLong: maxLongLat[0].toFixed(entryPrecision)
92 | minLat: minLongLat[1].toFixed(entryPrecision)
93 | maxLat: maxLongLat[1].toFixed(entryPrecision)
94 |
95 | # LongLat editable
96 | # see http://vitalets.github.io/x-editable/assets/x-editable/inputs-ext/address/address.js
97 |
98 | LongLat = (options) ->
99 | @init("longlat", options, LongLat.defaults)
100 | return
101 |
102 | # inherit from Abstract input
103 | $.fn.editableutils.inherit(LongLat, $.fn.editabletypes.abstractinput)
104 | $.extend LongLat.prototype, {
105 |
106 | ###
107 | Renders input from tpl
108 |
109 | @method render()
110 | ###
111 | render: ->
112 | @$input = @$tpl.find("input")
113 | return
114 |
115 | ###
116 | Default method to show value in element. Can be overwritten by display option.
117 |
118 | @method value2html(value, element)
119 | ###
120 | value2html: (value, element) ->
121 | unless value
122 | $(element).empty()
123 | return
124 | # Call above function to render
125 | $(element).html formatLocation(value)
126 | return
127 |
128 | ###
129 | Gets value from element's html
130 | We set value directly via javascript.
131 |
132 | @method html2value(html)
133 | ###
134 | html2value: (html) -> null
135 |
136 | ###
137 | Converts value to string.
138 | It is used in internal comparing (not for sending to server).
139 |
140 | @method value2str(value)
141 | ###
142 | value2str: formatLocation # This will ignore manual changes to the third decimal place.
143 |
144 | ###
145 | Converts string to value. Used for reading value from 'data-value' attribute.
146 |
147 | this is mainly for parsing value defined in data-value attribute.
148 | If you will always set value by javascript, no need to overwrite it
149 |
150 | @method str2value(str)
151 | ###
152 | str2value: (str) -> str
153 |
154 | ###
155 | Sets value of input.
156 |
157 | @method value2input(value)
158 | @param {mixed} value
159 | ###
160 | value2input: (value) ->
161 | return unless value
162 | transformed = transformLocation(value)
163 | @$input.filter('[name="long"]').val transformed[0].toFixed(entryPrecision)
164 | @$input.filter('[name="lat"]').val transformed[1].toFixed(entryPrecision)
165 | return
166 |
167 | ###
168 | Returns value of input.
169 |
170 | @method input2value()
171 | ###
172 | input2value: ->
173 | transformLongLat [
174 | @$input.filter('[name="long"]').val(),
175 | @$input.filter('[name="lat"]').val()
176 | ]
177 |
178 | ###
179 | Activates input: sets focus on the first field.
180 |
181 | @method activate()
182 | ###
183 | activate: ->
184 | @$input.filter('[name="long"]').focus()
185 | return
186 |
187 | ###
188 | Attaches handler to submit form in case of 'showbuttons=false' mode
189 |
190 | @method autosubmit()
191 | ###
192 | autosubmit: ->
193 | @$input.keydown (e) ->
194 | $(this).closest("form").submit() if e.which is 13
195 | return
196 | return
197 | }
198 |
199 | LongLat.defaults = $.extend {}, $.fn.editabletypes.abstractinput.defaults,
200 | tpl: Blaze.toHTML Template.longLatEntry # No reactive contents
201 | inputclass: ""
202 |
203 | $.fn.editabletypes.longlat = LongLat
204 |
--------------------------------------------------------------------------------
/packages/analysis/client/groupSlices.coffee:
--------------------------------------------------------------------------------
1 | height = 700
2 | leftMargin = 80
3 | bottomMargin = 50
4 |
5 | Template.overviewGroupSlices.helpers({
6 | height: height
7 | leftMargin: leftMargin
8 | bottomOffset: height - bottomMargin
9 | points: -> Analysis.Worlds.find({treated: true})
10 | treatments: -> this.treatments.join(" ")
11 | xLabelPosition: 450
12 | yLabelPosition: (height - bottomMargin) / 2
13 | settingOf: (key) ->
14 | val = Template.instance().settings.get(key)
15 | return val.toFixed(2) if $.isNumeric(val)
16 | return val
17 | })
18 |
19 | Template.overviewGroupSlices.created = ->
20 | @settings = new ReactiveDict
21 |
22 | Template.overviewGroupSlices.rendered = ->
23 | @slider = this.$(".slider").slider({
24 | min: 0
25 | step: 0.01
26 | slide: (event, ui) =>
27 | # This updates faster than the reactive bits
28 | this.$(".slider-immediate").text(ui.value)
29 | change: (event, ui) =>
30 | this.$(".slider-immediate").text(ui.value)
31 | @settings.set("sliceValue", ui.value)
32 | })
33 |
34 | @setField = (field, value, label) ->
35 | @settings.set(field, value)
36 | # Update y axis label if necessary
37 | if field is "groupScoring" then @settings.set("yLabel", label)
38 |
39 | # default settings - change in template; fields must match here
40 | for field in [ "groupScoring", "xScale", "groupComparator" ]
41 | $input = @$("input[name=#{field}]:checked")
42 | value = $input.val()
43 | label = $input.closest("label").text().trim()
44 | @setField(field, value, label)
45 |
46 | svg = @find("svg")
47 |
48 | graphWidth = $(svg).width() - leftMargin
49 | graphHeight = $(svg).height() - bottomMargin
50 |
51 | x = null
52 |
53 | y = d3.scale.linear()
54 | .range([graphHeight, 0])
55 |
56 | # Median line draw-er.
57 | line = d3.svg.line()
58 | .x((d) -> x(d.key))
59 | .y((d) -> y(d.values))
60 |
61 | xAxis = d3.svg.axis()
62 | .orient("bottom")
63 |
64 | xGrid = d3.svg.axis()
65 | .orient("bottom")
66 | .tickSize(-graphHeight, 0, 0)
67 | .tickFormat("")
68 |
69 | yAxis = d3.svg.axis()
70 | .orient("left")
71 | .scale(y)
72 |
73 | yGrid = d3.svg.axis()
74 | .orient("left")
75 | .scale(y)
76 | .tickSize(-graphWidth, 0, 0)
77 | .tickFormat("")
78 |
79 | transDuration = 600
80 |
81 | # Set slider value appropriately and a default slice value.
82 | this.autorun =>
83 | xField = Util.fieldAbbr[@settings.get("groupComparator")]
84 |
85 | data = d3.select(svg).selectAll(".point").data()
86 |
87 | # min-max is largest value for which all groups have this field
88 | [sliceVal, sliceMax] = d3.extent data, (g) ->
89 | g.progress[g.progress.length - 1][xField]
90 |
91 | # Special case: don't let wallTime go over 1
92 | sliceMax = Math.min(sliceMax, 1) if xField is "wt"
93 |
94 | # set new slice value, which will update the function below.
95 | @slider.slider "option",
96 | max: sliceMax
97 | value: sliceVal # Propagates through the callback
98 |
99 | @settings.set("sliceMax", sliceMax)
100 |
101 | return
102 |
103 | # Display x scale appropriately.
104 | this.autorun =>
105 | xScale = @settings.get("xScale")
106 |
107 | if xScale is "linear"
108 | x = d3.scale.linear()
109 | .domain([0, 33])
110 | .range([0, graphWidth])
111 | else
112 | x = d3.scale.log()
113 | .base(2)
114 | .domain([0.5, 40])
115 | .range([0, graphWidth])
116 |
117 | xAxis.tickValues([1, 2, 4, 8, 16, 32])
118 | xAxis.tickFormat(x.tickFormat(6, "2d"))
119 | xAxis.scale(x)
120 | xGrid.scale(x)
121 |
122 | # Transition x axis and grid.
123 | d3.select(svg)
124 | .transition()
125 | .duration(transDuration)
126 | .each ->
127 |
128 | d3.select(this).selectAll(".x.axis")
129 | .transition().call(xAxis)
130 | d3.select(this).selectAll(".x.grid")
131 | .transition().call(xGrid)
132 |
133 | # Update axes and transition the points.
134 | this.autorun =>
135 | xScale = @settings.get("xScale") # redraw if this changes.
136 |
137 | xField = Util.fieldAbbr[@settings.get("groupComparator")]
138 | yField = Util.fieldAbbr[@settings.get("groupScoring")]
139 |
140 | sliceVal = @settings.get("sliceValue")
141 |
142 | points = d3.select(svg).selectAll(".point")
143 | pointData = points.data()
144 |
145 | # Compute new interpolated values in-place while simultaneously returning them.
146 | yMax = d3.max pointData, (g) ->
147 | g.interp = Util.interpolateArray(g.progress, xField, yField, sliceVal)
148 |
149 | # Run linear regression on points that exist
150 | validPoints = pointData.filter (g) -> g.interp?
151 |
152 | regData = validPoints.map (g) ->
153 | [ (if xScale is "log" then Math.log(g.nominalSize)/Math.LN2 else g.nominalSize), g.interp ]
154 |
155 | # Run simple linear regression
156 | reg = ss.linear_regression().data(regData)
157 | regLine = reg.line()
158 | r2 = ss.r_squared(regData, regLine)
159 |
160 | indepVarText = if xScale is "log" then "log2(groupSize)" else "groupSize"
161 |
162 | regText = """#{@settings.get("groupScoring")} = #{reg.m().toFixed(2)} x #{indepVarText} + #{reg.b().toFixed(2)}, R^2 = #{r2.toFixed(4)}"""
163 |
164 | # Shared transition.
165 | d3.select(svg)
166 | .transition()
167 | .duration(transDuration)
168 | .each ->
169 | # Transition y axis
170 | y.domain([0, yMax * 1.1])
171 |
172 | d3.select(this).selectAll(".y.axis")
173 | .transition().call(yAxis)
174 |
175 | d3.select(this).selectAll(".y.grid")
176 | .transition().call(yGrid)
177 |
178 | # Update point values
179 | points.transition().attr({
180 | cx: (g) -> x(g.nominalSize)
181 | cy: (g) -> y(g.interp || 0)
182 | })
183 |
184 | # Transition median line, using only values that exist
185 | medians = d3.nest()
186 | .key((g) -> g.nominalSize ).sortKeys( (a, b) -> a - b)
187 | .rollup((leaves) -> d3.median(leaves, (g) -> g.interp) )
188 | .entries( validPoints )
189 |
190 | d3.select(this).selectAll(".line.median").datum(medians)
191 | .transition().attr("d", line)
192 |
193 | # Transition regression line
194 | if xScale is "log"
195 | y1 = y(regLine( Math.log(x.domain()[0]) / Math.LN2 ))
196 | y2 = y(regLine( Math.log(x.domain()[1]) / Math.LN2 ))
197 | else
198 | y1 = y(regLine(x.domain()[0]))
199 | y2 = y(regLine(x.domain()[1]))
200 |
201 | d3.select(this).selectAll(".line.regression")
202 | .transition().attr({
203 | "x1": x.range()[0]
204 | "y1": y1
205 | "x2": x.range()[1]
206 | "y2": y2
207 | })
208 |
209 | d3.select(this).selectAll("text.regression").text(regText)
210 |
211 | Template.overviewGroupSlices.events
212 | "change input": (e, t) ->
213 | t.setField(e.target.name, e.target.value, $(e.target).closest("label").text().trim())
214 |
215 | Template.groupSlicePoint.rendered = ->
216 | # Bind the meteor data to d3's datum.
217 | point = d3.select(this.firstNode)
218 | point.datum(@data)
219 |
220 |
221 |
--------------------------------------------------------------------------------
/packages/analysis/client/groupScatter.coffee:
--------------------------------------------------------------------------------
1 | labels = {
2 | avgIndivEntropy: "Average Individual Entropy"
3 | groupEntropy: "Group Entropy"
4 | nominalSize: "Nominal Group Size"
5 | partialCreditScore: "Group Score"
6 | fullCreditScore: "Group 0-1 Score"
7 | precision: "Precision"
8 | recall: "Recall"
9 | f1: "F1 Score"
10 | }
11 |
12 | # Custom values computed from the data
13 | transform = {
14 | f1: (d) -> 2 * d.precision * d.recall / (d.precision + d.recall)
15 | }
16 |
17 | accessor = (key) ->
18 | return transform[key] if transform[key]
19 | return (d) -> d[key]
20 |
21 | Template.overviewGroupScatter.helpers
22 | labels: _.map(labels, (v, k) -> { key: k, value: v } )
23 |
24 | Template.overviewGroupScatter.rendered = ->
25 | svg = @find("svg")
26 |
27 | leftMargin = 80
28 | bottomMargin = 50
29 |
30 | graphWidth = $(svg).width() - leftMargin
31 | graphHeight = $(svg).height() - bottomMargin
32 |
33 | colors = d3.scale.category10()
34 | .domain( [0...10] )
35 |
36 | graph = d3.select(svg).append("g")
37 | .attr("class", "graph")
38 | .attr("transform", "translate(#{leftMargin}, 0)")
39 |
40 | # TODO generalize this for transitions
41 | filteredData = null
42 | xKey = null
43 | yKey = null
44 | displayOrdinal = false
45 | showBoxes = false
46 |
47 | filterData = =>
48 | displayOrdinal = xKey is "nominalSize" and showBoxes
49 |
50 | # if displayOrdinal
51 | # filteredData = Analysis.Worlds.find({
52 | # nominalSize: { $gt: 1 }
53 | # treated: true
54 | # }).fetch()
55 | # else
56 | filteredData = Analysis.Worlds.find().fetch()
57 |
58 | # TODO allow selection of circle radius
59 | # radius = (g) -> 2 * Math.sqrt(g.partialCreditScore)
60 | radius = 5
61 |
62 | chart = d3.box()
63 | .whiskers(Util.iqrFun(1.5))
64 | .height(graphHeight)
65 | .showLabels(false)
66 |
67 | # x scale, to be set later with linear or ordinal
68 | x = null
69 |
70 | y = d3.scale.linear()
71 | .range([graphHeight, 0])
72 |
73 | xAxis = d3.svg.axis()
74 | .orient("bottom")
75 |
76 | xAxisGrid = d3.svg.axis()
77 | .orient("bottom")
78 | .tickSize(-graphHeight, 0, 0)
79 | .tickFormat("")
80 |
81 | yAxis = d3.svg.axis()
82 | .orient("left")
83 | .scale(y)
84 |
85 | yAxisGrid = d3.svg.axis()
86 | .orient("left")
87 | .scale(y)
88 | .tickSize(-graphWidth, 0, 0)
89 | .tickFormat("")
90 |
91 | d3.select(svg).append("g")
92 | .attr("class", "x axis")
93 | .attr("transform", "translate(#{leftMargin}, #{graphHeight})")
94 | .append("text")
95 | .attr("x", (graphWidth / 2))
96 | .attr("y", 30)
97 | .attr("dy", ".71em")
98 |
99 | d3.select(svg).append("g")
100 | .attr("class", "x grid")
101 | .attr("transform", "translate(#{leftMargin}, #{graphHeight})")
102 |
103 | d3.select(svg).append("g")
104 | .attr("class", "y axis")
105 | .attr("transform", "translate(#{leftMargin}, 0)")
106 | .append("text")
107 | .attr("transform", "rotate(-90)")
108 | .attr("x", -(graphHeight / 2))
109 | .attr("y", -55)
110 | .attr("dy", ".71em")
111 |
112 | d3.select(svg).append("g")
113 | .attr("class", "y grid")
114 | .attr("transform", "translate(#{leftMargin}, 0)")
115 |
116 | groupColor = (size) -> colors(Math.log(size) / Math.LN2)
117 |
118 | # Redraw X axis
119 | redrawX = ->
120 | xExtent = d3.extent(filteredData, accessor(xKey) )
121 |
122 | if displayOrdinal
123 | domain = _.uniq( filteredData.map((d) -> d.nominalSize) )
124 | .sort( (a,b) -> a-b )
125 |
126 | x = d3.scale.ordinal()
127 | .domain(domain)
128 | .rangeRoundBands([0, graphWidth], 0.6)
129 |
130 | chart.width(x.rangeBand())
131 |
132 | else
133 | # Start domain at 0 for group size
134 | xExtent[0] = if xKey is "nominalSize" then 0 else xExtent[0] * 0.9
135 | xExtent[1] *= 1.1
136 |
137 | x = d3.scale.linear()
138 | .domain(xExtent)
139 | .range([0, graphWidth])
140 |
141 | # Because x axis may have been replaced, update references
142 | xAxis.scale(x)
143 | xAxisGrid.scale(x)
144 |
145 | d3.select(".x.axis").call(xAxis)
146 | d3.select(".x.grid").call(xAxisGrid)
147 | d3.select(".x.axis > text").text(labels[xKey])
148 |
149 | redrawY = ->
150 | yExtent = d3.extent(filteredData, accessor(yKey) )
151 | yExtent[0] *= 0.9
152 | yExtent[1] *= 1.1
153 |
154 | y.domain(yExtent)
155 |
156 | d3.select(".y.axis").call(yAxis)
157 | d3.select(".y.grid").call(yAxisGrid)
158 | d3.select(".y.axis > text").text(labels[yKey])
159 |
160 | # Draw points or boxplots
161 | redrawData = ->
162 | if displayOrdinal
163 | graph.selectAll(".point").remove()
164 | # TODO hack; should not have to remove but box re-plotting is buggy
165 | graph.selectAll(".box").remove()
166 |
167 | # Draw box plots
168 | nested = d3.nest()
169 | .key( (d) -> d.nominalSize )
170 | .sortKeys(d3.ascending)
171 | .rollup( (leaves) -> leaves.map( accessor(yKey) ) )
172 | .entries(filteredData)
173 | .map( (o) -> [o.key, o.values] )
174 |
175 | # Update boxplot domain from y axis
176 | chart.domain(y.domain())
177 |
178 | boxes = graph.selectAll(".box")
179 | .data(nested)
180 |
181 | boxes.enter().append("g")
182 | .attr("class", "box")
183 |
184 | boxes.attr("transform", (d) -> "translate(#{x(d[0])},0)")
185 | .call(chart)
186 |
187 | else
188 | graph.selectAll(".box").remove()
189 |
190 | # Draw points
191 | points = graph.selectAll(".point")
192 | .data(filteredData, (d) -> d._id)
193 |
194 | # TODO Somehow, pseudo treatments get in here, even though they don't
195 | # appear in the final result.
196 | points.enter().append("circle")
197 | .attr("class", (g) -> "point " + g.treatments?.join(" "))
198 | .attr("stroke", (g) -> groupColor(g.nominalSize) )
199 | .attr("fill", (g) -> if g.treated then groupColor(g.nominalSize) else "white" )
200 | .append("svg:title")
201 | .text((g) -> g._id)
202 |
203 | # Update display values
204 | accX = accessor(xKey)
205 | accY = accessor(yKey)
206 | points.attr("cx", (g) -> x(accX(g)) #+ x.rangeBand() / 2
207 | )
208 | .attr("cy", (g) -> y(accY(g)))
209 | .attr("r", radius )
210 |
211 | @setX = (key) ->
212 | xKey = key
213 | filterData()
214 | redrawX()
215 | redrawData()
216 |
217 | @setY = (key) ->
218 | yKey = key
219 | filterData()
220 | redrawY()
221 | redrawData()
222 |
223 | @setShowBoxes = (show) ->
224 | showBoxes = show
225 | filterData()
226 | redrawX() # May need to switch to ordinal mode
227 | redrawData()
228 |
229 | # Initial config
230 | xKey = "nominalSize"
231 | yKey = "groupEntropy"
232 |
233 | filterData()
234 | redrawX()
235 | redrawY()
236 | redrawData()
237 |
238 | Template.overviewGroupScatter.events
239 | "change input[name=xaxis]": (e, t) -> t.setX(e.target.value)
240 | "change input[name=yaxis]": (e, t) -> t.setY(e.target.value)
241 | "change input[name=boxplot]": (e, t) -> t.setShowBoxes(e.target.checked)
242 |
243 |
--------------------------------------------------------------------------------
/client/tutorial/tutorial.coffee:
--------------------------------------------------------------------------------
1 | Template.tut_chatting.myusername = -> Meteor.users.findOne()?.username || "someone"
2 |
3 | editEvent = ->
4 | unless Events.findOne(editor: $exists: true)
5 | event = Events.findOne()
6 | Meteor.call("editEvent", event._id) if event?
7 |
8 | openDocument = ->
9 | unless Session.get("document")?
10 | # open a doc if there is one
11 | someDoc = Documents.findOne()
12 | Session.set("document", someDoc._id) if someDoc?
13 |
14 | joinChatroom = ->
15 | unless Session.get("room")?
16 | # join a chat room there is one
17 | someRoom = ChatRooms.findOne()
18 | Session.set("room", someRoom._id) if someRoom?
19 |
20 | tutorialSteps = [
21 | template: "tut_welcome"
22 | ,
23 | template: "tut_whatis"
24 | ,
25 | template: "tut_project"
26 | ,
27 | template: "tut_yourtask"
28 | ,
29 | template: "tut_goal"
30 | ,
31 | spot: ".datastream"
32 | template: "tut_datastream"
33 | ,
34 | spot: ".datastream"
35 | template: "tut_filterdata"
36 | require:
37 | event: "data-hide"
38 | ,
39 | spot: ".navbar"
40 | template: "tut_navbar"
41 | ,
42 | spot: ".navbar, #mapper-events"
43 | template: "tut_events"
44 | onLoad: -> Mapper.switchTab("events")
45 | ,
46 | spot: "#mapper-events"
47 | template: "tut_event_description"
48 | onLoad: -> Mapper.switchTab("events")
49 | ,
50 | spot: ".event-create"
51 | template: "tut_create_event"
52 | onLoad: ->
53 | Mapper.switchTab("events")
54 | require:
55 | event: "event-create"
56 | ,
57 | spot: "#mapper-events"
58 | template: "tut_editevent"
59 | onLoad: -> Mapper.switchTab("events")
60 | # require:
61 | # event: "event-edit"
62 | ,
63 | spot: ".events-header tr > th:eq(0), .events-body tr > td:nth-child(1)"
64 | template: "tut_events_index"
65 | onLoad: -> Mapper.switchTab("events")
66 | ,
67 | spot: ".events-header tr > th:eq(1), .events-body tr > td:nth-child(2), .datastream"
68 | template: "tut_events_sources"
69 | onLoad: -> Mapper.switchTab("events")
70 | require:
71 | event: "data-link"
72 | ,
73 | spot: ".events-header tr > th:eq(2), .events-body tr > td:nth-child(3)"
74 | template: "tut_events_type"
75 | onLoad: ->
76 | Mapper.switchTab("events")
77 | editEvent()
78 | require:
79 | event: "event-update-type"
80 | ,
81 | spot: ".events-header tr > th:eq(3), .events-body tr > td:nth-child(4)"
82 | template: "tut_events_description"
83 | onLoad: -> Mapper.switchTab("events")
84 | require:
85 | event: "event-update-description"
86 | ,
87 | spot: ".events-header tr > th:eq(4), .events-body tr > td:nth-child(5)"
88 | template: "tut_events_region"
89 | onLoad: -> Mapper.switchTab("events")
90 | require:
91 | event: "event-update-region"
92 | ,
93 | spot: ".events-header tr > th:eq(5), .events-body tr > td:nth-child(6)"
94 | template: "tut_events_province"
95 | onLoad: -> Mapper.switchTab("events")
96 | # No required event here.
97 | ,
98 | spot: ".events-header tr > th:eq(6), .events-body tr > td:nth-child(7)"
99 | template: "tut_events_location"
100 | onLoad: -> Mapper.switchTab("events")
101 | ,
102 | spot: ".datastream, #mapper-events"
103 | template: "tut_addtweet"
104 | onLoad: -> Mapper.switchTab("events")
105 | ,
106 | spot: ".events-header"
107 | template: "tut_sortevent"
108 | onLoad: -> Mapper.switchTab("events")
109 | ,
110 | spot: ".navbar, #mapper-map"
111 | template: "tut_map"
112 | onLoad: -> Mapper.switchTab("map")
113 | ,
114 | spot: ".olControlPanZoomBar > *"
115 | template: "tut_mapcontrols"
116 | onLoad: -> Mapper.switchTab("map")
117 | ,
118 | spot: ".navbar, #mapper-events"
119 | template: "tut_maplocate"
120 | onLoad: -> Mapper.switchTab("events")
121 | require:
122 | event: "event-update-location"
123 | ,
124 | spot: "#mapper-map"
125 | template: "tut_editmap"
126 | onLoad: -> Mapper.switchTab("map")
127 | ,
128 | spot: "#mapper-events"
129 | template: "tut_maplocation"
130 | onLoad: -> Mapper.switchTab("events")
131 | require:
132 | event: "event-save"
133 | ,
134 | spot: ".event-voting-container"
135 | template: "tut_verify"
136 | onLoad: -> Mapper.switchTab("events")
137 | require:
138 | event: "event-vote"
139 | ,
140 | spot: ".navbar, #mapper-docs"
141 | template: "tut_documents"
142 | onLoad: -> Mapper.switchTab("docs")
143 | require:
144 | event: "document-create"
145 | ,
146 | spot: "#mapper-docs"
147 | template: "tut_editdocs"
148 | onLoad: ->
149 | Mapper.switchTab("docs")
150 | openDocument()
151 | ,
152 | spot: ".user-list"
153 | template: "tut_userlist"
154 | ,
155 | spot: ".chat-overview"
156 | template: "tut_chatrooms"
157 | require:
158 | event: "chat-create"
159 | ,
160 | spot: ".notification"
161 | template: "tut_notifications"
162 | ,
163 | spot: ".chat-overview"
164 | template: "tut_joinchat"
165 | # require: event: "chat-join"
166 | ,
167 | spot: ".chat-overview, .chat-messaging"
168 | template: "tut_leavechat"
169 | onLoad: joinChatroom
170 | ,
171 | spot: ".chat-messaging"
172 | template: "tut_chatting"
173 | require:
174 | event: "chat-message"
175 | ]
176 |
177 | Template.tut_end.events =
178 | "change input[type=checkbox]": (e, tmpl) ->
179 | Session.set("consentChecked", e.target.checked)
180 | Mapper.events.emit("check-consent") if e.target.checked
181 |
182 | checked = -> Session.get("consentChecked")
183 |
184 | Template.tut_end.helpers
185 | checked: checked
186 | # Override stepComplete function on this template, to update message.
187 | stepCompleted: checked
188 |
189 | getRecruitingSteps = ->
190 | # replace templates with _recruiting if they exist
191 | # Don't modify original objects to avoid errors
192 | copiedSteps = $.map(tutorialSteps, (obj) -> $.extend({}, obj))
193 |
194 | for i, step of copiedSteps
195 | recruitingTemplate = step.template + "_recruiting"
196 | step.template = recruitingTemplate if Template[recruitingTemplate]
197 |
198 | return copiedSteps.concat [
199 | template: "tut_payment_recruiting"
200 | ]
201 |
202 | getTutorialSteps = ->
203 | return tutorialSteps.concat [
204 | template: "tut_groundrules"
205 | ,
206 | spot: ".payment"
207 | template: "tut_payment"
208 | ,
209 | template: "tut_end"
210 | require:
211 | event: "check-consent"
212 | ]
213 |
214 | Template.mapperTutorial.helpers
215 | tutorialEnabled: ->
216 | treatment = TurkServer.treatment()
217 | return treatment?.tutorialEnabled and not Meteor.user()?.admin
218 |
219 | options: ->
220 | treatment = TurkServer.treatment()
221 |
222 | steps = switch treatment?.tutorial
223 | when "recruiting" then getRecruitingSteps()
224 | when "pre_task" then getTutorialSteps()
225 | else
226 | Meteor._debug("Unknown tutorial type: " + treatment.tutorial)
227 | []
228 |
229 | return {
230 | id: "mapperTutorial"
231 | steps: steps
232 | emitter: Mapper.events
233 | onFinish: -> Meteor.call "finishTutorial"
234 | }
235 |
236 | # Handy function to allow the entire tutorial for testing
237 | Mapper.bypassTutorial = (skipToEnd) ->
238 | for i, step of tutorialSteps
239 | Mapper.events.emit(step.require.event) if step?.require?.event
240 |
241 | if skipToEnd
242 | tm = Blaze.getData( $(".modal-dialog.positioned")[0])
243 | # Get the tutorial manager and skip it to the end
244 | tm.step = tm.steps.length - 1
245 | tm.stepDep.changed()
246 | return
247 |
--------------------------------------------------------------------------------
/client/views/events.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> eventsHeader}}
4 |
5 | {{! We add and remove a highlighted class to this div to avoid messing with scroll}}
6 |
7 |
8 | {{!-- This extra container solves the problem of the absolute div clipping shit --}}
9 |
10 | {{#if loaded}}
11 | {{> eventsBody}}
12 | {{else}}
13 | {{> spinner}}
14 | {{/if}}
15 |
16 |
17 |
18 | {{> createFooter}}
19 |
20 |
21 |
22 |
45 |
46 |
47 |
48 |
49 | {{! Replicate column setup above }}
50 |
51 |
52 |
53 | {{#each eventFields}} {{/each}}
54 |
55 |
56 |
57 |
58 |
59 | {{#if noEvents}}
60 | {{> emptyRow}}
61 | {{/if}}
62 |
63 | {{#each records}}
64 | {{> eventRow}}
65 | {{/each}}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | No events yet. Create some!
74 |
75 |
76 |
77 |
78 |
79 |
80 | Create New Event
81 |
82 |
83 |
84 |
85 |
86 | {{> eventNum}}
87 |
88 | {{> eventSources}}
89 |
90 | {{#each eventFields}}
91 | {{#with eventCell}}
92 | {{#with buildData ../.. ..}}
93 | {{> .. }}
94 | {{/with}}
95 | {{/with}}
96 | {{/each}}
97 |
98 | {{> eventLocation}}
99 |
100 |
101 | {{> editCell }}
102 | {{> eventVoting }}
103 |
104 |
105 |
106 |
107 |
108 |
109 | {{num}}
110 |
111 |
112 |
113 |
114 |
115 |
116 | {{#each sources}}
117 | {{> tweetIcon findTweet}}
118 | {{/each}}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | {{#if editable}}
128 | {{> eventCellTextEditable}}
129 | {{else}}
130 | {{value}}
131 | {{/if}}
132 |
133 |
134 |
135 |
136 |
141 | {{#with value}}
142 | {{this}}
143 | {{else}}
144 | (empty)
145 | {{/with}}
146 |
147 |
148 |
149 |
150 |
151 | {{#if editable}}
152 | {{> eventCellSelectEditable}}
153 | {{else}}
154 | {{textValue}}
155 | {{/if}}
156 |
157 |
158 |
159 |
160 |
165 | {{#with textValue}}
166 | {{this}}
167 | {{else}}
168 | (empty)
169 | {{/with}}
170 |
171 |
172 |
173 |
174 |
175 |
176 | {{#if location}}
177 | {{#if editable}} {{! defined as a helper for location}}
178 | {{> eventLocationEditable}}
179 | {{else}}
180 |
181 | {{{formatLocation}}}
182 |
183 | {{/if}}
184 | {{else}}
185 |
186 | Locate
187 |
188 | {{/if}}
189 |
190 |
191 |
192 |
193 |
194 |
195 | {{{formatLocation}}}
196 |
197 |
198 |
199 |
200 |
201 | {{#if editor}}
202 | {{#with otherEditorUser}}
203 | {{> userPill}} is editing
204 | {{else}}
205 | {{> _editCellSelf}}
206 | {{/with}}
207 | {{else}}
208 | {{> _editCellOpen}}
209 | {{/if}}
210 |
211 |
212 |
213 |
214 | Save
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 | {{numVotes}}
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 | {{#if anyVotes}}
239 | Users who verified this event:
240 | {{#each votes}}
241 | {{> userPill lookupUser}}
242 | {{/each}}
243 | {{else}}
244 | No one has verified this event.
245 | {{/if}}
246 |
247 |
248 | {{#if iVoted}}
249 |
250 | Unverify
251 |
252 | {{else}}
253 |
254 | Verify
255 |
256 | {{/if}}
257 |
258 |
259 |
--------------------------------------------------------------------------------
/packages/analysis/client/overview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
35 | {{> yield}}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Overview
43 |
44 |
Click bits to the left to see graphs.
45 |
46 |
Run data analysis functions:
47 |
48 |
49 |
51 | Export exit survey data
52 |
53 |
54 |
55 |
57 | Populate data analysis collections
58 |
59 |
60 |
61 |
63 | Download individual group CSV
64 |
65 |
66 |
67 |
69 | Compute metadata for users
70 |
71 |
72 |
73 |
75 | Compute metadata for groups
76 |
77 |
78 |
79 |
81 | Compute average action weights
82 |
83 |
84 |
85 |
87 | Compute group performance and effort
88 |
89 |
90 |
91 |
93 | Compute group performance (quadrants)
94 |
95 |
96 |
97 |
99 | Compute group chat weight
100 |
101 |
102 |
103 | Download data frame:
104 |
106 | 1/4
107 |
108 |
110 | 1/2
111 |
112 |
114 | 3/4
115 |
116 |
118 | End
119 |
120 |
121 |
122 | Download data frame (time normalized):
123 |
125 | 1 hour
126 |
127 |
129 | 2 hours
130 |
131 |
133 | 3 hours
134 |
135 |
136 |
137 | Download data frame (effort normalized):
138 |
140 | 1 hour
141 |
142 |
144 | 2 hours
145 |
146 |
148 | 3 hours
149 |
150 |
151 |
152 |
154 | Download effort quadrants
155 |
156 |
157 |
158 |
160 | Compute synthetic group performance
161 |
162 |
163 |
164 |
166 | Download synthetic performance
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | viz
175 | watch
176 | replay
177 |
178 |
179 |
180 |
181 |
List of Experiments
182 | {{> reactiveTable collection=this settings=settings}}
183 |
184 |
185 |
186 |
187 |
188 |
Computed Seconds Per Action
189 | {{> reactiveTable collection=actionArray settings=settings }}
190 |
191 |
192 |
193 |
194 |
195 |
List of Participants
196 | {{> reactiveTable collection=this settings=settings }}
197 |
198 |
199 |
200 |
201 | Legend
202 |
203 | Size 1
204 | Size 2
205 | Size 4
206 | Size 8
207 | Size 16
208 | Size 32
209 |
210 |
211 |
--------------------------------------------------------------------------------
/client/index.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | Subscriptions
3 | ###
4 |
5 | fieldSub = Meteor.subscribe("eventFieldData", Mapper.processSources)
6 |
7 | # Unsets and sets a session variable for a subscription
8 | watchReady = (key) ->
9 | Session.set(key, false)
10 | # TODO temporary hack for people pushing back button / Meteor re-subscribe foolishness.
11 | # Show loading for at most 8 seconds. This is better than infinitely bugging out because most people seem to load the app just fine.
12 | Meteor.setTimeout((-> Session.set(key, true)), 8000)
13 | return (-> Session.set(key, true))
14 |
15 | ###
16 | Routing
17 | ###
18 |
19 | Router.configure
20 | notFoundTemplate: 'home'
21 | loadingTemplate: 'spinner'
22 |
23 | # TODO move the functionality of these before functions into TurkServer by providing built-in Iron Router controllers
24 | Router.map ->
25 | @route('home', {path: '/'})
26 |
27 | @route 'mapper',
28 | path: '/mapper'
29 | onBeforeAction: ->
30 | unless Meteor.user()
31 | @layout("defaultContainer")
32 | @render("awaitingLogin")
33 | else unless TurkServer.isAdmin() or TurkServer.inExperiment()
34 | @layout("defaultContainer")
35 | @render("loadError")
36 | else
37 | @next()
38 |
39 | waitOn: ->
40 | subHandles = [ fieldSub ]
41 |
42 | group = TurkServer.group()
43 | # Don't keep a room when going from tutorial to actual task
44 | # TODO this can be removed when the chat subscription is fixed
45 | unless group
46 | Session.set("room", undefined)
47 | return subHandles # Otherwise admin will derpily subscribe to the entire set of users
48 |
49 | # No need to clean up subscriptions because this is a Deps.autorun
50 | # We need to pass the group handle down to make Meteor think the subscription is different
51 | subHandles.push Meteor.subscribe("userStatus", group, watchReady("userSubReady"))
52 | # Chat messages are subscribed to by room
53 | subHandles.push Meteor.subscribe("chatrooms", group, watchReady("chatSubReady"))
54 | subHandles.push Meteor.subscribe("datastream", group, watchReady("dataSubReady"))
55 | subHandles.push Meteor.subscribe("docs", group, watchReady("docSubReady"))
56 | subHandles.push Meteor.subscribe("events", group, watchReady("eventSubReady"))
57 | # User specific, but shouldn't leak across instances
58 | subHandles.push Meteor.subscribe("notifications", group)
59 |
60 | return subHandles
61 |
62 | @route 'exitsurvey/:template?',
63 | layoutTemplate: 'defaultContainer'
64 | onBeforeAction: ->
65 | unless TurkServer.isAdmin() or TurkServer.inExitSurvey()
66 | @layout("defaultContainer")
67 | @render("loadError")
68 | else
69 | @next()
70 | action: ->
71 | # Override the route, for debugging use.
72 | if @params.template?
73 | @render(@params.template)
74 | else
75 | @render("exitsurvey")
76 |
77 | Meteor.startup ->
78 | Session.setDefault("taskView", 'events')
79 |
80 | # Defer setting up these autorun functions:
81 | # https://github.com/EventedMind/iron-router/issues/639
82 | Meteor.defer ->
83 | Deps.autorun ->
84 | Router.go("/mapper") if TurkServer.inExperiment()
85 |
86 | Deps.autorun ->
87 | Router.go("/exitsurvey") if TurkServer.inExitSurvey()
88 |
89 | ###
90 | Window sizing warning
91 | ###
92 | sizeWarningDialog = null
93 |
94 | checkSize = ->
95 | bigEnough = $(window).width() > 1200 and $(window).height() > 500
96 |
97 | if bigEnough and sizeWarningDialog?
98 | sizeWarningDialog.modal("hide")
99 | sizeWarningDialog = null
100 | return
101 |
102 | if !bigEnough and sizeWarningDialog is null
103 | sizeWarningDialog = bootbox.dialog
104 | closeButton: false
105 | message: "Your screen is not big enough for this task. Please maximize your window if possible, or use a computer with a higher-resolution screen. "
106 | return
107 |
108 | Meteor.startup ->
109 | checkSize()
110 | $(window).resize checkSize
111 | # Ask for username once user logs in
112 | TurkServer.ensureUsername()
113 |
114 | ###
115 | Idle Monitoring
116 | ###
117 | Deps.autorun ->
118 | return unless (treatment = TurkServer.treatment())?
119 |
120 | # Change monitoring setting whenever treatment changes
121 | # The TurkServer code will automatically handle starting and stopping during an experiment
122 | if treatment?.tutorialEnabled
123 | # Mostly for testing purposes during tutorial
124 | TurkServer.enableIdleMonitor(30000, true)
125 | else
126 | # 8 minute idle timer, ignore window blur
127 | TurkServer.enableIdleMonitor(8 * 60 * 1000, false)
128 |
129 | return
130 |
131 | ###
132 | Templates and helpers
133 | ###
134 |
135 | # TODO update this to use a more generalized API
136 | Template.home.helpers
137 | landingTemplate: ->
138 | treatments = TurkServer.batch()?.treatments
139 | if _.indexOf(treatments, "recruiting") >= 0
140 | Template.recruitingLanding
141 | else if _.indexOf(treatments, "parallel_worlds") >= 0
142 | Template.taskLanding
143 | else
144 | Template.loadingLanding
145 |
146 | ###
147 | Global level events in the mapper application - activating popovers on
148 | mouseover
149 |
150 | TODO generalize events below to remove boilerplate
151 | ###
152 | popoverDelay = 200
153 |
154 | Template.mapper.events
155 | # Attach and destroy a popover when mousing over a container. 'mouseenter'
156 | # only fires once when entering an element, so we use that to ensure that we
157 | # get the right target. However, exclude containers being dragged.
158 | "mouseenter .tweet-icon-container:not(.ui-draggable-dragging)": (e) ->
159 | container = $(e.target)
160 | tweet = Blaze.getData(e.target)
161 | delayShow = true
162 |
163 | Meteor.setTimeout ->
164 | # Skip creating popover if moused out already
165 | return unless delayShow
166 |
167 | container.popover({
168 | html: true
169 | placement: "auto right" # Otherwise it goes off the top of the screen
170 | trigger: "manual"
171 | container: e.target # Hovering over the popover should hold it open
172 | # No need for reactivity here since tweet does not change
173 | content: Blaze.toHTMLWithData Template.tweetPopup, Datastream.findOne(tweet._id)
174 | }).popover("show")
175 | , popoverDelay
176 |
177 | container.one "mouseleave", ->
178 | delayShow = false
179 | # Destroy any popover if it was created
180 | container.popover("destroy")
181 |
182 | "mouseenter .user-pill-container": (e) ->
183 | container = $(e.target)
184 |
185 | container.popover({
186 | html: true
187 | placement: "auto right"
188 | trigger: "manual"
189 | container: e.target
190 | content: ->
191 | # Grab updated data
192 | user = Blaze.getData(e.target)
193 | # Check if we should show chat invite
194 | if user.status?.online and user._id isnt Meteor.userId()
195 | return Blaze.toHTML Template.userInvitePopup
196 | else
197 | return null
198 | }).popover("show")
199 |
200 | container.one("mouseleave", -> container.popover("destroy") )
201 |
202 | Template.mapper.helpers
203 | adminControls: Template.adminControls
204 |
205 | Template.mapper.rendered = ->
206 | # Set initial active tab when state changes
207 | @comp = Deps.autorun ->
208 | tab = Session.get('taskView')
209 | return unless tab?
210 | $('.stack .pages').removeClass('active')
211 | $('#mapper-'+tab).addClass('active')
212 |
213 | Template.mapper.destroyed = -> @comp?.stop()
214 |
215 | Template.guidance.helpers
216 | message: -> Session.get("guidanceMessage")
217 |
218 | switchTab = (page) ->
219 | return if Deps.nonreactive(-> Session.equals("taskView", page))
220 | Session.set("taskView", page)
221 |
222 | Template.pageNav.events =
223 | "click a": (e) -> e.preventDefault()
224 | # This function sets the styling on the navbar as well
225 | "click a[data-toggle='tab']": (e) ->
226 | if (target = $(e.target).data("target"))?
227 | switchTab(target)
228 | else
229 | e.stopPropagation() # Avoid effect of click if no tab change
230 |
231 | Template.pageNav.helpers
232 | payment: ->
233 | return null unless (treatment = TurkServer.treatment())?
234 | return switch
235 | when treatment.payment? then Template.tutorialPayment
236 | when treatment.wage? then Template.scaledPayment
237 | else null
238 | treatment: TurkServer.treatment
239 |
240 | Template.tutorialPayment.helpers
241 | amount: -> "$" + @payment.toFixed(2)
242 |
243 | Template.scaledPayment.helpers
244 | amount: ->
245 | hours = TurkServer.Timers.activeTime() / (3600000) # millis per hour
246 | lowest = (@wage * hours).toFixed(2)
247 | highest = ((@wage + @bonus) * hours).toFixed(2)
248 | return "$#{lowest} - $#{highest}"
249 | lowest: -> @wage.toFixed(2)
250 | highest: -> (@wage + @bonus).toFixed(2)
251 |
252 | Template.help.helpers
253 | teamInfo: ->
254 | switch @groupSize
255 | when 1 then "You are working by yourself . There are no other team members for this task."
256 | when undefined then "You are working in a team ."
257 | else "You are working in a team of #{@groupSize} members ."
258 |
259 | instructionsInfo: ->
260 | instr = "Refer to the Instructions document for an overview of the instructions."
261 | if @groupSize > 1 or not @groupSize?
262 | instr += " You should feel free to ask your teammates about anything that you don't understand."
263 | return instr
264 |
265 | Template.help.rendered = ->
266 | this.$(".dropdown > a").click()
267 |
--------------------------------------------------------------------------------
/client/views/chat.coffee:
--------------------------------------------------------------------------------
1 | # This should only change if the room changes, else chat box will be re-rendering a lot
2 | Handlebars.registerHelper "currentRoom", ->
3 | return unless Meteor.userId()?
4 | # Return room only if it exists in the collection (not deleted)
5 | return ChatRooms.findOne(Session.get("room"), {fields: _id: 1})?._id
6 |
7 | Template.chat.rendered = ->
8 | # Send room changes to server
9 | # TODO this incurs a high traffic/rendering cost when switching between rooms
10 | # TODO only subscribe to the room if found in the ChatRooms collection
11 | this.autorun ->
12 | roomId = Session.get("room")
13 |
14 | # in replay mode, messages will be automatically pushed over by the server
15 | if Router.current().route.getName() is "replay"
16 | console.log "Room change in replay; no action taken."
17 | return
18 |
19 | # request the contents of the chat room otherwise
20 | Session.set("chatRoomReady", false)
21 | Meteor.subscribe("chatstate", roomId, -> Session.set("chatRoomReady", true))
22 |
23 | Template.chat.events
24 | "click .action-room-create": (e) ->
25 | e.preventDefault()
26 |
27 | bootbox.prompt "Name the room", (roomName) ->
28 | return unless !!roomName
29 | Meteor.call "createChat", roomName, (err, id) ->
30 | return unless id
31 | Session.set "room", id
32 |
33 | Template.currentChatroom.events
34 | "click .action-room-leave": ->
35 | # TODO convert this to a method call ... !
36 | # bootbox.confirm "Leave this room?", (value) ->
37 | Session.set("room", undefined) # if value
38 |
39 | Template.currentChatroom.helpers
40 | nameDoc: -> ChatRooms.findOne(""+@, {fields: name: 1})
41 |
42 | Template.rooms.helpers
43 | loaded: -> Session.equals("chatSubReady", true)
44 |
45 | availableRooms: ->
46 | selector = if TurkServer.isAdmin() and Session.equals("adminShowDeleted", true) then {}
47 | else { deleted: {$exists: false} }
48 | ChatRooms.find(selector, {sort: {name: 1}}) # For a consistent ordering
49 |
50 | Template.roomItem.helpers
51 | active: -> if Session.equals("room", @_id) then "active" else ""
52 | deleted: -> if @deleted then "deleted" else ""
53 |
54 | empty: -> @users is 0
55 |
56 | Template.roomItem.events =
57 | "click .action-room-enter": (e) ->
58 | e.preventDefault()
59 |
60 | unless Meteor.userId()
61 | bootbox.alert "You must be logged in to join chat rooms."
62 | return
63 |
64 | Session.set "room", @_id
65 | Mapper.events.emit("chat-join")
66 |
67 | "click .action-room-delete": (e) ->
68 | e.preventDefault()
69 | roomId = @_id
70 | bootbox.confirm "This will delete the chat room and its messages. Are you sure?", (res) ->
71 | return unless res # Only if "yes" clicked
72 | Meteor.call("deleteChat", roomId) if roomId
73 |
74 | # don't select chatroom (above function) - http://stackoverflow.com/questions/10407783/stop-event-propagation-in-meteor
75 | e.stopImmediatePropagation()
76 |
77 | Template.roomUsers.helpers
78 | users: -> ChatUsers.find {}
79 | findUser: -> Meteor.users.findOne @userId
80 |
81 | Template.roomHeader.rendered = ->
82 | tmplInst = this
83 |
84 | this.autorun ->
85 | # Trigger this whenever title changes - note only name is reactively depended on
86 | Blaze.getData()
87 | # Destroy old editable if it exists
88 | tmplInst.$(".editable").editable("destroy").editable
89 | display: ->
90 | success: (response, newValue) ->
91 | roomId = Session.get("room")
92 | return unless roomId
93 | Meteor.call "renameChat", roomId, newValue
94 |
95 | showEvent = (eventId) ->
96 | Mapper.switchTab 'events' # Make sure we are on the event page
97 | # Set up a scroll event, then trigger a re-render
98 | Mapper.selectEvent(eventId)
99 | Mapper.scrollToEvent(eventId)
100 |
101 | Template.messageBox.events =
102 | "click .tweet-icon.clickme": (e) ->
103 | tweetId = $(e.target).data("tweetid") + "" # Ensure string, not integer
104 | data = Datastream.findOne(tweetId)
105 | return unless data
106 |
107 | # Error message if tweet is hidden, or went on a deleted event
108 | if data.hidden or Events.findOne(data.events?[0])?.deleted
109 | bootbox.alert("That data has been deleted.")
110 | return
111 |
112 | if data.events? and data.events.length > 0
113 | showEvent data.events[0] # Scroll to event
114 | else
115 | Mapper.selectData(tweetId)
116 | Mapper.scrollToData(tweetId)
117 |
118 | "click .event-icon.clickme": (e) ->
119 | eventId = $(e.target).data("eventid") + ""
120 | event = Events.findOne(eventId)
121 | return unless event
122 |
123 | if event.deleted
124 | bootbox.alert("That event has been deleted.")
125 | return
126 |
127 | showEvent(eventId)
128 |
129 | Template.messageBox.helpers
130 | loaded: -> Session.equals("chatRoomReady", true)
131 | messages: ->
132 | # Multiple chatrooms may be loaded, to save on traffic or for the replay.
133 | # Since we're sorting anyway, filter by room.
134 | ChatMessages.find { room: Session.get("room") },
135 | sort: {timestamp: 1}
136 |
137 | # These usernames are nonreactive because find does not use any reactive variables
138 | Template.messageItem.helpers
139 | username: -> Meteor.users.findOne(@userId)?.username || @userId
140 |
141 | # If updating the user, also update server notification generations.
142 | userRegex = new RegExp('(^|\\b|\\s)(@[\\w.]+)($|\\b|\\s)','g')
143 | tweetRegex = new RegExp('(^|\\b|\\s)(~[\\d]+)($|\\b|\\s)','g')
144 | eventRegex = new RegExp('(^|\\b|\\s)(#[\\d]+)($|\\b|\\s)','g')
145 |
146 | ###
147 | Blaze.toHTML registers reactive dependencies, so chat messages can get
148 | re-rendered with state. However, this can cause excessive CPU usage.
149 |
150 | As a result, we use Blaze.toHTML with static data, and use very specific
151 | reactive dependencies below. It has to be reactive, or if the chat loads
152 | before the events/tweets then messages will be empty.
153 |
154 | Moving the findOne functions inside the Blaze.With won't make any difference
155 | below as the entire chat message has to be re-rendered anyway.
156 | ###
157 |
158 | # TODO: remove ugly spaces added below
159 | userFunc = (_, p1, p2) ->
160 | username = p2.substring(1)
161 | # userPill uses _id, username, and status
162 | user = Meteor.users.findOne(username: username, {fields: {username: 1, status: 1}})
163 | return " " + if user then Blaze.toHTMLWithData(Template.userPill, user) else p2
164 |
165 | tweetFunc = (_, p1, p2) ->
166 | tweetNum = parseInt( p2.substring(1) )
167 | # tweetIconClickable only uses _id and num
168 | tweet = Datastream.findOne( {num: tweetNum}, {fields: num: 1} )
169 | return " " + if tweet then Blaze.toHTMLWithData(Template.tweetIconClickable, tweet) else p2
170 |
171 | eventFunc = (_, p1, p2) ->
172 | eventNum = parseInt( p2.substring(1) )
173 | # eventIconClickable only uses _id and num
174 | event = Events.findOne( {num: eventNum}, {fields: num: 1} )
175 | return " " + if event then Blaze.toHTMLWithData(Template.eventIconClickable, event) else p2
176 |
177 | # Because messages only render when inserted, we can use this to scroll the chat window
178 | Template.messageItem.rendered = ->
179 | # Scroll down whenever anything happens
180 | $messages = $(".messages-body")
181 | $messages.scrollTop $messages[0].scrollHeight
182 |
183 | # Replace any matched users, tweets, or events with links
184 | Template.messageItem.helpers
185 | renderText: ->
186 | text = Handlebars._escape(@text)
187 | # No SafeString needed here as long as renderText is unescaped
188 | text = text.replace userRegex, userFunc
189 | text = text.replace tweetRegex, tweetFunc
190 | text = text.replace eventRegex, eventFunc
191 |
192 | eventText: ->
193 | username = Meteor.users.findOne(@userId).username
194 | return username + " has " + (if @event is "enter" then "entered" else "left" ) + " the room."
195 |
196 | Template.chatInput.rendered = ->
197 | $(@find(".chat-help")).popover
198 | html: true
199 | placement: "top"
200 | trigger: "hover"
201 | content: Blaze.toHTML Template.chatPopover
202 |
203 | Template.chatInput.events =
204 | submit: (e, tmpl) ->
205 | e.preventDefault()
206 | $msg = $( tmpl.find(".chat-input") )
207 | return unless $msg.val()
208 |
209 | Meteor.call "sendChat", Session.get("room"), $msg.val() # Server only method
210 |
211 | $msg.val("")
212 | $msg.focus()
213 | Meteor.flush()
214 |
215 | # Auto scroll happens on messageBox render now..
216 | Mapper.events.emit("chat-message")
217 |
218 | # RegExp syntax below taken from
219 | # https://github.com/meteor/meteor/blob/devel/packages/minimongo/selector.js
220 | # We use $where because we need the regex to match on a number!
221 | # This worked before but was removed in 0.7.1:
222 | # https://github.com/meteor/meteor/pull/1874#issuecomment-37074734
223 | # However, since it's all on the client, this will result in the same performance.
224 | numericMatcher = (filter) ->
225 | re = new RegExp("^" + filter)
226 | return { $where: -> re.test(@num) }
227 |
228 | Template.chatInput.helpers
229 | settings: -> {
230 | position: "top"
231 | limit: 5
232 | rules: [
233 | {
234 | token: '@'
235 | collection: Meteor.users
236 | field: "username"
237 | template: Template.userPill
238 | },
239 | {
240 | token: '~'
241 | collection: Datastream
242 | field: "num"
243 | template: Template.tweetNumbered
244 | # TODO this can select tweets attached to deleted events, but error
245 | # shows up when they are clicked
246 | filter: { hidden: $exists: false }
247 | selector: numericMatcher
248 | },
249 | {
250 | token: '#'
251 | collection: Events
252 | field: "num"
253 | template: Template.eventShort
254 | filter: { deleted: $exists: false }
255 | selector: numericMatcher
256 | }
257 | ]
258 | }
259 |
--------------------------------------------------------------------------------