diff --git a/.example-env b/.example-env index f3bace35..c2c9a2e9 100644 --- a/.example-env +++ b/.example-env @@ -7,7 +7,9 @@ export DB_NAME='metamap002' export REALTIME_SERVER='http://localhost:5001' export MAILER_DEFAULT_URL='localhost:3000' export DEVISE_MAILER_SENDER='team@metamaps.cc' + export DEVISE_SECRET_KEY='f71c467e526f23d614b3b08866cad4788c502bed869c282f06e73ee6c94675b62fe1f6d52fa7ba8196b33031f0d2f3b67e27ea07693c52ecebccb01700cad614' +export SECRET_KEY_BASE='267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c8005f85ee99d6d01b1ab6394cdee9ca7f8c9213a0cf91d3d8d3350f096123e2caccbcc0924f' # # you can safely leave these blank, unless you're deploying an instance, in # # which case you'll need to set them up diff --git a/.gitignore b/.gitignore index 52428f17..4d02a77f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,10 @@ # git config --global core.excludesfile ~/.gitignore_global #assety stuff -realtime/node_modules public/assets public/metamaps_mobile vendor/ +node_modules #secrets and config .env diff --git a/.travis.yml b/.travis.yml index 58bf9d25..30a831b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,21 @@ sudo: false language: ruby +cache: + bundler: true + directories: + - app/assets/javascripts/node_modules rvm: - 2.1.3 before_script: + - echo "Rspec setup" - export RAILS_ENV=test - cp .example-env .env - bundle exec rake db:create - bundle exec rake db:schema:load + - echo "node setup" + - . $HOME/.nvm/nvm.sh + - nvm install stable + - nvm use stable + - (cd app/assets/javascripts && npm install) +script: + - bundle exec rspec && (cd app/assets/javascripts && npm test) diff --git a/Gemfile b/Gemfile index 815c5eee..07159461 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ gem 'delayed_job', '~> 4.0.2' gem 'delayed_job_active_record', '~> 4.0.1' gem 'devise' gem 'doorkeeper' -gem 'dotenv' +gem 'dotenv-rails' gem 'exception_notification' gem 'formtastic' gem 'formula' diff --git a/Gemfile.lock b/Gemfile.lock index 59a2e295..c1cf8409 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,7 +87,10 @@ GEM docile (1.1.5) doorkeeper (3.1.0) railties (>= 3.2) - dotenv (2.1.0) + dotenv (2.1.1) + dotenv-rails (2.1.1) + dotenv (= 2.1.1) + railties (>= 4.0, < 5.1) erubis (2.7.0) exception_notification (4.1.4) actionmailer (~> 4.0) @@ -273,7 +276,7 @@ DEPENDENCIES delayed_job_active_record (~> 4.0.1) devise doorkeeper - dotenv + dotenv-rails exception_notification factory_girl_rails formtastic diff --git a/app/assets/images/addcollab_sprite.png b/app/assets/images/addcollab_sprite.png new file mode 100644 index 00000000..d3f49872 Binary files /dev/null and b/app/assets/images/addcollab_sprite.png differ diff --git a/app/assets/images/exploremaps_sprite.png b/app/assets/images/exploremaps_sprite.png old mode 100755 new mode 100644 index 16535918..492daaa8 Binary files a/app/assets/images/exploremaps_sprite.png and b/app/assets/images/exploremaps_sprite.png differ diff --git a/app/assets/images/mm_logo.png b/app/assets/images/mm_logo.png new file mode 100644 index 00000000..af104b09 Binary files /dev/null and b/app/assets/images/mm_logo.png differ diff --git a/app/assets/images/removecollab_sprite.png b/app/assets/images/removecollab_sprite.png new file mode 100644 index 00000000..2036da5e Binary files /dev/null and b/app/assets/images/removecollab_sprite.png differ diff --git a/app/assets/javascripts/README-testing-js.md b/app/assets/javascripts/README-testing-js.md new file mode 100644 index 00000000..d83a29e1 --- /dev/null +++ b/app/assets/javascripts/README-testing-js.md @@ -0,0 +1,9 @@ +Change directories to where this file is, and then run + + npm install + +to set up your testing environment. Then use + + npm test + +to see the results of testing the current javascript files. diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3556d617..8dcb1df3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -24,7 +24,23 @@ //= require ./src/views/videoView //= require ./src/views/room //= require ./src/JIT +//= require ./src/check-canvas-support //= require ./src/Metamaps +//= require ./src/Metamaps.Create +//= require ./src/Metamaps.TopicCard +//= require ./src/Metamaps.SynapseCard +//= require ./src/Metamaps.Visualize +//= require ./src/Metamaps.Util +//= require ./src/Metamaps.Realtime +//= require ./src/Metamaps.Control +//= require ./src/Metamaps.Filter +//= require ./src/Metamaps.Listeners +//= require ./src/Metamaps.Organize +//= require ./src/Metamaps.Topic +//= require ./src/Metamaps.Synapse +//= require ./src/Metamaps.Map +//= require ./src/Metamaps.Account +//= require ./src/Metamaps.Mapper //= require ./src/Metamaps.Admin //= require ./src/Metamaps.Import //= require ./src/Metamaps.JIT diff --git a/app/assets/javascripts/package.json b/app/assets/javascripts/package.json new file mode 100644 index 00000000..a322d7ad --- /dev/null +++ b/app/assets/javascripts/package.json @@ -0,0 +1,22 @@ +{ + "name": "metamaps-frontend", + "version": "1.0.0", + "description": "Metamaps frontend - currently just tests", + "scripts": { + "test": "mocha test || echo 'Run `npm install` to setup testing'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/metamaps/metamaps.git" + }, + "author": "", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/metamaps/metamaps/issues" + }, + "homepage": "https://github.com/metamaps/metamaps#readme", + "devDependencies": { + "chai": "^3.5.0", + "mocha": "^2.4.5" + } +} diff --git a/app/assets/javascripts/src/Metamaps.Account.js b/app/assets/javascripts/src/Metamaps.Account.js new file mode 100644 index 00000000..a2286ad8 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Account.js @@ -0,0 +1,122 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Account.js.erb + * + * Dependencies: + * - Metamaps.Erb + */ + +Metamaps.Account = { + listenersInitialized: false, + init: function () { + var self = Metamaps.Account + }, + initListeners: function () { + var self = Metamaps.Account + + $('#user_image').change(self.showImagePreview) + self.listenersInitialized = true + }, + toggleChangePicture: function () { + var self = Metamaps.Account + + $('.userImageMenu').toggle() + if (!self.listenersInitialized) self.initListeners() + }, + openChangePicture: function () { + var self = Metamaps.Account + + $('.userImageMenu').show() + if (!self.listenersInitialized) self.initListeners() + }, + closeChangePicture: function () { + var self = Metamaps.Account + + $('.userImageMenu').hide() + }, + showLoading: function () { + var self = Metamaps.Account + + var loader = new CanvasLoader('accountPageLoading') + loader.setColor('#4FC059'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + $('#accountPageLoading').show() + }, + showImagePreview: function () { + var self = Metamaps.Account + + var file = $('#user_image')[0].files[0] + + var reader = new FileReader() + + reader.onload = function (e) { + var $canvas = $('').attr({ + width: 84, + height: 84 + }) + var context = $canvas[0].getContext('2d') + var imageObj = new Image() + + imageObj.onload = function () { + $('.userImageDiv canvas').remove() + $('.userImageDiv img').hide() + + var imgWidth = imageObj.width + var imgHeight = imageObj.height + + var dimensionToMatch = imgWidth > imgHeight ? imgHeight : imgWidth + // draw cropped image + var nonZero = Math.abs(imgHeight - imgWidth) / 2 + var sourceX = dimensionToMatch === imgWidth ? 0 : nonZero + var sourceY = dimensionToMatch === imgHeight ? 0 : nonZero + var sourceWidth = dimensionToMatch + var sourceHeight = dimensionToMatch + var destX = 0 + var destY = 0 + var destWidth = 84 + var destHeight = 84 + + context.drawImage(imageObj, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight) + $('.userImageDiv').prepend($canvas) + } + imageObj.src = reader.result + } + + if (file) { + reader.readAsDataURL(file) + $('.userImageMenu').hide() + $('#remove_image').val('0') + } + }, + removePicture: function () { + var self = Metamaps.Account + + $('.userImageDiv canvas').remove() + $('.userImageDiv img').attr('src', Metamaps.Erb['user.png']).show() + $('.userImageMenu').hide() + + var input = $('#user_image') + input.replaceWith(input.val('').clone(true)) + $('#remove_image').val('1') + }, + changeName: function () { + $('.accountName').hide() + $('.changeName').show() + }, + showPass: function () { + $('.toHide').show() + $('.changePass').hide() + }, + hidePass: function () { + $('.toHide').hide() + $('.changePass').show() + + $('#current_password').val('') + $('#user_password').val('') + $('#user_password_confirmation').val('') + } +} diff --git a/app/assets/javascripts/src/Metamaps.Admin.js.erb b/app/assets/javascripts/src/Metamaps.Admin.js similarity index 100% rename from app/assets/javascripts/src/Metamaps.Admin.js.erb rename to app/assets/javascripts/src/Metamaps.Admin.js diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js new file mode 100644 index 00000000..63025c54 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -0,0 +1,723 @@ +/* global Metamaps, Backbone, _, $ */ + +/* + * Metamaps.Backbone.js.erb + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Collaborators + * - Metamaps.Creators + * - Metamaps.Filter + * - Metamaps.JIT + * - Metamaps.Loading + * - Metamaps.Map + * - Metamaps.Mapper + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Metacodes + * - Metamaps.Realtime + * - Metamaps.Synapse + * - Metamaps.SynapseCard + * - Metamaps.Synapses + * - Metamaps.Topic + * - Metamaps.TopicCard + * - Metamaps.Topics + * - Metamaps.Visualize + */ + +Metamaps.Backbone = {} + +Metamaps.Backbone.Map = Backbone.Model.extend({ + urlRoot: '/maps', + blacklist: ['created_at', 'updated_at', 'created_at_clean', 'updated_at_clean', 'user_name', 'contributor_count', 'topic_count', 'synapse_count', 'topics', 'synapses', 'mappings', 'mappers'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + save: function (key, val, options) { + var attrs + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key + options = val + } else { + (attrs = {})[key] = val + } + + var newOptions = options || {} + var s = newOptions.success + + newOptions.success = function (model, response, opt) { + if (s) s(model, response, opt) + model.trigger('saved') + } + return Backbone.Model.prototype.save.call(this, attrs, newOptions) + }, + initialize: function () { + this.on('changeByOther', this.updateView) + this.on('saved', this.savedEvent) + }, + savedEvent: function () { + Metamaps.Realtime.sendMapChange(this) + }, + authorizeToEdit: function (mapper) { + if (mapper && ( + this.get('permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')) || + this.get('user_id') === mapper.get('id'))) { + return true + } else { + return false + } + }, + authorizePermissionChange: function (mapper) { + if (mapper && this.get('user_id') === mapper.get('id')) { + return true + } else { + return false + } + }, + getUser: function () { + return Metamaps.Mapper.get(this.get('user_id')) + }, + fetchContained: function () { + var bb = Metamaps.Backbone + var that = this + var start = function (data) { + that.set('mappers', new bb.MapperCollection(data.mappers)) + that.set('topics', new bb.TopicCollection(data.topics)) + that.set('synapses', new bb.SynapseCollection(data.synapses)) + that.set('mappings', new bb.MappingCollection(data.mappings)) + } + + $.ajax({ + url: '/maps/' + this.id + '/contains.json', + success: start, + error: errorFunc, + async: false + }) + }, + getTopics: function () { + if (!this.get('topics')) { + this.fetchContained() + } + return this.get('topics') + }, + getSynapses: function () { + if (!this.get('synapses')) { + this.fetchContained() + } + return this.get('synapses') + }, + getMappings: function () { + if (!this.get('mappings')) { + this.fetchContained() + } + return this.get('mappings') + }, + getMappers: function () { + if (!this.get('mappers')) { + this.fetchContained() + } + return this.get('mappers') + }, + attrForCards: function () { + function capitalize (string) { + return string.charAt(0).toUpperCase() + string.slice(1) + } + + var n = this.get('name') + var d = this.get('desc') + + var maxNameLength = 32 + var maxDescLength = 118 + var truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' + var truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' + + var obj = { + id: this.id, + name: truncatedName, + fullName: n, + desc: truncatedDesc, + permission: this.get('permission') ? capitalize(this.get('permission')) : 'Commons', + editPermission: this.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEdit' : 'cannotEdit', + contributor_count_number: '' + this.get('contributor_count') + '', + contributor_count_string: this.get('contributor_count') === 1 ? ' contributor' : ' contributors', + topic_count_number: '' + this.get('topic_count') + '', + topic_count_string: this.get('topic_count') === 1 ? ' topic' : ' topics', + synapse_count_number: '' + this.get('synapse_count') + '', + synapse_count_string: this.get('synapse_count') === 1 ? ' synapse' : ' synapses', + screenshot: '' + } + return obj + }, + updateView: function () { + var map = Metamaps.Active.Map + var isActiveMap = this.id === map.id + if (isActiveMap) { + Metamaps.Map.InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) + this.updateMapWrapper() + } + }, + updateMapWrapper: function () { + var map = Metamaps.Active.Map + var isActiveMap = this.id === map.id + var authorized = map && map.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEditMap' : '' + var commonsMap = map && map.get('permission') === 'commons' ? 'commonsMap' : '' + if (isActiveMap) { + $('.wrapper').removeClass('canEditMap commonsMap').addClass(authorized + ' ' + commonsMap) + } + } +}) +Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ + model: Metamaps.Backbone.Map, + initialize: function (models, options) { + this.id = options.id + this.sortBy = options.sortBy + + if (options.mapperId) { + this.mapperId = options.mapperId + } + + // this.page represents the NEXT page to fetch + this.page = models.length > 0 ? (models.length < 20 ? 'loadedAll' : 2) : 1 + }, + url: function () { + if (!this.mapperId) { + return '/explore/' + this.id + '.json' + } else { + return '/explore/mapper/' + this.mapperId + '.json' + } + }, + comparator: function (a, b) { + a = a.get(this.sortBy) + b = b.get(this.sortBy) + var temp + if (this.sortBy === 'name') { + a = a ? a.toLowerCase() : '' + b = b ? b.toLowerCase() : '' + } else { + // this is for updated_at and created_at + temp = a + a = b + b = temp + a = (new Date(a)).getTime() + b = (new Date(b)).getTime() + } + return a > b ? 1 : a < b ? -1 : 0 + }, + getMaps: function (cb) { + var self = this + + Metamaps.Loading.show() + + if (this.page !== 'loadedAll') { + var numBefore = this.length + this.fetch({ + remove: false, + silent: true, + data: { page: this.page }, + success: function (collection, response, options) { + // you can pass additional options to the event you trigger here as well + if (collection.length - numBefore < 20) { + self.page = 'loadedAll' + } else { + self.page += 1 + } + self.trigger('successOnFetch', cb) + }, + error: function (collection, response, options) { + // you can pass additional options to the event you trigger here as well + self.trigger('errorOnFetch') + } + }) + } else { + self.trigger('successOnFetch', cb) + } + } +}) + +Metamaps.Backbone.Message = Backbone.Model.extend({ + urlRoot: '/messages', + blacklist: ['created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + prepareLiForFilter: function () { + /* var li = '' + * li += '
  • ' + * li += '' + * li += '

    ' + this.get('name') + '

  • ' + * return li + */ + } +}) +Metamaps.Backbone.MessageCollection = Backbone.Collection.extend({ + model: Metamaps.Backbone.Message, + url: '/messages' +}) + +Metamaps.Backbone.Mapper = Backbone.Model.extend({ + urlRoot: '/users', + blacklist: ['created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + prepareLiForFilter: function () { + var li = '' + li += '
  • ' + li += '' + li += '

    ' + this.get('name') + '

  • ' + return li + } +}) + +Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({ + model: Metamaps.Backbone.Mapper, + url: '/users' +}) + +Metamaps.Backbone.init = function () { + var self = Metamaps.Backbone + + self.Metacode = Backbone.Model.extend({ + initialize: function () { + var image = new Image() + image.crossOrigin = 'Anonymous' + image.src = this.get('icon') + this.set('image', image) + }, + prepareLiForFilter: function () { + var li = '' + li += '
  • ' + li += '' + li += '

    ' + this.get('name').toLowerCase() + '

  • ' + return li + } + + }) + self.MetacodeCollection = Backbone.Collection.extend({ + model: this.Metacode, + url: '/metacodes', + comparator: function (a, b) { + a = a.get('name').toLowerCase() + b = b.get('name').toLowerCase() + return a > b ? 1 : a < b ? -1 : 0 + } + }) + + self.Topic = Backbone.Model.extend({ + urlRoot: '/topics', + blacklist: ['node', 'created_at', 'updated_at', 'user_name', 'user_image', 'map_count', 'synapse_count'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + save: function (key, val, options) { + var attrs + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key + options = val + } else { + (attrs = {})[key] = val + } + + var newOptions = options || {} + var s = newOptions.success + + var permBefore = this.get('permission') + + newOptions.success = function (model, response, opt) { + if (s) s(model, response, opt) + model.trigger('saved') + + if (permBefore === 'private' && model.get('permission') !== 'private') { + model.trigger('noLongerPrivate') + } + else if (permBefore !== 'private' && model.get('permission') === 'private') { + model.trigger('nowPrivate') + } + } + return Backbone.Model.prototype.save.call(this, attrs, newOptions) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'desc': '', + 'link': '', + 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' + }) + } + + this.on('changeByOther', this.updateCardView) + this.on('change', this.updateNodeView) + this.on('saved', this.savedEvent) + this.on('nowPrivate', function () { + var removeTopicData = { + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]) + }) + this.on('noLongerPrivate', function () { + var newTopicData = { + mappingid: this.getMapping().id, + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + }) + + this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this) + }, + authorizeToEdit: function (mapper) { + if (mapper && + (this.get('calculated_permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')) || + this.get('user_id') === mapper.get('id'))) { + return true + } else { + return false + } + }, + authorizePermissionChange: function (mapper) { + if (mapper && this.get('user_id') === mapper.get('id')) return true + else return false + }, + getDate: function () {}, + getMetacode: function () { + return Metamaps.Metacodes.get(this.get('metacode_id')) + }, + getMapping: function () { + if (!Metamaps.Active.Map) return false + + return Metamaps.Mappings.findWhere({ + map_id: Metamaps.Active.Map.id, + mappable_type: 'Topic', + mappable_id: this.isNew() ? this.cid : this.id + }) + }, + createNode: function () { + var mapping + var node = { + adjacencies: [], + id: this.isNew() ? this.cid : this.id, + name: this.get('name') + } + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + node.data = { + $mapping: null, + $mappingID: mapping.id + } + } + + return node + }, + updateNode: function () { + var mapping + var node = this.get('node') + node.setData('topic', this) + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + node.setData('mapping', mapping) + } + + return node + }, + savedEvent: function () { + Metamaps.Realtime.sendTopicChange(this) + }, + updateViews: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + // update topic card, if this topic is the one open there + if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { + Metamaps.TopicCard.showCard(node) + } + + // update the node on the map + if (onPageWithTopicCard && node) { + node.name = this.get('name') + Metamaps.Visualize.mGraph.plot() + } + }, + updateCardView: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + // update topic card, if this topic is the one open there + if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { + Metamaps.TopicCard.showCard(node) + } + }, + updateNodeView: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + + // update the node on the map + if (onPageWithTopicCard && node) { + node.name = this.get('name') + Metamaps.Visualize.mGraph.plot() + } + } + }) + + self.TopicCollection = Backbone.Collection.extend({ + model: self.Topic, + url: '/topics' + }) + + self.Synapse = Backbone.Model.extend({ + urlRoot: '/synapses', + blacklist: ['edge', 'created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + save: function (key, val, options) { + var attrs + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key + options = val + } else { + (attrs = {})[key] = val + } + + var newOptions = options || {} + var s = newOptions.success + + var permBefore = this.get('permission') + + newOptions.success = function (model, response, opt) { + if (s) s(model, response, opt) + model.trigger('saved') + + if (permBefore === 'private' && model.get('permission') !== 'private') { + model.trigger('noLongerPrivate') + } + else if (permBefore !== 'private' && model.get('permission') === 'private') { + model.trigger('nowPrivate') + } + } + return Backbone.Model.prototype.save.call(this, attrs, newOptions) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', + 'category': 'from-to' + }) + } + + this.on('changeByOther', this.updateCardView) + this.on('change', this.updateEdgeView) + this.on('saved', this.savedEvent) + this.on('noLongerPrivate', function () { + var newSynapseData = { + mappingid: this.getMapping().id, + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + }) + this.on('nowPrivate', function () { + $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + mappableid: this.id + }]) + }) + + this.on('change:desc', Metamaps.Filter.checkSynapses, this) + }, + prepareLiForFilter: function () { + var li = '' + li += '
  • ' + li += '
  • ' + return li + }, + authorizeToEdit: function (mapper) { + if (mapper && (this.get('calculated_permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true + else return false + }, + authorizePermissionChange: function (mapper) { + if (mapper && this.get('user_id') === mapper.get('id')) return true + else return false + }, + getTopic1: function () { + return Metamaps.Topics.get(this.get('node1_id')) + }, + getTopic2: function () { + return Metamaps.Topics.get(this.get('node2_id')) + }, + getDirection: function () { + var t1 = this.getTopic1(), + t2 = this.getTopic2() + + return t1 && t2 ? [ + t1.get('node').id, + t2.get('node').id + ] : false + }, + getMapping: function () { + if (!Metamaps.Active.Map) return false + + return Metamaps.Mappings.findWhere({ + map_id: Metamaps.Active.Map.id, + mappable_type: 'Synapse', + mappable_id: this.isNew() ? this.cid : this.id + }) + }, + createEdge: function (providedMapping) { + var mapping, mappingID + var synapseID = this.isNew() ? this.cid : this.id + + var edge = { + nodeFrom: this.get('node1_id'), + nodeTo: this.get('node2_id'), + data: { + $synapses: [], + $synapseIDs: [synapseID], + } + } + + if (Metamaps.Active.Map) { + mapping = providedMapping || this.getMapping() + mappingID = mapping.isNew() ? mapping.cid : mapping.id + edge.data.$mappings = [] + edge.data.$mappingIDs = [mappingID] + } + + return edge + }, + updateEdge: function () { + var mapping + var edge = this.get('edge') + edge.getData('synapses').push(this) + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + edge.getData('mappings').push(mapping) + } + + return edge + }, + savedEvent: function () { + Metamaps.Realtime.sendSynapseChange(this) + }, + updateViews: function () { + this.updateCardView() + this.updateEdgeView() + }, + updateCardView: function () { + var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var edge = this.get('edge') + + // update synapse card, if this synapse is the one open there + if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { + Metamaps.SynapseCard.showCard(edge) + } + }, + updateEdgeView: function () { + var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var edge = this.get('edge') + + // update the edge on the map + if (onPageWithSynapseCard && edge) { + Metamaps.Visualize.mGraph.plot() + } + } + }) + + self.SynapseCollection = Backbone.Collection.extend({ + model: self.Synapse, + url: '/synapses' + }) + + self.Mapping = Backbone.Model.extend({ + urlRoot: '/mappings', + blacklist: ['created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'map_id': Metamaps.Active.Map ? Metamaps.Active.Map.id : null + }) + } + }, + getMap: function () { + return Metamaps.Map.get(this.get('map_id')) + }, + getTopic: function () { + if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')) + else return false + }, + getSynapse: function () { + if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')) + else return false + } + }) + + self.MappingCollection = Backbone.Collection.extend({ + model: self.Mapping, + url: '/mappings' + }) + + Metamaps.Metacodes = Metamaps.Metacodes ? new self.MetacodeCollection(Metamaps.Metacodes) : new self.MetacodeCollection() + + Metamaps.Topics = Metamaps.Topics ? new self.TopicCollection(Metamaps.Topics) : new self.TopicCollection() + + Metamaps.Synapses = Metamaps.Synapses ? new self.SynapseCollection(Metamaps.Synapses) : new self.SynapseCollection() + + Metamaps.Mappers = Metamaps.Mappers ? new self.MapperCollection(Metamaps.Mappers) : new self.MapperCollection() + + Metamaps.Collaborators = Metamaps.Collaborators ? new self.MapperCollection(Metamaps.Collaborators) : new self.MapperCollection() + + // this is for topic view + Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection() + + if (Metamaps.Active.Map) { + Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection() + + Metamaps.Active.Map = new self.Map(Metamaps.Active.Map) + } + + if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic) + + // attach collection event listeners + self.attachCollectionEvents = function () { + Metamaps.Topics.on('add remove', function (topic) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkMappers() + }) + + Metamaps.Synapses.on('add remove', function (synapse) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMappers() + }) + + if (Metamaps.Active.Map) { + Metamaps.Mappings.on('add remove', function (mapping) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkMappers() + }) + } + } + self.attachCollectionEvents() +}; // end Metamaps.Backbone.init diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js.erb b/app/assets/javascripts/src/Metamaps.Backbone.js.erb deleted file mode 100644 index 7b40c3cc..00000000 --- a/app/assets/javascripts/src/Metamaps.Backbone.js.erb +++ /dev/null @@ -1,256 +0,0 @@ -/* global Metamaps, Backbone, _, $ */ - -/* - * Metamaps.Backbone.js.erb - * - * Dependencies: - * - Metamaps.Active - * - Metamaps.Loading - * - Metamaps.Map - * - Metamaps.Mapper - * - Metamaps.Realtime - */ - -Metamaps.Backbone = {} - -Metamaps.Backbone.Map = Backbone.Model.extend({ - urlRoot: '/maps', - blacklist: ['created_at', 'updated_at', 'created_at_clean', 'updated_at_clean', 'user_name', 'contributor_count', 'topic_count', 'synapse_count', 'topics', 'synapses', 'mappings', 'mappers'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist) - }, - save: function (key, val, options) { - var attrs - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key - options = val - } else { - (attrs = {})[key] = val - } - - var newOptions = options || {} - var s = newOptions.success - - newOptions.success = function (model, response, opt) { - if (s) s(model, response, opt) - model.trigger('saved') - } - return Backbone.Model.prototype.save.call(this, attrs, newOptions) - }, - initialize: function () { - this.on('changeByOther', this.updateView) - this.on('saved', this.savedEvent) - }, - savedEvent: function () { - Metamaps.Realtime.sendMapChange(this) - }, - authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === 'commons' || this.get('user_id') === mapper.get('id'))) return true - else return false - }, - authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true - else return false - }, - getUser: function () { - return Metamaps.Mapper.get(this.get('user_id')) - }, - fetchContained: function () { - var bb = Metamaps.Backbone - var that = this - var start = function (data) { - that.set('mappers', new bb.MapperCollection(data.mappers)) - that.set('topics', new bb.TopicCollection(data.topics)) - that.set('synapses', new bb.SynapseCollection(data.synapses)) - that.set('mappings', new bb.MappingCollection(data.mappings)) - } - - $.ajax({ - url: '/maps/' + this.id + '/contains.json', - success: start, - async: false - }) - }, - getTopics: function () { - if (!this.get('topics')) { - this.fetchContained() - } - return this.get('topics') - }, - getSynapses: function () { - if (!this.get('synapses')) { - this.fetchContained() - } - return this.get('synapses') - }, - getMappings: function () { - if (!this.get('mappings')) { - this.fetchContained() - } - return this.get('mappings') - }, - getMappers: function () { - if (!this.get('mappers')) { - this.fetchContained() - } - return this.get('mappers') - }, - attrForCards: function () { - function capitalize (string) { - return string.charAt(0).toUpperCase() + string.slice(1) - } - - var n = this.get('name') - var d = this.get('desc') - - var maxNameLength = 32 - var maxDescLength = 118 - var truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' - var truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' - - var obj = { - id: this.id, - name: truncatedName, - fullName: n, - desc: truncatedDesc, - permission: this.get('permission') ? capitalize(this.get('permission')) : 'Commons', - editPermission: this.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEdit' : 'cannotEdit', - contributor_count_number: '' + this.get('contributor_count') + '', - contributor_count_string: this.get('contributor_count') === 1 ? ' contributor' : ' contributors', - topic_count_number: '' + this.get('topic_count') + '', - topic_count_string: this.get('topic_count') === 1 ? ' topic' : ' topics', - synapse_count_number: '' + this.get('synapse_count') + '', - synapse_count_string: this.get('synapse_count') === 1 ? ' synapse' : ' synapses', - screenshot: '' - } - return obj - }, - updateView: function () { - var map = Metamaps.Active.Map - var isActiveMap = this.id === map.id - if (isActiveMap) { - Metamaps.Map.InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) - this.updateMapWrapper() - } - }, - updateMapWrapper: function () { - var map = Metamaps.Active.Map - var isActiveMap = this.id === map.id - var authorized = map && map.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEditMap' : '' - var commonsMap = map && map.get('permission') === 'commons' ? 'commonsMap' : '' - if (isActiveMap) { - $('.wrapper').removeClass('canEditMap commonsMap').addClass(authorized + ' ' + commonsMap) - } - } -}) -Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Map, - initialize: function (models, options) { - this.id = options.id - this.sortBy = options.sortBy - - if (options.mapperId) { - this.mapperId = options.mapperId - } - - // this.page represents the NEXT page to fetch - this.page = models.length > 0 ? (models.length < 20 ? 'loadedAll' : 2) : 1 - }, - url: function () { - if (!this.mapperId) { - return '/explore/' + this.id + '.json' - } else { - return '/explore/mapper/' + this.mapperId + '.json' - } - }, - comparator: function (a, b) { - a = a.get(this.sortBy) - b = b.get(this.sortBy) - var temp - if (this.sortBy === 'name') { - a = a ? a.toLowerCase() : '' - b = b ? b.toLowerCase() : '' - } else { - // this is for updated_at and created_at - temp = a - a = b - b = temp - a = (new Date(a)).getTime() - b = (new Date(b)).getTime() - } - return a > b ? 1 : a < b ? -1 : 0 - }, - getMaps: function (cb) { - var self = this - - Metamaps.Loading.show() - - if (this.page !== 'loadedAll') { - var numBefore = this.length - this.fetch({ - remove: false, - silent: true, - data: { page: this.page }, - success: function (collection, response, options) { - // you can pass additional options to the event you trigger here as well - if (collection.length - numBefore < 20) { - self.page = 'loadedAll' - } else { - self.page += 1 - } - self.trigger('successOnFetch', cb) - }, - error: function (collection, response, options) { - // you can pass additional options to the event you trigger here as well - self.trigger('errorOnFetch') - } - }) - } else { - self.trigger('successOnFetch', cb) - } - } -}) - -Metamaps.Backbone.Message = Backbone.Model.extend({ - urlRoot: '/messages', - blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist) - }, - prepareLiForFilter: function () { - /* var li = '' - * li += '
  • ' - * li += '' - * li += '

    ' + this.get('name') + '

  • ' - * return li - */ - } -}) -Metamaps.Backbone.MessageCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Message, - url: '/messages' -}) - -Metamaps.Backbone.Mapper = Backbone.Model.extend({ - urlRoot: '/users', - blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist) - }, - prepareLiForFilter: function () { - var li = '' - li += '
  • ' - li += '' - li += '

    ' + this.get('name') + '

  • ' - return li - } -}) - -Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Mapper, - url: '/users' -}) diff --git a/app/assets/javascripts/src/Metamaps.Control.js b/app/assets/javascripts/src/Metamaps.Control.js new file mode 100644 index 00000000..4db7f82b --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Control.js @@ -0,0 +1,437 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Control.js.erb + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Control + * - Metamaps.Filter + * - Metamaps.GlobalUI + * - Metamaps.JIT + * - Metamaps.Mappings + * - Metamaps.Metacodes + * - Metamaps.Mouse + * - Metamaps.Selected + * - Metamaps.Settings + * - Metamaps.Synapses + * - Metamaps.Topics + * - Metamaps.Visualize + */ + +Metamaps.Control = { + init: function () {}, + selectNode: function (node, e) { + var filtered = node.getData('alpha') === 0 + + if (filtered || Metamaps.Selected.Nodes.indexOf(node) != -1) return + node.selected = true + node.setData('dim', 30, 'current') + Metamaps.Selected.Nodes.push(node) + }, + deselectAllNodes: function () { + var l = Metamaps.Selected.Nodes.length + for (var i = l - 1; i >= 0; i -= 1) { + var node = Metamaps.Selected.Nodes[i] + Metamaps.Control.deselectNode(node) + } + Metamaps.Visualize.mGraph.plot() + }, + deselectNode: function (node) { + delete node.selected + node.setData('dim', 25, 'current') + + // remove the node + Metamaps.Selected.Nodes.splice( + Metamaps.Selected.Nodes.indexOf(node), 1) + }, + deleteSelected: function () { + if (!Metamaps.Active.Map) return + + var n = Metamaps.Selected.Nodes.length + var e = Metamaps.Selected.Edges.length + var ntext = n == 1 ? '1 topic' : n + ' topics' + var etext = e == 1 ? '1 synapse' : e + ' synapses' + var text = 'You have ' + ntext + ' and ' + etext + ' selected. ' + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + var r = confirm(text + 'Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.') + if (r == true) { + Metamaps.Control.deleteSelectedEdges() + Metamaps.Control.deleteSelectedNodes() + } + }, + deleteSelectedNodes: function () { // refers to deleting topics permanently + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + var l = Metamaps.Selected.Nodes.length + for (var i = l - 1; i >= 0; i -= 1) { + var node = Metamaps.Selected.Nodes[i] + Metamaps.Control.deleteNode(node.id) + } + }, + deleteNode: function (nodeid) { // refers to deleting topics permanently + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var topic = node.getData('topic') + + var permToDelete = Metamaps.Active.Mapper.id === topic.get('user_id') || Metamaps.Active.Mapper.get('admin') + if (permToDelete) { + var mappableid = topic.id + var mapping = node.getData('mapping') + topic.destroy() + Metamaps.Mappings.remove(mapping) + $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ + mappableid: mappableid + }]) + Metamaps.Control.hideNode(nodeid) + } else { + Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted') + } + }, + removeSelectedNodes: function () { // refers to removing topics permanently from a map + if (!Metamaps.Active.Map) return + + var l = Metamaps.Selected.Nodes.length, + i, + node, + authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + for (i = l - 1; i >= 0; i -= 1) { + node = Metamaps.Selected.Nodes[i] + Metamaps.Control.removeNode(node.id) + } + }, + removeNode: function (nodeid) { // refers to removing topics permanently from a map + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + var topic = node.getData('topic') + var mappableid = topic.id + var mapping = node.getData('mapping') + mapping.destroy() + Metamaps.Topics.remove(topic) + $(document).trigger(Metamaps.JIT.events.removeTopic, [{ + mappableid: mappableid + }]) + Metamaps.Control.hideNode(nodeid) + }, + hideSelectedNodes: function () { + var l = Metamaps.Selected.Nodes.length, + i, + node + + for (i = l - 1; i >= 0; i -= 1) { + node = Metamaps.Selected.Nodes[i] + Metamaps.Control.hideNode(node.id) + } + }, + hideNode: function (nodeid) { + var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var graph = Metamaps.Visualize.mGraph + + Metamaps.Control.deselectNode(node) + + node.setData('alpha', 0, 'end') + node.eachAdjacency(function (adj) { + adj.setData('alpha', 0, 'end') + }) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['node-property:alpha', + 'edge-property:alpha' + ], + duration: 500 + }) + setTimeout(function () { + if (nodeid == Metamaps.Visualize.mGraph.root) { // && Metamaps.Visualize.type === "RGraph" + var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) + graph.root = newroot ? newroot.id : null + } + Metamaps.Visualize.mGraph.graph.removeNode(nodeid) + }, 500) + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkMappers() + }, + selectEdge: function (edge) { + var filtered = edge.getData('alpha') === 0; // don't select if the edge is filtered + + if (filtered || Metamaps.Selected.Edges.indexOf(edge) != -1) return + + var width = Metamaps.Mouse.edgeHoveringOver === edge ? 4 : 2 + edge.setDataset('current', { + showDesc: true, + lineWidth: width, + color: Metamaps.Settings.colors.synapses.selected + }) + Metamaps.Visualize.mGraph.plot() + + Metamaps.Selected.Edges.push(edge) + }, + deselectAllEdges: function () { + var l = Metamaps.Selected.Edges.length + for (var i = l - 1; i >= 0; i -= 1) { + var edge = Metamaps.Selected.Edges[i] + Metamaps.Control.deselectEdge(edge) + } + Metamaps.Visualize.mGraph.plot() + }, + deselectEdge: function (edge) { + edge.setData('showDesc', false, 'current') + + edge.setDataset('current', { + lineWidth: 2, + color: Metamaps.Settings.colors.synapses.normal + }) + + if (Metamaps.Mouse.edgeHoveringOver == edge) { + edge.setDataset('current', { + showDesc: true, + lineWidth: 4 + }) + } + + Metamaps.Visualize.mGraph.plot() + + // remove the edge + Metamaps.Selected.Edges.splice( + Metamaps.Selected.Edges.indexOf(edge), 1) + }, + deleteSelectedEdges: function () { // refers to deleting topics permanently + var edge, + l = Metamaps.Selected.Edges.length + + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + for (var i = l - 1; i >= 0; i -= 1) { + edge = Metamaps.Selected.Edges[i] + Metamaps.Control.deleteEdge(edge) + } + }, + deleteEdge: function (edge) { + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 + + var synapse = edge.getData('synapses')[index] + var mapping = edge.getData('mappings')[index] + + var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin') + if (permToDelete) { + if (edge.getData('synapses').length - 1 === 0) { + Metamaps.Control.hideEdge(edge) + } + var mappableid = synapse.id + synapse.destroy() + + // the server will destroy the mapping, we just need to remove it here + Metamaps.Mappings.remove(mapping) + edge.getData('mappings').splice(index, 1) + edge.getData('synapses').splice(index, 1) + if (edge.getData('displayIndex')) { + delete edge.data.$displayIndex + } + $(document).trigger(Metamaps.JIT.events.deleteSynapse, [{ + mappableid: mappableid + }]) + } else { + Metamaps.GlobalUI.notifyUser('Only synapses you created can be deleted') + } + }, + removeSelectedEdges: function () { + var l = Metamaps.Selected.Edges.length, + i, + edge + + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + for (i = l - 1; i >= 0; i -= 1) { + edge = Metamaps.Selected.Edges[i] + Metamaps.Control.removeEdge(edge) + } + Metamaps.Selected.Edges = [ ] + }, + removeEdge: function (edge) { + if (!Metamaps.Active.Map) return + + var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + + if (edge.getData('mappings').length - 1 === 0) { + Metamaps.Control.hideEdge(edge) + } + + var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 + + var synapse = edge.getData('synapses')[index] + var mapping = edge.getData('mappings')[index] + var mappableid = synapse.id + mapping.destroy() + + Metamaps.Synapses.remove(synapse) + + edge.getData('mappings').splice(index, 1) + edge.getData('synapses').splice(index, 1) + if (edge.getData('displayIndex')) { + delete edge.data.$displayIndex + } + $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + mappableid: mappableid + }]) + }, + hideSelectedEdges: function () { + var edge, + l = Metamaps.Selected.Edges.length, + i + for (i = l - 1; i >= 0; i -= 1) { + edge = Metamaps.Selected.Edges[i] + Metamaps.Control.hideEdge(edge) + } + Metamaps.Selected.Edges = [ ] + }, + hideEdge: function (edge) { + var from = edge.nodeFrom.id + var to = edge.nodeTo.id + edge.setData('alpha', 0, 'end') + Metamaps.Control.deselectEdge(edge) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['edge-property:alpha'], + duration: 500 + }) + setTimeout(function () { + Metamaps.Visualize.mGraph.graph.removeAdjacence(from, to) + }, 500) + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMappers() + }, + updateSelectedPermissions: function (permission) { + var edge, synapse, node, topic + + Metamaps.GlobalUI.notifyUser('Working...') + + // variables to keep track of how many nodes and synapses you had the ability to change the permission of + var nCount = 0, + sCount = 0 + + // change the permission of the selected synapses, if logged in user is the original creator + var l = Metamaps.Selected.Edges.length + for (var i = l - 1; i >= 0; i -= 1) { + edge = Metamaps.Selected.Edges[i] + synapse = edge.getData('synapses')[0] + + if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + synapse.save({ + permission: permission + }) + sCount++ + } + } + + // change the permission of the selected topics, if logged in user is the original creator + var l = Metamaps.Selected.Nodes.length + for (var i = l - 1; i >= 0; i -= 1) { + node = Metamaps.Selected.Nodes[i] + topic = node.getData('topic') + + if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + topic.save({ + permission: permission + }) + nCount++ + } + } + + var nString = nCount == 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and ') + var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') + + var message = nString + sString + ' you created updated to ' + permission + Metamaps.GlobalUI.notifyUser(message) + }, + updateSelectedMetacodes: function (metacode_id) { + var node, topic + + Metamaps.GlobalUI.notifyUser('Working...') + + var metacode = Metamaps.Metacodes.get(metacode_id) + + // variables to keep track of how many nodes and synapses you had the ability to change the permission of + var nCount = 0 + + // change the permission of the selected topics, if logged in user is the original creator + var l = Metamaps.Selected.Nodes.length + for (var i = l - 1; i >= 0; i -= 1) { + node = Metamaps.Selected.Nodes[i] + topic = node.getData('topic') + + if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + topic.save({ + 'metacode_id': metacode_id + }) + nCount++ + } + } + + var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') + + var message = nString + ' you can edit updated to ' + metacode.get('name') + Metamaps.GlobalUI.notifyUser(message) + Metamaps.Visualize.mGraph.plot() + }, +}; // end Metamaps.Control diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/app/assets/javascripts/src/Metamaps.Create.js new file mode 100644 index 00000000..bb01d129 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Create.js @@ -0,0 +1,326 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Create.js + * + * Dependencies: + * - Metamaps.Backbone + * - Metamaps.GlobalUI + * - Metamaps.Metacodes + * - Metamaps.Mouse + * - Metamaps.Selected + * - Metamaps.Synapse + * - Metamaps.Topic + * - Metamaps.Visualize + */ + +Metamaps.Create = { + isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open + selectedMetacodeSet: null, + selectedMetacodeSetIndex: null, + selectedMetacodeNames: [], + newSelectedMetacodeNames: [], + selectedMetacodes: [], + newSelectedMetacodes: [], + init: function () { + var self = Metamaps.Create + self.newTopic.init() + self.newSynapse.init() + + // // SWITCHING METACODE SETS + + $('#metacodeSwitchTabs').tabs({ + selected: self.selectedMetacodeSetIndex + }).addClass('ui-tabs-vertical ui-helper-clearfix') + $('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') + $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab + }, + toggleMetacodeSelected: function () { + var self = Metamaps.Create + + if ($(this).attr('class') != 'toggledOff') { + $(this).addClass('toggledOff') + var value_to_remove = $(this).attr('id') + var name_to_remove = $(this).attr('data-name') + self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(value_to_remove), 1) + self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(name_to_remove), 1) + } else if ($(this).attr('class') == 'toggledOff') { + $(this).removeClass('toggledOff') + self.newSelectedMetacodes.push($(this).attr('id')) + self.newSelectedMetacodeNames.push($(this).attr('data-name')) + } + }, + updateMetacodeSet: function (set, index, custom) { + if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { + alert('Please select at least one metacode to use!') + return false + } + + var codesToSwitchToIds + var metacodeModels = new Metamaps.Backbone.MetacodeCollection() + Metamaps.Create.selectedMetacodeSetIndex = index + Metamaps.Create.selectedMetacodeSet = 'metacodeset-' + set + + if (!custom) { + codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(',') + $('.customMetacodeList li').addClass('toggledOff') + Metamaps.Create.selectedMetacodes = [] + Metamaps.Create.selectedMetacodeNames = [] + Metamaps.Create.newSelectedMetacodes = [] + Metamaps.Create.newSelectedMetacodeNames = [] + } + else if (custom) { + // uses .slice to avoid setting the two arrays to the same actual array + Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0) + Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0) + codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0) + } + + // sort by name + for (var i = 0; i < codesToSwitchToIds.length; i++) { + metacodeModels.add(Metamaps.Metacodes.get(codesToSwitchToIds[i])) + } + metacodeModels.sort() + + $('#metacodeImg, #metacodeImgTitle').empty() + $('#metacodeImg').removeData('cloudcarousel') + var newMetacodes = '' + metacodeModels.each(function (metacode) { + newMetacodes += '' + metacode.get('name') + '' + }) + + $('#metacodeImg').empty().append(newMetacodes).CloudCarousel({ + titleBox: $('#metacodeImgTitle'), + yRadius: 40, + xRadius: 190, + xPos: 170, + yPos: 40, + speed: 0.3, + mouseWheel: true, + bringToFront: true + }) + + Metamaps.GlobalUI.closeLightbox() + $('#topic_name').focus() + + var mdata = { + 'metacodes': { + 'value': custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet + } + } + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/user/updatemetacodes', + data: mdata, + success: function (data) { + console.log('selected metacodes saved') + }, + error: function () { + console.log('failed to save selected metacodes') + } + }) + }, + + cancelMetacodeSetSwitch: function () { + var self = Metamaps.Create + self.isSwitchingSet = false + + if (self.selectedMetacodeSet != 'metacodeset-custom') { + $('.customMetacodeList li').addClass('toggledOff') + self.selectedMetacodes = [] + self.selectedMetacodeNames = [] + self.newSelectedMetacodes = [] + self.newSelectedMetacodeNames = [] + } else { // custom set is selected + // reset it to the current actual selection + $('.customMetacodeList li').addClass('toggledOff') + for (var i = 0; i < self.selectedMetacodes.length; i++) { + $('#' + self.selectedMetacodes[i]).removeClass('toggledOff') + } + // uses .slice to avoid setting the two arrays to the same actual array + self.newSelectedMetacodeNames = self.selectedMetacodeNames.slice(0) + self.newSelectedMetacodes = self.selectedMetacodes.slice(0) + } + $('#metacodeSwitchTabs').tabs('option', 'active', self.selectedMetacodeSetIndex) + $('#topic_name').focus() + }, + newTopic: { + init: function () { + $('#topic_name').keyup(function () { + Metamaps.Create.newTopic.name = $(this).val() + }) + + var topicBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/topics/autocomplete_topic?term=%QUERY', + wildcard: '%QUERY', + }, + }) + + // initialize the autocomplete results for the metacode spinner + $('#topic_name').typeahead( + { + highlight: true, + minLength: 2, + }, + [{ + name: 'topic_autocomplete', + limit: 8, + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s) + }, + }, + source: topicBloodhound, + }] + ) + + // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete + $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { + Metamaps.Topic.getTopicFromAutocomplete(datum.id) + }) + + // initialize metacode spinner and then hide it + $('#metacodeImg').CloudCarousel({ + titleBox: $('#metacodeImgTitle'), + yRadius: 40, + xRadius: 190, + xPos: 170, + yPos: 40, + speed: 0.3, + mouseWheel: true, + bringToFront: true + }) + $('.new_topic').hide() + }, + name: null, + newId: 1, + beingCreated: false, + metacode: null, + x: null, + y: null, + addSynapse: false, + open: function () { + $('#new_topic').fadeIn('fast', function () { + $('#topic_name').focus() + }) + Metamaps.Create.newTopic.beingCreated = true + Metamaps.Create.newTopic.name = '' + }, + hide: function () { + $('#new_topic').fadeOut('fast') + $('#topic_name').typeahead('val', '') + Metamaps.Create.newTopic.beingCreated = false + } + }, + newSynapse: { + init: function () { + var self = Metamaps.Create.newSynapse + + var synapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?term=%QUERY', + wildcard: '%QUERY', + }, + }) + var existingSynapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', + prepare: function (query, settings) { + var self = Metamaps.Create.newSynapse + if (Metamaps.Selected.Nodes.length < 2) { + settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) + return settings + } else { + return null + } + }, + }, + }) + + // initialize the autocomplete results for synapse creation + $('#synapse_desc').typeahead( + { + highlight: true, + minLength: 2, + }, + [{ + name: 'synapse_autocomplete', + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile("
    {{label}}
    ").render(s) + }, + }, + source: synapseBloodhound, + }, + { + name: 'existing_synapses', + limit: 50, + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s) + }, + header: '

    Existing synapses

    ' + }, + source: existingSynapseBloodhound, + }] + ) + + $('#synapse_desc').keyup(function (e) { + var ESC = 27, BACKSPACE = 8, DELETE = 46 + if (e.keyCode === BACKSPACE && $(this).val() === '' || + e.keyCode === DELETE && $(this).val() === '' || + e.keyCode === ESC) { + Metamaps.Create.newSynapse.hide() + } // if + Metamaps.Create.newSynapse.description = $(this).val() + }) + + $('#synapse_desc').focusout(function () { + if (Metamaps.Create.newSynapse.beingCreated) { + Metamaps.Synapse.createSynapseLocally() + } + }) + + $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { + if (datum.id) { // if they clicked on an existing synapse get it + Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) + } else { + Metamaps.Create.newSynapse.description = datum.value + Metamaps.Synapse.createSynapseLocally() + } + }) + }, + beingCreated: false, + description: null, + topic1id: null, + topic2id: null, + newSynapseId: null, + open: function () { + $('#new_synapse').fadeIn(100, function () { + $('#synapse_desc').focus() + }) + Metamaps.Create.newSynapse.beingCreated = true + }, + hide: function () { + $('#new_synapse').fadeOut('fast') + $('#synapse_desc').typeahead('val', '') + Metamaps.Create.newSynapse.beingCreated = false + Metamaps.Create.newTopic.addSynapse = false + Metamaps.Create.newSynapse.topic1id = 0 + Metamaps.Create.newSynapse.topic2id = 0 + Metamaps.Mouse.synapseStartCoordinates = [] + Metamaps.Visualize.mGraph.plot() + }, + } +}; // end Metamaps.Create diff --git a/app/assets/javascripts/src/Metamaps.Debug.js.erb b/app/assets/javascripts/src/Metamaps.Debug.js similarity index 72% rename from app/assets/javascripts/src/Metamaps.Debug.js.erb rename to app/assets/javascripts/src/Metamaps.Debug.js index 3989b7fb..7dc61246 100644 --- a/app/assets/javascripts/src/Metamaps.Debug.js.erb +++ b/app/assets/javascripts/src/Metamaps.Debug.js @@ -4,10 +4,10 @@ * Dependencies: none! */ -Metamaps.Debug = function() { +Metamaps.Debug = function () { console.debug(Metamaps) console.debug(`Metamaps Version: ${Metamaps.VERSION}`) } -Metamaps.debug = function() { +Metamaps.debug = function () { Metamaps.Debug() } diff --git a/app/assets/javascripts/src/Metamaps.Filter.js b/app/assets/javascripts/src/Metamaps.Filter.js new file mode 100644 index 00000000..1dba099c --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Filter.js @@ -0,0 +1,466 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Filter.js.erb + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Control + * - Metamaps.Creators + * - Metamaps.GlobalUI + * - Metamaps.Mappers + * - Metamaps.Metacodes + * - Metamaps.Settings + * - Metamaps.Synapses + * - Metamaps.Topics + * - Metamaps.Visualize + */ +Metamaps.Filter = { + filters: { + name: '', + metacodes: [], + mappers: [], + synapses: [] + }, + visible: { + metacodes: [], + mappers: [], + synapses: [] + }, + isOpen: false, + changing: false, + init: function () { + var self = Metamaps.Filter + + $('.sidebarFilterIcon').click(self.toggleBox) + + $('.sidebarFilterBox .showAllMetacodes').click(self.filterNoMetacodes) + $('.sidebarFilterBox .showAllSynapses').click(self.filterNoSynapses) + $('.sidebarFilterBox .showAllMappers').click(self.filterNoMappers) + $('.sidebarFilterBox .hideAllMetacodes').click(self.filterAllMetacodes) + $('.sidebarFilterBox .hideAllSynapses').click(self.filterAllSynapses) + $('.sidebarFilterBox .hideAllMappers').click(self.filterAllMappers) + + self.bindLiClicks() + self.getFilterData() + }, + toggleBox: function (event) { + var self = Metamaps.Filter + + if (self.isOpen) self.close() + else self.open() + + event.stopPropagation() + }, + open: function () { + var self = Metamaps.Filter + + Metamaps.GlobalUI.Account.close() + $('.sidebarFilterIcon div').addClass('hide') + + if (!self.isOpen && !self.changing) { + self.changing = true + + var height = $(document).height() - 108 + $('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function () { + self.changing = false + self.isOpen = true + }) + } + }, + close: function () { + var self = Metamaps.Filter + $('.sidebarFilterIcon div').removeClass('hide') + + if (!self.changing) { + self.changing = true + + $('.sidebarFilterBox').fadeOut(200, function () { + self.changing = false + self.isOpen = false + }) + } + }, + reset: function () { + var self = Metamaps.Filter + + self.filters.metacodes = [] + self.filters.mappers = [] + self.filters.synapses = [] + self.visible.metacodes = [] + self.visible.mappers = [] + self.visible.synapses = [] + + $('#filter_by_metacode ul').empty() + $('#filter_by_mapper ul').empty() + $('#filter_by_synapse ul').empty() + + $('.filterBox .showAll').addClass('active') + }, + /* + Most of this data essentially depends on the ruby function which are happening for filter inside view filterBox + But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses + */ + getFilterData: function () { + var self = Metamaps.Filter + + var metacode, mapper, synapse + + $('#filter_by_metacode li').each(function () { + metacode = $(this).attr('data-id') + self.filters.metacodes.push(metacode) + self.visible.metacodes.push(metacode) + }) + + $('#filter_by_mapper li').each(function () { + mapper = ($(this).attr('data-id')) + self.filters.mappers.push(mapper) + self.visible.mappers.push(mapper) + }) + + $('#filter_by_synapse li').each(function () { + synapse = ($(this).attr('data-id')) + self.filters.synapses.push(synapse) + self.visible.synapses.push(synapse) + }) + }, + bindLiClicks: function () { + var self = Metamaps.Filter + $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode) + $('#filter_by_mapper ul li').unbind().click(self.toggleMapper) + $('#filter_by_synapse ul li').unbind().click(self.toggleSynapse) + }, + // an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce + // code redundancy + /* + @param + */ + updateFilters: function (collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { + var self = Metamaps.Filter + + var newList = [] + var removed = [] + var added = [] + + // the first option enables us to accept + // ['Topics', 'Synapses'] as 'collection' + if (typeof collection === 'object') { + Metamaps[collection[0]].each(function (model) { + var prop = model.get(propertyToCheck) + if (prop !== null) { + prop = prop.toString() + if (newList.indexOf(prop) === -1) { + newList.push(prop) + } + } + }) + Metamaps[collection[1]].each(function (model) { + var prop = model.get(propertyToCheck) + if (prop !== null) { + prop = prop.toString() + if (newList.indexOf(prop) === -1) { + newList.push(prop) + } + } + }) + } else if (typeof collection === 'string') { + Metamaps[collection].each(function (model) { + var prop = model.get(propertyToCheck) + if (prop !== null) { + prop = prop.toString() + if (newList.indexOf(prop) === -1) { + newList.push(prop) + } + } + }) + } + + removed = _.difference(self.filters[filtersToUse], newList) + added = _.difference(newList, self.filters[filtersToUse]) + + // remove the list items for things no longer present on the map + _.each(removed, function (identifier) { + $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function () { + $(this).remove() + }) + index = self.visible[filtersToUse].indexOf(identifier) + self.visible[filtersToUse].splice(index, 1) + }) + + var model, li, jQueryLi + function sortAlpha (a, b) { + return a.childNodes[1].innerHTML.toLowerCase() > b.childNodes[1].innerHTML.toLowerCase() ? 1 : -1 + } + // for each new filter to be added, create a list item for it and fade it in + _.each(added, function (identifier) { + model = Metamaps[correlatedModel].get(identifier) || + Metamaps[correlatedModel].find(function (model) { + return model.get(propertyToCheck) === identifier + }) + li = model.prepareLiForFilter() + jQueryLi = $(li).hide() + $('li', '#filter_by_' + listToModify + ' ul').add(jQueryLi.fadeIn('fast')) + .sort(sortAlpha).appendTo('#filter_by_' + listToModify + ' ul') + self.visible[filtersToUse].push(identifier) + }) + + // update the list of filters with the new list we just generated + self.filters[filtersToUse] = newList + + // make sure clicks on list items still trigger the right events + self.bindLiClicks() + }, + checkMetacodes: function () { + var self = Metamaps.Filter + self.updateFilters('Topics', 'metacode_id', 'Metacodes', 'metacodes', 'metacode') + }, + checkMappers: function () { + var self = Metamaps.Filter + var onMap = Metamaps.Active.Map ? true : false + if (onMap) { + self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') + } else { + // on topic view + self.updateFilters(['Topics', 'Synapses'], 'user_id', 'Creators', 'mappers', 'mapper') + } + }, + checkSynapses: function () { + var self = Metamaps.Filter + self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') + }, + filterAllMetacodes: function (e) { + var self = Metamaps.Filter + $('#filter_by_metacode ul li').addClass('toggledOff') + $('.showAllMetacodes').removeClass('active') + $('.hideAllMetacodes').addClass('active') + self.visible.metacodes = [] + self.passFilters() + }, + filterNoMetacodes: function (e) { + var self = Metamaps.Filter + $('#filter_by_metacode ul li').removeClass('toggledOff') + $('.showAllMetacodes').addClass('active') + $('.hideAllMetacodes').removeClass('active') + self.visible.metacodes = self.filters.metacodes.slice() + self.passFilters() + }, + filterAllMappers: function (e) { + var self = Metamaps.Filter + $('#filter_by_mapper ul li').addClass('toggledOff') + $('.showAllMappers').removeClass('active') + $('.hideAllMappers').addClass('active') + self.visible.mappers = [] + self.passFilters() + }, + filterNoMappers: function (e) { + var self = Metamaps.Filter + $('#filter_by_mapper ul li').removeClass('toggledOff') + $('.showAllMappers').addClass('active') + $('.hideAllMappers').removeClass('active') + self.visible.mappers = self.filters.mappers.slice() + self.passFilters() + }, + filterAllSynapses: function (e) { + var self = Metamaps.Filter + $('#filter_by_synapse ul li').addClass('toggledOff') + $('.showAllSynapses').removeClass('active') + $('.hideAllSynapses').addClass('active') + self.visible.synapses = [] + self.passFilters() + }, + filterNoSynapses: function (e) { + var self = Metamaps.Filter + $('#filter_by_synapse ul li').removeClass('toggledOff') + $('.showAllSynapses').addClass('active') + $('.hideAllSynapses').removeClass('active') + self.visible.synapses = self.filters.synapses.slice() + self.passFilters() + }, + // an abstraction function for toggleMetacode, toggleMapper, toggleSynapse + // to reduce code redundancy + // gets called in the context of a list item in a filter box + toggleLi: function (whichToFilter) { + var self = Metamaps.Filter, index + var id = $(this).attr('data-id') + if (self.visible[whichToFilter].indexOf(id) == -1) { + self.visible[whichToFilter].push(id) + $(this).removeClass('toggledOff') + } else { + index = self.visible[whichToFilter].indexOf(id) + self.visible[whichToFilter].splice(index, 1) + $(this).addClass('toggledOff') + } + self.passFilters() + }, + toggleMetacode: function () { + var self = Metamaps.Filter + self.toggleLi.call(this, 'metacodes') + + if (self.visible.metacodes.length === self.filters.metacodes.length) { + $('.showAllMetacodes').addClass('active') + $('.hideAllMetacodes').removeClass('active') + } + else if (self.visible.metacodes.length === 0) { + $('.showAllMetacodes').removeClass('active') + $('.hideAllMetacodes').addClass('active') + } else { + $('.showAllMetacodes').removeClass('active') + $('.hideAllMetacodes').removeClass('active') + } + }, + toggleMapper: function () { + var self = Metamaps.Filter + self.toggleLi.call(this, 'mappers') + + if (self.visible.mappers.length === self.filters.mappers.length) { + $('.showAllMappers').addClass('active') + $('.hideAllMappers').removeClass('active') + } + else if (self.visible.mappers.length === 0) { + $('.showAllMappers').removeClass('active') + $('.hideAllMappers').addClass('active') + } else { + $('.showAllMappers').removeClass('active') + $('.hideAllMappers').removeClass('active') + } + }, + toggleSynapse: function () { + var self = Metamaps.Filter + self.toggleLi.call(this, 'synapses') + + if (self.visible.synapses.length === self.filters.synapses.length) { + $('.showAllSynapses').addClass('active') + $('.hideAllSynapses').removeClass('active') + } + else if (self.visible.synapses.length === 0) { + $('.showAllSynapses').removeClass('active') + $('.hideAllSynapses').addClass('active') + } else { + $('.showAllSynapses').removeClass('active') + $('.hideAllSynapses').removeClass('active') + } + }, + passFilters: function () { + var self = Metamaps.Filter + var visible = self.visible + + var passesMetacode, passesMapper, passesSynapse + var onMap + + if (Metamaps.Active.Map) { + onMap = true + } + else if (Metamaps.Active.Topic) { + onMap = false + } + + var opacityForFilter = onMap ? 0 : 0.4 + + Metamaps.Topics.each(function (topic) { + var n = topic.get('node') + var metacode_id = topic.get('metacode_id').toString() + + if (visible.metacodes.indexOf(metacode_id) == -1) passesMetacode = false + else passesMetacode = true + + if (onMap) { + // when on a map, + // we filter by mapper according to the person who added the + // topic or synapse to the map + var user_id = topic.getMapping().get('user_id').toString() + if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + else passesMapper = true + } else { + // when on a topic view, + // we filter by mapper according to the person who created the + // topic or synapse + var user_id = topic.get('user_id').toString() + if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + else passesMapper = true + } + + if (passesMetacode && passesMapper) { + if (n) { + n.setData('alpha', 1, 'end') + } + else console.log(topic) + } else { + if (n) { + Metamaps.Control.deselectNode(n, true) + n.setData('alpha', opacityForFilter, 'end') + n.eachAdjacency(function (e) { + Metamaps.Control.deselectEdge(e, true) + }) + } + else console.log(topic) + } + }) + + // flag all the edges back to 'untouched' + Metamaps.Synapses.each(function (synapse) { + var e = synapse.get('edge') + e.setData('touched', false) + }) + Metamaps.Synapses.each(function (synapse) { + var e = synapse.get('edge') + var desc + var user_id = synapse.get('user_id').toString() + + if (e && !e.getData('touched')) { + var synapses = e.getData('synapses') + + // if any of the synapses represent by the edge are still unfiltered + // leave the edge visible + passesSynapse = false + for (var i = 0; i < synapses.length; i++) { + desc = synapses[i].get('desc') + if (visible.synapses.indexOf(desc) > -1) passesSynapse = true + } + + // if the synapse description being displayed is now being + // filtered, set the displayIndex to the first unfiltered synapse if there is one + var displayIndex = e.getData('displayIndex') ? e.getData('displayIndex') : 0 + var displayedSynapse = synapses[displayIndex] + desc = displayedSynapse.get('desc') + if (passesSynapse && visible.synapses.indexOf(desc) == -1) { + // iterate and find an unfiltered one + for (var i = 0; i < synapses.length; i++) { + desc = synapses[i].get('desc') + if (visible.synapses.indexOf(desc) > -1) { + e.setData('displayIndex', i) + break + } + } + } + + if (onMap) { + // when on a map, + // we filter by mapper according to the person who added the + // topic or synapse to the map + user_id = synapse.getMapping().get('user_id').toString() + } + if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + else passesMapper = true + + var color = Metamaps.Settings.colors.synapses.normal + if (passesSynapse && passesMapper) { + e.setData('alpha', 1, 'end') + e.setData('color', color, 'end') + } else { + Metamaps.Control.deselectEdge(e, true) + e.setData('alpha', opacityForFilter, 'end') + } + + e.setData('touched', true) + } + else if (!e) console.log(synapse) + }) + + // run the animation + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['node-property:alpha', + 'edge-property:alpha'], + duration: 200 + }) + } +}; // end Metamaps.Filter diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.GlobalUI.js.erb rename to app/assets/javascripts/src/Metamaps.GlobalUI.js index 690bba1f..d8b91a31 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js @@ -93,6 +93,7 @@ Metamaps.GlobalUI = { if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; var mapperCollection = []; var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; if (Metamaps.Maps.Mapper) { @@ -102,6 +103,7 @@ Metamaps.GlobalUI = { var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); // 'Mapper' refers to another mapper Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); @@ -501,7 +503,7 @@ Metamaps.GlobalUI.Search = { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ value: "No results", label: "No results", - typeImageURL: "<%= asset_path('icons/wildcard.png') %>", + typeImageURL: Metamaps.Erb['icons/wildcard.png'], rtype: "noresult" }); }, @@ -569,7 +571,7 @@ Metamaps.GlobalUI.Search = { value: "No results", label: "No results", rtype: "noresult", - profile: "<%= asset_path('user.png') %>", + profile: Metamaps.Erb['user.png'] }); }, header: mapperheader, diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Import.js.erb rename to app/assets/javascripts/src/Metamaps.Import.js index 6578afc7..159298b8 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -229,7 +229,7 @@ Metamaps.Import = { importSynapses: function (parsedSynapses) { var self = Metamaps.Import - parsedSynapses.forEach(function(synapse) { + parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted var topic1 = Metamaps.Topics.get(self.cidMappings[synapse.topic1]) var topic2 = Metamaps.Topics.get(self.cidMappings[synapse.topic2]) @@ -320,3 +320,5 @@ Metamaps.Import = { Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true) } } + +module.exports = Metamaps.Import diff --git a/app/assets/javascripts/src/Metamaps.JIT.js b/app/assets/javascripts/src/Metamaps.JIT.js new file mode 100644 index 00000000..01b321f7 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.JIT.js @@ -0,0 +1,1908 @@ +Metamaps.JIT = { + events: { + topicDrag: 'Metamaps:JIT:events:topicDrag', + newTopic: 'Metamaps:JIT:events:newTopic', + deleteTopic: 'Metamaps:JIT:events:deleteTopic', + removeTopic: 'Metamaps:JIT:events:removeTopic', + newSynapse: 'Metamaps:JIT:events:newSynapse', + deleteSynapse: 'Metamaps:JIT:events:deleteSynapse', + removeSynapse: 'Metamaps:JIT:events:removeSynapse', + pan: 'Metamaps:JIT:events:pan', + zoom: 'Metamaps:JIT:events:zoom', + animationDone: 'Metamaps:JIT:events:animationDone', + }, + vizData: [], // contains the visualization-compatible graph + /** + * This method will bind the event handlers it is interested and initialize the class. + */ + init: function () { + var self = Metamaps.JIT + + $('.zoomIn').click(self.zoomIn) + $('.zoomOut').click(self.zoomOut) + + var zoomExtents = function (event) { + self.zoomExtents(event, Metamaps.Visualize.mGraph.canvas) + } + $('.zoomExtents').click(zoomExtents) + + $('.takeScreenshot').click(Metamaps.Map.exportImage) + + self.topicDescImage = new Image() + self.topicDescImage.src = Metamaps.Erb['topic_description_signifier.png'] + + self.topicLinkImage = new Image() + self.topicLinkImage.src = Metamaps.Erb['topic_link_signifier.png'] + }, + /** + * convert our topic JSON into something JIT can use + */ + convertModelsToJIT: function (topics, synapses) { + var jitReady = [] + + var synapsesToRemove = [] + var topic + var mapping + var node + var nodes = {} + var existingEdge + var edge + var edges = [] + + topics.each(function (t) { + node = t.createNode() + nodes[node.id] = node + }) + synapses.each(function (s) { + edge = s.createEdge() + + if (topics.get(s.get('node1_id')) === undefined || topics.get(s.get('node2_id')) === undefined) { + // this means it's an invalid synapse + synapsesToRemove.push(s) + } + else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { + existingEdge = _.findWhere(edges, { + nodeFrom: edge.nodeFrom, + nodeTo: edge.nodeTo + }) || + _.findWhere(edges, { + nodeFrom: edge.nodeTo, + nodeTo: edge.nodeFrom + }) + + if (existingEdge) { + // for when you're dealing with multiple relationships between the same two topics + if (Metamaps.Active.Map) { + mapping = s.getMapping() + existingEdge.data['$mappingIDs'].push(mapping.id) + } + existingEdge.data['$synapseIDs'].push(s.id) + } else { + // for when you're dealing with a topic that has relationships to many different nodes + nodes[edge.nodeFrom].adjacencies.push(edge) + edges.push(edge) + } + } + }) + + _.each(nodes, function (node) { + jitReady.push(node) + }) + + return [jitReady, synapsesToRemove] + }, + prepareVizData: function () { + var self = Metamaps.JIT + var mapping + + // reset/empty vizData + self.vizData = [] + Metamaps.Visualize.loadLater = false + + var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) + + self.vizData = results[0] + + // clean up the synapses array in case of any faulty data + _.each(results[1], function (synapse) { + mapping = synapse.getMapping() + Metamaps.Synapses.remove(synapse) + if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping) + }) + + if (self.vizData.length == 0) { + Metamaps.Famous.viz.showInstructions() + Metamaps.Visualize.loadLater = true + } + else Metamaps.Famous.viz.hideInstructions() + + Metamaps.Visualize.render() + }, // prepareVizData + edgeRender: function (adj, canvas) { + // get nodes cartesian coordinates + var pos = adj.nodeFrom.pos.getc(true) + var posChild = adj.nodeTo.pos.getc(true) + + var synapse + if (adj.getData('displayIndex')) { + synapse = adj.getData('synapses')[adj.getData('displayIndex')] + if (!synapse) { + delete adj.data.$displayIndex + synapse = adj.getData('synapses')[0] + } + } else { + synapse = adj.getData('synapses')[0] + } + + if (!synapse) return // this means there are no corresponding synapses for + // this edge, don't render it + + var directionCat = synapse.get('category') + + // label placement on edges + if (canvas.denySelected) { + var color = Metamaps.Settings.colors.synapses.normal + canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color + } + Metamaps.JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) + + // check for edge label in data + var desc = synapse.get('desc') + + var showDesc = adj.getData('showDesc') + + var drawSynapseCount = function (context, x, y, count) { + /* + circle size: 16x16px + positioning: overlay and center on top right corner of synapse label - 8px left and 8px down + color: #dab539 + border color: #424242 + border size: 1.5px + font: DIN medium + font-size: 14pt + font-color: #424242 + */ + context.beginPath() + context.arc(x, y, 8, 0, 2 * Math.PI, false) + context.fillStyle = '#DAB539' + context.strokeStyle = '#424242' + context.lineWidth = 1.5 + context.closePath() + context.fill() + context.stroke() + + // add the synapse count + context.fillStyle = '#424242' + context.textAlign = 'center' + context.font = '14px din-medium' + + context.fillText(count, x, y + 5) + } + + if (!canvas.denySelected && desc != '' && showDesc) { + // '&' to '&' + desc = Metamaps.Util.decodeEntities(desc) + + // now adjust the label placement + var ctx = canvas.getCtx() + ctx.font = 'bold 14px arial' + ctx.fillStyle = '#FFF' + ctx.textBaseline = 'alphabetic' + + var arrayOfLabelLines = Metamaps.Util.splitLine(desc, 30).split('\n') + var index, lineWidths = [] + for (index = 0; index < arrayOfLabelLines.length; ++index) { + lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) + } + var width = Math.max.apply(null, lineWidths) + 16 + var height = (16 * arrayOfLabelLines.length) + 8 + + var x = (pos.x + posChild.x - width) / 2 + var y = ((pos.y + posChild.y) / 2) - height / 2 + + var radius = 5 + + // render background + ctx.beginPath() + ctx.moveTo(x + radius, y) + ctx.lineTo(x + width - radius, y) + ctx.quadraticCurveTo(x + width, y, x + width, y + radius) + ctx.lineTo(x + width, y + height - radius) + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height) + ctx.lineTo(x + radius, y + height) + ctx.quadraticCurveTo(x, y + height, x, y + height - radius) + ctx.lineTo(x, y + radius) + ctx.quadraticCurveTo(x, y, x + radius, y) + ctx.closePath() + ctx.fill() + + // get number of synapses + var synapseNum = adj.getData('synapses').length + + // render text + ctx.fillStyle = '#424242' + ctx.textAlign = 'center' + for (index = 0; index < arrayOfLabelLines.length; ++index) { + ctx.fillText(arrayOfLabelLines[index], x + (width / 2), y + 18 + (16 * index)) + } + + if (synapseNum > 1) { + drawSynapseCount(ctx, x + width, y, synapseNum) + } + } + else if (!canvas.denySelected && showDesc) { + // get number of synapses + var synapseNum = adj.getData('synapses').length + + if (synapseNum > 1) { + var ctx = canvas.getCtx() + var x = (pos.x + posChild.x) / 2 + var y = (pos.y + posChild.y) / 2 + drawSynapseCount(ctx, x, y, synapseNum) + } + } + }, // edgeRender + ForceDirected: { + animateSavedLayout: { + modes: ['linear'], + transition: $jit.Trans.Quad.easeInOut, + duration: 800, + onComplete: function () { + Metamaps.Visualize.mGraph.busy = false + $(document).trigger(Metamaps.JIT.events.animationDone) + } + }, + animateFDLayout: { + modes: ['linear'], + transition: $jit.Trans.Elastic.easeOut, + duration: 800, + onComplete: function () { + Metamaps.Visualize.mGraph.busy = false + } + }, + graphSettings: { + // id of the visualization container + injectInto: 'infovis', + // Enable zooming and panning + // by scrolling and DnD + Navigation: { + enable: true, + // Enable panning events only if we're dragging the empty + // canvas (and not a node). + panning: 'avoid nodes', + zooming: 28 // zoom speed. higher is more sensible + }, + // background: { + // type: 'Metamaps' + // }, + // NodeStyles: { + // enable: true, + // type: 'Native', + // stylesHover: { + // dim: 30 + // }, + // duration: 300 + // }, + // Change node and edge styles such as + // color and width. + // These properties are also set per node + // with dollar prefixed data-properties in the + // JSON structure. + Node: { + overridable: true, + color: '#2D6A5D', + type: 'customNode', + dim: 25 + }, + Edge: { + overridable: true, + color: Metamaps.Settings.colors.synapses.normal, + type: 'customEdge', + lineWidth: 2, + alpha: 1 + }, + // Native canvas text styling + Label: { + type: 'Native', // Native or HTML + size: 20, + family: 'arial', + textBaseline: 'alphabetic', + color: Metamaps.Settings.colors.labels.text + }, + // Add Tips + Tips: { + enable: false, + onShow: function (tip, node) {} + }, + // Add node events + Events: { + enable: true, + enableForEdges: true, + onMouseMove: function (node, eventInfo, e) { + Metamaps.JIT.onMouseMoveHandler(node, eventInfo, e) + // console.log('called mouse move handler') + }, + // Update node positions when dragged + onDragMove: function (node, eventInfo, e) { + Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e) + // console.log('called drag move handler') + }, + onDragEnd: function (node, eventInfo, e) { + Metamaps.JIT.onDragEndTopicHandler(node, eventInfo, e, false) + // console.log('called drag end handler') + }, + onDragCancel: function (node, eventInfo, e) { + Metamaps.JIT.onDragCancelHandler(node, eventInfo, e, false) + }, + // Implement the same handler for touchscreens + onTouchStart: function (node, eventInfo, e) { + Metamaps.Visualize.mGraph.events.touched = true + + var canvas = Metamaps.Visualize.mGraph.canvas + + Metamaps.Touch.touchPos = eventInfo.getPos() + Metamaps.Touch.touchPos.x *= canvas.scaleOffsetX + Metamaps.Touch.touchPos.y *= canvas.scaleOffsetY + Metamaps.Touch.touchPos.x += canvas.translateOffsetX + Metamaps.Touch.touchPos.y += canvas.translateOffsetY + + touchDragNode = node + }, + // Implement the same handler for touchscreens + onTouchMove: function (node, eventInfo, e) { + if (Metamaps.Touch.touchDragNode) { + Metamaps.JIT.onDragMoveTopicHandler(Metamaps.Touch.touchDragNode, eventInfo, e) + } else { + Metamaps.JIT.touchPanZoomHandler(eventInfo, e) + } + }, + // Implement the same handler for touchscreens + onTouchEnd: function (node, eventInfo, e) {}, + // Implement the same handler for touchscreens + onTouchCancel: function (node, eventInfo, e) {}, + // Add also a click handler to nodes + onClick: function (node, eventInfo, e) { + // remove the rightclickmenu + $('.rightclickmenu').remove() + + if (Metamaps.Mouse.boxStartCoordinates) { + if (e.ctrlKey) { + Metamaps.Visualize.mGraph.busy = false + Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + + var bS = Metamaps.Mouse.boxStartCoordinates + var bE = Metamaps.Mouse.boxEndCoordinates + if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { + Metamaps.JIT.zoomToBox(e) + return + } else { + Metamaps.Mouse.boxStartCoordinates = null + Metamaps.Mouse.boxEndCoordinates = null + } + // console.log('called zoom to box') + } + + if (e.shiftKey) { + Metamaps.Visualize.mGraph.busy = false + Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Metamaps.JIT.selectWithBox(e) + // console.log('called select with box') + return + } + } + + if (e.target.id != 'infovis-canvas') return false + + // clicking on a edge, node, or clicking on blank part of canvas? + if (node.nodeFrom) { + Metamaps.JIT.selectEdgeOnClickHandler(node, e) + // console.log('called selectEdgeOnClickHandler') + } else if (node && !node.nodeFrom) { + Metamaps.JIT.selectNodeOnClickHandler(node, e) + // console.log('called selectNodeOnClickHandler') + } else { + Metamaps.JIT.canvasClickHandler(eventInfo.getPos(), e) + // console.log('called canvasClickHandler') + } // if + }, + // Add also a click handler to nodes + onRightClick: function (node, eventInfo, e) { + // remove the rightclickmenu + $('.rightclickmenu').remove() + + if (Metamaps.Mouse.boxStartCoordinates) { + Metamaps.Visualize.mGraph.busy = false + Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Metamaps.JIT.selectWithBox(e) + return + } + + if (e.target.id != 'infovis-canvas') return false + + // clicking on a edge, node, or clicking on blank part of canvas? + if (node.nodeFrom) { + Metamaps.JIT.selectEdgeOnRightClickHandler(node, e) + } else if (node && !node.nodeFrom) { + Metamaps.JIT.selectNodeOnRightClickHandler(node, e) + } else { + // console.log('right clicked on open space') + } + } + }, + // Number of iterations for the FD algorithm + iterations: 200, + // Edge length + levelDistance: 200, + }, + nodeSettings: { + 'customNode': { + 'render': function (node, canvas) { + var pos = node.pos.getc(true), + dim = node.getData('dim'), + topic = node.getData('topic'), + metacode = topic ? topic.getMetacode() : false, + ctx = canvas.getCtx() + + // if the topic is selected draw a circle around it + if (!canvas.denySelected && node.selected) { + ctx.beginPath() + ctx.arc(pos.x, pos.y, dim + 3, 0, 2 * Math.PI, false) + ctx.strokeStyle = Metamaps.Settings.colors.topics.selected + ctx.lineWidth = 2 + ctx.stroke() + } + + if (!metacode || + !metacode.get('image') || + !metacode.get('image').complete || + (typeof metacode.get('image').naturalWidth !== 'undefined' && + metacode.get('image').naturalWidth === 0)) { + ctx.beginPath() + ctx.arc(pos.x, pos.y, dim, 0, 2 * Math.PI, false) + ctx.fillStyle = '#B6B2FD' + ctx.fill() + } else { + ctx.drawImage(metacode.get('image'), pos.x - dim, pos.y - dim, dim * 2, dim * 2) + } + + // if the topic has a link, draw a small image to indicate that + var hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null + var linkImage = Metamaps.JIT.topicLinkImage + var linkImageLoaded = linkImage.complete || + (typeof linkImage.naturalWidth !== 'undefined' && + linkImage.naturalWidth !== 0) + if (hasLink && linkImageLoaded) { + ctx.drawImage(linkImage, pos.x - dim - 8, pos.y - dim - 8, 16, 16) + } + + // if the topic has a desc, draw a small image to indicate that + var hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null + var descImage = Metamaps.JIT.topicDescImage + var descImageLoaded = descImage.complete || + (typeof descImage.naturalWidth !== 'undefined' && + descImage.naturalWidth !== 0) + if (hasDesc && descImageLoaded) { + ctx.drawImage(descImage, pos.x + dim - 8, pos.y - dim - 8, 16, 16) + } + }, + 'contains': function (node, pos) { + var npos = node.pos.getc(true), + dim = node.getData('dim'), + arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 30).split('\n'), + ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + + var height = 25 * arrayOfLabelLines.length + + var index, lineWidths = [] + for (index = 0; index < arrayOfLabelLines.length; ++index) { + lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) + } + var width = Math.max.apply(null, lineWidths) + 8 + var labely = npos.y + node.getData('height') + 5 + height / 2 + + var overLabel = this.nodeHelper.rectangle.contains({ + x: npos.x, + y: labely + }, pos, width, height) + + return this.nodeHelper.circle.contains(npos, pos, dim) || overLabel + } + } + }, + edgeSettings: { + 'customEdge': { + 'render': function (adj, canvas) { + Metamaps.JIT.edgeRender(adj, canvas) + }, + 'contains': function (adj, pos) { + var from = adj.nodeFrom.pos.getc(), + to = adj.nodeTo.pos.getc() + + // this fixes an issue where when edges are perfectly horizontal or perfectly vertical + // it becomes incredibly difficult to hover over them + if (-1 < pos.x && pos.x < 1) pos.x = 0 + if (-1 < pos.y && pos.y < 1) pos.y = 0 + + return $jit.Graph.Plot.edgeHelper.line.contains(from, to, pos, adj.Edge.epsilon + 5) + } + } + } + }, // ForceDirected + ForceDirected3D: { + animate: { + modes: ['linear'], + transition: $jit.Trans.Elastic.easeOut, + duration: 2500, + onComplete: function () { + Metamaps.Visualize.mGraph.busy = false + } + }, + graphSettings: { + // id of the visualization container + injectInto: 'infovis', + type: '3D', + Scene: { + Lighting: { + enable: false, + ambient: [0.5, 0.5, 0.5], + directional: { + direction: { + x: 1, + y: 0, + z: -1 + }, + color: [0.9, 0.9, 0.9] + } + } + }, + // Enable zooming and panning + // by scrolling and DnD + Navigation: { + enable: false, + // Enable panning events only if we're dragging the empty + // canvas (and not a node). + panning: 'avoid nodes', + zooming: 10 // zoom speed. higher is more sensible + }, + // Change node and edge styles such as + // color and width. + // These properties are also set per node + // with dollar prefixed data-properties in the + // JSON structure. + Node: { + overridable: true, + type: 'sphere', + dim: 15, + color: '#ffffff' + }, + Edge: { + overridable: false, + type: 'tube', + color: '#111', + lineWidth: 3 + }, + // Native canvas text styling + Label: { + type: 'HTML', // Native or HTML + size: 10, + style: 'bold' + }, + // Add node events + Events: { + enable: true, + type: 'Native', + i: 0, + onMouseMove: function (node, eventInfo, e) { + // if(this.i++ % 3) return + var pos = eventInfo.getPos() + Metamaps.Visualize.cameraPosition.x += (pos.x - Metamaps.Visualize.cameraPosition.x) * 0.5 + Metamaps.Visualize.cameraPosition.y += (-pos.y - Metamaps.Visualize.cameraPosition.y) * 0.5 + Metamaps.Visualize.mGraph.plot() + }, + onMouseWheel: function (delta) { + Metamaps.Visualize.cameraPosition.z += -delta * 20 + Metamaps.Visualize.mGraph.plot() + }, + onClick: function () {} + }, + // Number of iterations for the FD algorithm + iterations: 200, + // Edge length + levelDistance: 100 + }, + nodeSettings: { + + }, + edgeSettings: { + + } + }, // ForceDirected3D + RGraph: { + animate: { + modes: ['polar'], + duration: 800, + onComplete: function () { + Metamaps.Visualize.mGraph.busy = false + } + }, + // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ + background: { + // type: 'Metamaps', + levelDistance: 200, + numberOfCircles: 4, + CanvasStyles: { + strokeStyle: '#333', + lineWidth: 1.5 + } + }, + levelDistance: 200 + }, + onMouseEnter: function (edge) { + var filtered = edge.getData('alpha') === 0 + + // don't do anything if the edge is filtered + // or if the canvas is animating + if (filtered || Metamaps.Visualize.mGraph.busy) return + + $('canvas').css('cursor', 'pointer') + var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + // following if statement only executes if the edge being hovered over is not selected + if (edgeIsSelected == -1) { + edge.setData('showDesc', true, 'current') + } + + edge.setDataset('end', { + lineWidth: 4 + }) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['edge-property:lineWidth'], + duration: 100 + }) + Metamaps.Visualize.mGraph.plot() + }, // onMouseEnter + onMouseLeave: function (edge) { + if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered + $('canvas').css('cursor', 'default') + var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + // following if statement only executes if the edge being hovered over is not selected + if (edgeIsSelected == -1) { + edge.setData('showDesc', false, 'current') + } + + edge.setDataset('end', { + lineWidth: 2 + }) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['edge-property:lineWidth'], + duration: 100 + }) + Metamaps.Visualize.mGraph.plot() + }, // onMouseLeave + onMouseMoveHandler: function (node, eventInfo, e) { + var self = Metamaps.JIT + + if (Metamaps.Visualize.mGraph.busy) return + + var node = eventInfo.getNode() + var edge = eventInfo.getEdge() + + // if we're on top of a node object, act like there aren't edges under it + if (node != false) { + if (Metamaps.Mouse.edgeHoveringOver) { + self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + } + $('canvas').css('cursor', 'pointer') + return + } + + if (edge == false && Metamaps.Mouse.edgeHoveringOver != false) { + // mouse not on an edge, but we were on an edge previously + self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + } else if (edge != false && Metamaps.Mouse.edgeHoveringOver == false) { + // mouse is on an edge, but there isn't a stored edge + self.onMouseEnter(edge) + } else if (edge != false && Metamaps.Mouse.edgeHoveringOver != edge) { + // mouse is on an edge, but a different edge is stored + self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + self.onMouseEnter(edge) + } + + // could be false + Metamaps.Mouse.edgeHoveringOver = edge + + if (!node && !edge) { + $('canvas').css('cursor', 'default') + } + }, // onMouseMoveHandler + enterKeyHandler: function () { + var creatingMap = Metamaps.GlobalUI.lightbox + if (creatingMap === 'newmap' || creatingMap === 'forkmap') { + Metamaps.GlobalUI.CreateMap.submit() + } + // this is to submit new topic creation + else if (Metamaps.Create.newTopic.beingCreated) { + Metamaps.Topic.createTopicLocally() + } + // to submit new synapse creation + else if (Metamaps.Create.newSynapse.beingCreated) { + Metamaps.Synapse.createSynapseLocally() + } + }, // enterKeyHandler + escKeyHandler: function () { + Metamaps.Control.deselectAllEdges() + Metamaps.Control.deselectAllNodes() + }, // escKeyHandler + touchPanZoomHandler: function (eventInfo, e) { + if (e.touches.length == 1) { + var thispos = Metamaps.Touch.touchPos, + currentPos = eventInfo.getPos(), + canvas = Metamaps.Visualize.mGraph.canvas, + ox = canvas.translateOffsetX, + oy = canvas.translateOffsetY, + sx = canvas.scaleOffsetX, + sy = canvas.scaleOffsetY + currentPos.x *= sx + currentPos.y *= sy + currentPos.x += ox + currentPos.y += oy + // var x = currentPos.x - thispos.x, + // y = currentPos.y - thispos.y + var x = currentPos.x - thispos.x, + y = currentPos.y - thispos.y + Metamaps.Touch.touchPos = currentPos + Metamaps.Visualize.mGraph.canvas.translate(x * 1 / sx, y * 1 / sy) + } else if (e.touches.length == 2) { + var touch1 = e.touches[0] + var touch2 = e.touches[1] + + var dist = Metamaps.Util.getDistance({ + x: touch1.clientX, + y: touch1.clientY + }, { + x: touch2.clientX, + y: touch2.clientY + }) + + if (!lastDist) { + lastDist = dist + } + + var scale = dist / lastDist + + if (8 >= Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale && Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale >= 1) { + Metamaps.Visualize.mGraph.canvas.scale(scale, scale) + } + if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX < 0.5) { + Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(true) + } else if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX > 0.5) { + Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(false) + } + lastDist = dist + } + }, // touchPanZoomHandler + onDragMoveTopicHandler: function (node, eventInfo, e) { + var self = Metamaps.JIT + + // this is used to send nodes that are moving to + // other realtime collaborators on the same map + var positionsToSend = {} + var topic + + var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (node && !node.nodeFrom) { + var pos = eventInfo.getPos() + // if it's a left click, or a touch, move the node + if (e.touches || (e.button == 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons == 0 || e.buttons == 1 || e.buttons == undefined))) { + // if the node dragged isn't already selected, select it + var whatToDo = self.handleSelectionBeforeDragging(node, e) + if (node.pos.rho || node.pos.rho === 0) { + // this means we're in topic view + var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) + var theta = Math.atan2(pos.y, pos.x) + node.pos.setp(theta, rho) + } else if (whatToDo == 'only-drag-this-one') { + node.pos.setc(pos.x, pos.y) + + if (Metamaps.Active.Map) { + topic = node.getData('topic') + // we use the topic ID not the node id + // because we can't depend on the node id + // to be the same as on other collaborators + // maps + positionsToSend[topic.id] = pos + $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + } + } else { + var len = Metamaps.Selected.Nodes.length + + // first define offset for each node + var xOffset = [] + var yOffset = [] + for (var i = 0; i < len; i += 1) { + var n = Metamaps.Selected.Nodes[i] + xOffset[i] = n.pos.x - node.pos.x + yOffset[i] = n.pos.y - node.pos.y + } // for + + for (var i = 0; i < len; i += 1) { + var n = Metamaps.Selected.Nodes[i] + var x = pos.x + xOffset[i] + var y = pos.y + yOffset[i] + n.pos.setc(x, y) + + if (Metamaps.Active.Map) { + topic = n.getData('topic') + // we use the topic ID not the node id + // because we can't depend on the node id + // to be the same as on other collaborators + // maps + positionsToSend[topic.id] = n.pos + } + } // for + + if (Metamaps.Active.Map) { + $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + } + } // if + + if (whatToDo == 'deselect') { + Metamaps.Control.deselectNode(node) + } + Metamaps.Visualize.mGraph.plot() + } + // if it's a right click or holding down alt, start synapse creation ->third option is for firefox + else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { + if (Metamaps.tempInit == false) { + Metamaps.tempNode = node + Metamaps.tempInit = true + + Metamaps.Create.newTopic.hide() + Metamaps.Create.newSynapse.hide() + // set the draw synapse start positions + var l = Metamaps.Selected.Nodes.length + if (l > 0) { + for (var i = l - 1; i >= 0; i -= 1) { + var n = Metamaps.Selected.Nodes[i] + Metamaps.Mouse.synapseStartCoordinates.push({ + x: n.pos.getc().x, + y: n.pos.getc().y + }) + } + } else { + Metamaps.Mouse.synapseStartCoordinates = [{ + x: Metamaps.tempNode.pos.getc().x, + y: Metamaps.tempNode.pos.getc().y + }] + } + Metamaps.Mouse.synapseEndCoordinates = { + x: pos.x, + y: pos.y + } + } + // + temp = eventInfo.getNode() + if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned + Metamaps.tempNode2 = temp + + Metamaps.Mouse.synapseEndCoordinates = { + x: Metamaps.tempNode2.pos.getc().x, + y: Metamaps.tempNode2.pos.getc().y + } + + // before making the highlighted one bigger, make sure all the others are regular size + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + n.setData('dim', 25, 'current') + }) + temp.setData('dim', 35, 'current') + Metamaps.Visualize.mGraph.plot() + } else if (!temp) { + Metamaps.tempNode2 = null + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + n.setData('dim', 25, 'current') + }) + // pop up node creation :) + var myX = e.clientX - 110 + var myY = e.clientY - 30 + $('#new_topic').css('left', myX + 'px') + $('#new_topic').css('top', myY + 'px') + Metamaps.Create.newTopic.x = eventInfo.getPos().x + Metamaps.Create.newTopic.y = eventInfo.getPos().y + Metamaps.Visualize.mGraph.plot() + + Metamaps.Mouse.synapseEndCoordinates = { + x: pos.x, + y: pos.y + } + } + } + else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Metamaps.Active.Topic) { + Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + } + else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + } + } + }, // onDragMoveTopicHandler + onDragCancelHandler: function (node, eventInfo, e) { + Metamaps.tempNode = null + if (Metamaps.tempNode2) Metamaps.tempNode2.setData('dim', 25, 'current') + Metamaps.tempNode2 = null + Metamaps.tempInit = false + // reset the draw synapse positions to false + Metamaps.Mouse.synapseStartCoordinates = [] + Metamaps.Mouse.synapseEndCoordinates = null + Metamaps.Visualize.mGraph.plot() + }, // onDragCancelHandler + onDragEndTopicHandler: function (node, eventInfo, e) { + var midpoint = {}, pixelPos, mapping + + if (Metamaps.tempInit && Metamaps.tempNode2 == null) { + // this means you want to add a new topic, and then a synapse + Metamaps.Create.newTopic.addSynapse = true + Metamaps.Create.newTopic.open() + } else if (Metamaps.tempInit && Metamaps.tempNode2 != null) { + // this means you want to create a synapse between two existing topics + Metamaps.Create.newTopic.addSynapse = false + Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id + Metamaps.Create.newSynapse.topic2id = Metamaps.tempNode2.getData('topic').id + Metamaps.tempNode2.setData('dim', 25, 'current') + Metamaps.Visualize.mGraph.plot() + midpoint.x = Metamaps.tempNode.pos.getc().x + (Metamaps.tempNode2.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 + midpoint.y = Metamaps.tempNode.pos.getc().y + (Metamaps.tempNode2.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + pixelPos = Metamaps.Util.coordsToPixels(midpoint) + $('#new_synapse').css('left', pixelPos.x + 'px') + $('#new_synapse').css('top', pixelPos.y + 'px') + Metamaps.Create.newSynapse.open() + Metamaps.tempNode = null + Metamaps.tempNode2 = null + Metamaps.tempInit = false + } else if (!Metamaps.tempInit && node && !node.nodeFrom) { + // this means you dragged an existing node, autosave that to the database + + // check whether to save mappings + var checkWhetherToSave = function () { + var map = Metamaps.Active.Map + + if (!map) return false + + var mapper = Metamaps.Active.Mapper + // this case + // covers when it is a public map owned by you + // and also when it's a private map + var activeMappersMap = map.authorizePermissionChange(mapper) + var commonsMap = map.get('permission') === 'commons' + var realtimeOn = Metamaps.Realtime.status + + // don't save if commons map, and you have realtime off, + // even if you're map creator + return map && mapper && ((commonsMap && realtimeOn) || (activeMappersMap && !commonsMap)) + } + + if (checkWhetherToSave()) { + mapping = node.getData('mapping') + mapping.save({ + xloc: node.getPos().x, + yloc: node.getPos().y + }) + // also save any other selected nodes that also got dragged along + var l = Metamaps.Selected.Nodes.length + for (var i = l - 1; i >= 0; i -= 1) { + var n = Metamaps.Selected.Nodes[i] + if (n !== node) { + mapping = n.getData('mapping') + mapping.save({ + xloc: n.getPos().x, + yloc: n.getPos().y + }) + } + } + } + } + }, // onDragEndTopicHandler + canvasClickHandler: function (canvasLoc, e) { + // grab the location and timestamp of the click + var storedTime = Metamaps.Mouse.lastCanvasClick + var now = Date.now() // not compatible with IE8 FYI + Metamaps.Mouse.lastCanvasClick = now + + var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + + if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE && !Metamaps.Mouse.didPan) { + if (Metamaps.Active.Map && !authorized) { + Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + return + } + else if (Metamaps.Active.Topic) { + Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + return + } + // DOUBLE CLICK + // pop up node creation :) + Metamaps.Create.newTopic.addSynapse = false + Metamaps.Create.newTopic.x = canvasLoc.x + Metamaps.Create.newTopic.y = canvasLoc.y + $('#new_topic').css('left', e.clientX + 'px') + $('#new_topic').css('top', e.clientY + 'px') + Metamaps.Create.newTopic.open() + } else if (!Metamaps.Mouse.didPan) { + // SINGLE CLICK, no pan + Metamaps.Filter.close() + Metamaps.TopicCard.hideCard() + Metamaps.SynapseCard.hideCard() + Metamaps.Create.newTopic.hide() + $('.rightclickmenu').remove() + // reset the draw synapse positions to false + Metamaps.Mouse.synapseStartCoordinates = [] + Metamaps.Mouse.synapseEndCoordinates = null + Metamaps.tempInit = false + Metamaps.tempNode = null + Metamaps.tempNode2 = null + if (!e.ctrlKey && !e.shiftKey) { + Metamaps.Control.deselectAllEdges() + Metamaps.Control.deselectAllNodes() + } + } + }, // canvasClickHandler + nodeDoubleClickHandler: function (node, e) { + Metamaps.TopicCard.showCard(node) + }, // nodeDoubleClickHandler + edgeDoubleClickHandler: function (adj, e) { + Metamaps.SynapseCard.showCard(adj, e) + }, // nodeDoubleClickHandler + nodeWasDoubleClicked: function () { + // grab the timestamp of the click + var storedTime = Metamaps.Mouse.lastNodeClick + var now = Date.now() // not compatible with IE8 FYI + Metamaps.Mouse.lastNodeClick = now + + if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) { + return true + } else { + return false + } + }, // nodeWasDoubleClicked + handleSelectionBeforeDragging: function (node, e) { + // four cases: + // 1 nothing is selected, so pretend you aren't selecting + // 2 others are selected only and shift, so additionally select this one + // 3 others are selected only, no shift: drag only this one + // 4 this node and others were selected, so drag them (just return false) + // return value: deselect node again after? + if (Metamaps.Selected.Nodes.length == 0) { + return 'only-drag-this-one' + } + if (Metamaps.Selected.Nodes.indexOf(node) == -1) { + if (e.shiftKey) { + Metamaps.Control.selectNode(node, e) + return 'nothing' + } else { + return 'only-drag-this-one' + } + } + return 'nothing'; // case 4? + }, // handleSelectionBeforeDragging + selectWithBox: function (e) { + var sX = Metamaps.Mouse.boxStartCoordinates.x, + sY = Metamaps.Mouse.boxStartCoordinates.y, + eX = Metamaps.Mouse.boxEndCoordinates.x, + eY = Metamaps.Mouse.boxEndCoordinates.y + + if (!e.shiftKey) { + Metamaps.Control.deselectAllNodes() + Metamaps.Control.deselectAllEdges() + } + + // select all nodes that are within the box + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + var x = n.pos.x, + y = n.pos.y + + if ((sX < x && x < eX && sY < y && y < eY) || (sX > x && x > eX && sY > y && y > eY) || (sX > x && x > eX && sY < y && y < eY) || (sX < x && x < eX && sY > y && y > eY)) { + if (e.shiftKey) { + if (n.selected) { + Metamaps.Control.deselectNode(n) + } else { + Metamaps.Control.selectNode(n, e) + } + } else { + Metamaps.Control.selectNode(n, e) + } + } + }) + + // Convert selection box coordinates to traditional coordinates (+,+) in upper right + sY = -1 * sY + eY = -1 * eY + + var edgesToToggle = [] + Metamaps.Synapses.each(function (synapse) { + var e = synapse.get('edge') + if (edgesToToggle.indexOf(e) === -1) { + edgesToToggle.push(e) + } + }) + edgesToToggle.forEach(function (edge) { + var fromNodeX = edge.nodeFrom.pos.x + var fromNodeY = -1 * edge.nodeFrom.pos.y + var toNodeX = edge.nodeTo.pos.x + var toNodeY = -1 * edge.nodeTo.pos.y + + var maxX = fromNodeX + var maxY = fromNodeY + var minX = fromNodeX + var minY = fromNodeY + + // Correct maxX, MaxY values + ;(toNodeX > maxX) ? (maxX = toNodeX) : (minX = toNodeX) + ;(toNodeY > maxY) ? (maxY = toNodeY) : (minY = toNodeY) + + var maxBoxX = sX + var maxBoxY = sY + var minBoxX = sX + var minBoxY = sY + + // Correct maxBoxX, maxBoxY values + ;(eX > maxBoxX) ? (maxBoxX = eX) : (minBoxX = eX) + ;(eY > maxBoxY) ? (maxBoxY = eY) : (minBoxY = eY) + + // Find the slopes from the synapse fromNode to the 4 corners of the selection box + var slopes = [] + slopes.push((sY - fromNodeY) / (sX - fromNodeX)) + slopes.push((sY - fromNodeY) / (eX - fromNodeX)) + slopes.push((eY - fromNodeY) / (eX - fromNodeX)) + slopes.push((eY - fromNodeY) / (sX - fromNodeX)) + + var minSlope = slopes[0] + var maxSlope = slopes[0] + slopes.forEach(function (entry) { + if (entry > maxSlope) maxSlope = entry + if (entry < minSlope) minSlope = entry + }) + + // Find synapse-in-question's slope + var synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX) + var b = fromNodeY - synSlope * fromNodeX + + // Use the selection box edges as test cases for synapse intersection + var testX = sX + var testY = synSlope * testX + b + + var selectTest + + if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY) { + selectTest = true + } + + testX = eX + testY = synSlope * testX + b + + if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY) { + selectTest = true + } + + testY = sY + testX = (testY - b) / synSlope + + if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX) { + selectTest = true + } + + testY = eY + testX = (testY - b) / synSlope + + if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX) { + selectTest = true + } + + // Case where the synapse is wholly enclosed in the seldction box + if (fromNodeX >= minBoxX && fromNodeX <= maxBoxX && fromNodeY >= minBoxY && fromNodeY <= maxBoxY && toNodeX >= minBoxX && toNodeX <= maxBoxX && toNodeY >= minBoxY && toNodeY <= maxBoxY) { + selectTest = true + } + + // The test synapse was selected! + + if (selectTest) { + // shiftKey = toggleSelect, otherwise + if (e.shiftKey) { + if (Metamaps.Selected.Edges.indexOf(edge) != -1) { + Metamaps.Control.deselectEdge(edge) + } else { + Metamaps.Control.selectEdge(edge) + } + } else { + Metamaps.Control.selectEdge(edge) + } + } + }) + Metamaps.Mouse.boxStartCoordinates = false + Metamaps.Mouse.boxEndCoordinates = false + Metamaps.Visualize.mGraph.plot() + }, // selectWithBox + drawSelectBox: function (eventInfo, e) { + var ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + + var startX = Metamaps.Mouse.boxStartCoordinates.x, + startY = Metamaps.Mouse.boxStartCoordinates.y, + currX = eventInfo.getPos().x, + currY = eventInfo.getPos().y + + Metamaps.Visualize.mGraph.canvas.clear() + Metamaps.Visualize.mGraph.plot() + + ctx.beginPath() + ctx.moveTo(startX, startY) + ctx.lineTo(startX, currY) + ctx.lineTo(currX, currY) + ctx.lineTo(currX, startY) + ctx.lineTo(startX, startY) + ctx.strokeStyle = 'black' + ctx.stroke() + }, // drawSelectBox + selectNodeOnClickHandler: function (node, e) { + if (Metamaps.Visualize.mGraph.busy) return + + var self = Metamaps.JIT + + // catch right click on mac, which is often like ctrl+click + if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + self.selectNodeOnRightClickHandler(node, e) + return + } + + // if on a topic page, let alt+click center you on a new topic + if (Metamaps.Active.Topic && e.altKey) { + Metamaps.RGraph.centerOn(node.id) + return + } + + var check = self.nodeWasDoubleClicked() + if (check) { + self.nodeDoubleClickHandler(node, e) + return + } else { + // wait a certain length of time, then check again, then run this code + setTimeout(function () { + if (!Metamaps.JIT.nodeWasDoubleClicked()) { + var nodeAlreadySelected = node.selected + + if (!e.shiftKey) { + Metamaps.Control.deselectAllNodes() + Metamaps.Control.deselectAllEdges() + } + + if (nodeAlreadySelected) { + Metamaps.Control.deselectNode(node) + } else { + Metamaps.Control.selectNode(node, e) + } + + // trigger animation to final styles + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['edge-property:lineWidth:color:alpha'], + duration: 500 + }) + Metamaps.Visualize.mGraph.plot() + } + }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + } + }, // selectNodeOnClickHandler + selectNodeOnRightClickHandler: function (node, e) { + // the 'node' variable is a JIT node, the one that was clicked on + // the 'e' variable is the click event + + e.preventDefault() + e.stopPropagation() + + if (Metamaps.Visualize.mGraph.busy) return + + // select the node + Metamaps.Control.selectNode(node, e) + + // delete old right click menu + $('.rightclickmenu').remove() + // create new menu for clicked on node + var rightclickmenu = document.createElement('div') + rightclickmenu.className = 'rightclickmenu' + // add the proper options to the menu + var menustring = '' + rightclickmenu.innerHTML = menustring + + // position the menu where the click happened + var position = {} + var RIGHTCLICK_WIDTH = 300 + var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static + var SUBMENUS_WIDTH = 256 + var MAX_SUBMENU_HEIGHT = 270 + var windowWidth = $(window).width() + var windowHeight = $(window).height() + + if (windowWidth - e.clientX < SUBMENUS_WIDTH) { + position.right = windowWidth - e.clientX + $(rightclickmenu).addClass('moveMenusToLeft') + } + else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + position.right = windowWidth - e.clientX + } + else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { + position.left = e.clientX + $(rightclickmenu).addClass('moveMenusToLeft') + } + else position.left = e.clientX + + if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { + position.bottom = windowHeight - e.clientY + $(rightclickmenu).addClass('moveMenusUp') + } + else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + position.top = e.clientY + $(rightclickmenu).addClass('moveMenusUp') + } + else position.top = e.clientY + + $(rightclickmenu).css(position) + // add the menu to the page + $('#wrapper').append(rightclickmenu) + + // attach events to clicks on the list items + + // delete the selected things from the database + if (authorized) { + $('.rc-delete').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.deleteSelected() + }) + } + + // remove the selected things from the map + if (authorized) { + $('.rc-remove').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.removeSelectedEdges() + Metamaps.Control.removeSelectedNodes() + }) + } + + // hide selected nodes and synapses until refresh + $('.rc-hide').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.hideSelectedEdges() + Metamaps.Control.hideSelectedNodes() + }) + + // when in radial, center on the topic you picked + $('.rc-center').click(function () { + $('.rightclickmenu').remove() + Metamaps.Topic.centerOn(node.id) + }) + + // open the entity in a new tab + $('.rc-popout').click(function () { + $('.rightclickmenu').remove() + var win = window.open('/topics/' + node.id, '_blank') + win.focus() + }) + + // change the permission of all the selected nodes and synapses that you were the originator of + $('.rc-permission li').click(function () { + $('.rightclickmenu').remove() + // $(this).text() will be 'commons' 'public' or 'private' + Metamaps.Control.updateSelectedPermissions($(this).text()) + }) + + // change the metacode of all the selected nodes that you have edit permission for + $('.rc-metacode li li').click(function () { + $('.rightclickmenu').remove() + // + Metamaps.Control.updateSelectedMetacodes($(this).attr('data-id')) + }) + + // fetch relatives + var fetched = false + $('.rc-siblings').hover(function () { + if (!fetched) { + Metamaps.JIT.populateRightClickSiblings(node) + fetched = true + } + }) + $('.rc-siblings .fetchAll').click(function () { + $('.rightclickmenu').remove() + // data-id is a metacode id + Metamaps.Topic.fetchRelatives(node) + }) + }, // selectNodeOnRightClickHandler, + populateRightClickSiblings: function (node) { + var self = Metamaps.JIT + + // depending on how many topics are selected, do different things + /*if (Metamaps.Selected.Nodes.length > 1) { + // we don't bother filling the submenu with + // specific numbers, because there are too many topics + // selected to find those numbers + $('#loadingSiblings').remove() + return + }*/ + + var topic = node.getData('topic') + + // add a loading icon for now + var loader = new CanvasLoader('loadingSiblings') + loader.setColor('#4FC059'); // default is '#000000' + loader.setDiameter(15) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + + var topics = Metamaps.Topics.map(function (t) { return t.id }) + var topics_string = topics.join() + + var successCallback = function (data) { + $('#loadingSiblings').remove() + + for (var key in data) { + var string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' + $('#fetchSiblingList').append('
  • ' + string + '
  • ') + } + + $('.rc-siblings .getSiblings').click(function () { + $('.rightclickmenu').remove() + // data-id is a metacode id + Metamaps.Topic.fetchRelatives(node, $(this).attr('data-id')) + }) + } + + $.ajax({ + type: 'Get', + url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topics_string, + success: successCallback, + error: function () {} + }) + }, + selectEdgeOnClickHandler: function (adj, e) { + if (Metamaps.Visualize.mGraph.busy) return + + var self = Metamaps.JIT + + // catch right click on mac, which is often like ctrl+click + if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + self.selectEdgeOnRightClickHandler(adj, e) + return + } + + var check = self.nodeWasDoubleClicked() + if (check) { + self.edgeDoubleClickHandler(adj, e) + return + } else { + // wait a certain length of time, then check again, then run this code + setTimeout(function () { + if (!Metamaps.JIT.nodeWasDoubleClicked()) { + var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1 + + if (!e.shiftKey) { + Metamaps.Control.deselectAllNodes() + Metamaps.Control.deselectAllEdges() + } + + if (edgeAlreadySelected) { + Metamaps.Control.deselectEdge(adj) + } else { + Metamaps.Control.selectEdge(adj) + } + + Metamaps.Visualize.mGraph.plot() + } + }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + } + }, // selectEdgeOnClickHandler + selectEdgeOnRightClickHandler: function (adj, e) { + // the 'node' variable is a JIT node, the one that was clicked on + // the 'e' variable is the click event + + if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered + + var authorized + + e.preventDefault() + e.stopPropagation() + + if (Metamaps.Visualize.mGraph.busy) return + + Metamaps.Control.selectEdge(adj) + + // delete old right click menu + $('.rightclickmenu').remove() + // create new menu for clicked on node + var rightclickmenu = document.createElement('div') + rightclickmenu.className = 'rightclickmenu' + + // add the proper options to the menu + var menustring = '' + rightclickmenu.innerHTML = menustring + + // position the menu where the click happened + var position = {} + var RIGHTCLICK_WIDTH = 300 + var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static + var SUBMENUS_WIDTH = 256 + var MAX_SUBMENU_HEIGHT = 270 + var windowWidth = $(window).width() + var windowHeight = $(window).height() + + if (windowWidth - e.clientX < SUBMENUS_WIDTH) { + position.right = windowWidth - e.clientX + $(rightclickmenu).addClass('moveMenusToLeft') + } + else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + position.right = windowWidth - e.clientX + } + else position.left = e.clientX + + if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { + position.bottom = windowHeight - e.clientY + $(rightclickmenu).addClass('moveMenusUp') + } + else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + position.top = e.clientY + $(rightclickmenu).addClass('moveMenusUp') + } + else position.top = e.clientY + + $(rightclickmenu).css(position) + + // add the menu to the page + $('#wrapper').append(rightclickmenu) + + // attach events to clicks on the list items + + // delete the selected things from the database + if (authorized) { + $('.rc-delete').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.deleteSelected() + }) + } + + // remove the selected things from the map + if (authorized) { + $('.rc-remove').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.removeSelectedEdges() + Metamaps.Control.removeSelectedNodes() + }) + } + + // hide selected nodes and synapses until refresh + $('.rc-hide').click(function () { + $('.rightclickmenu').remove() + Metamaps.Control.hideSelectedEdges() + Metamaps.Control.hideSelectedNodes() + }) + + // change the permission of all the selected nodes and synapses that you were the originator of + $('.rc-permission li').click(function () { + $('.rightclickmenu').remove() + // $(this).text() will be 'commons' 'public' or 'private' + Metamaps.Control.updateSelectedPermissions($(this).text()) + }) + }, // selectEdgeOnRightClickHandler + SmoothPanning: function () { + var sx = Metamaps.Visualize.mGraph.canvas.scaleOffsetX, + sy = Metamaps.Visualize.mGraph.canvas.scaleOffsetY, + y_velocity = Metamaps.Mouse.changeInY, // initial y velocity + x_velocity = Metamaps.Mouse.changeInX, // initial x velocity + easing = 1 // frictional value + + easing = 1 + window.clearInterval(Metamaps.panningInt) + Metamaps.panningInt = setInterval(function () { + myTimer() + }, 1) + + function myTimer () { + Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) + $(document).trigger(Metamaps.JIT.events.pan) + easing = easing * 0.75 + + if (easing < 0.1) window.clearInterval(Metamaps.panningInt) + } + }, // SmoothPanning + renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { + var ctx = canvas.getCtx() + // invert edge direction + if (swap) { + var tmp = from + from = to + to = tmp + } + // vect represents a line from tip to tail of the arrow + var vect = new $jit.Complex(to.x - from.x, to.y - from.y) + // scale it + vect.$scale(dim / vect.norm()) + // compute the midpoint of the edge line + var newX = (to.x - from.x) * placement + from.x + var newY = (to.y - from.y) * placement + from.y + var midPoint = new $jit.Complex(newX, newY) + + // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint + var arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y) + // compute the tail intersection point with the edge line + var intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y) + // vector perpendicular to vect + var normal = new $jit.Complex(-vect.y / 2, vect.x / 2) + var v1 = intermediatePoint.add(normal) + var v2 = intermediatePoint.$add(normal.$scale(-1)) + + if (newSynapse) { + ctx.strokeStyle = '#4fc059' + ctx.lineWidth = 2 + ctx.globalAlpha = 1 + } + ctx.beginPath() + ctx.moveTo(from.x, from.y) + ctx.lineTo(to.x, to.y) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(v1.x, v1.y) + ctx.lineTo(arrowPoint.x, arrowPoint.y) + ctx.lineTo(v2.x, v2.y) + ctx.stroke() + }, // renderMidArrow + renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { + var self = Metamaps.JIT + + var directionCat = synapse.get('category') + var direction = synapse.getDirection() + + var pos = adj.nodeFrom.pos.getc(true) + var posChild = adj.nodeTo.pos.getc(true) + + // plot arrow edge + if (!direction) { + // render nothing for this arrow if the direction couldn't be retrieved + } else if (directionCat == 'none') { + edgeHelper.line.render({ + x: pos.x, + y: pos.y + }, { + x: posChild.x, + y: posChild.y + }, canvas) + } else if (directionCat == 'both') { + self.renderMidArrow({ + x: pos.x, + y: pos.y + }, { + x: posChild.x, + y: posChild.y + }, 13, true, canvas, 0.7) + self.renderMidArrow({ + x: pos.x, + y: pos.y + }, { + x: posChild.x, + y: posChild.y + }, 13, false, canvas, 0.7) + } else if (directionCat == 'from-to') { + var inv = (direction[0] != adj.nodeFrom.id) + self.renderMidArrow({ + x: pos.x, + y: pos.y + }, { + x: posChild.x, + y: posChild.y + }, 13, inv, canvas, 0.7) + self.renderMidArrow({ + x: pos.x, + y: pos.y + }, { + x: posChild.x, + y: posChild.y + }, 13, inv, canvas, 0.3) + } + }, // renderEdgeArrows + zoomIn: function (event) { + Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25) + $(document).trigger(Metamaps.JIT.events.zoom, [event]) + }, + zoomOut: function (event) { + Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8) + $(document).trigger(Metamaps.JIT.events.zoom, [event]) + }, + centerMap: function (canvas) { + var offsetScale = canvas.scaleOffsetX + + canvas.scale(1 / offsetScale, 1 / offsetScale) + + var offsetX = canvas.translateOffsetX + var offsetY = canvas.translateOffsetY + + canvas.translate(-1 * offsetX, -1 * offsetY) + }, + zoomToBox: function (event) { + var sX = Metamaps.Mouse.boxStartCoordinates.x, + sY = Metamaps.Mouse.boxStartCoordinates.y, + eX = Metamaps.Mouse.boxEndCoordinates.x, + eY = Metamaps.Mouse.boxEndCoordinates.y + + var canvas = Metamaps.Visualize.mGraph.canvas + Metamaps.JIT.centerMap(canvas) + + var height = $(document).height(), + width = $(document).width() + + var spanX = Math.abs(sX - eX) + var spanY = Math.abs(sY - eY) + var ratioX = width / spanX + var ratioY = height / spanY + + var newRatio = Math.min(ratioX, ratioY) + + if (canvas.scaleOffsetX * newRatio <= 5 && canvas.scaleOffsetX * newRatio >= 0.2) { + canvas.scale(newRatio, newRatio) + } + else if (canvas.scaleOffsetX * newRatio > 5) { + newRatio = 5 / canvas.scaleOffsetX + canvas.scale(newRatio, newRatio) + } else { + newRatio = 0.2 / canvas.scaleOffsetX + canvas.scale(newRatio, newRatio) + } + + var cogX = (sX + eX) / 2 + var cogY = (sY + eY) / 2 + + canvas.translate(-1 * cogX, -1 * cogY) + $(document).trigger(Metamaps.JIT.events.zoom, [event]) + + Metamaps.Mouse.boxStartCoordinates = false + Metamaps.Mouse.boxEndCoordinates = false + Metamaps.Visualize.mGraph.plot() + }, + zoomExtents: function (event, canvas, denySelected) { + Metamaps.JIT.centerMap(canvas) + var height = canvas.getSize().height, + width = canvas.getSize().width, + maxX, minX, maxY, minY, counter = 0 + + if (!denySelected && Metamaps.Selected.Nodes.length > 0) { + var nodes = Metamaps.Selected.Nodes + } else { + var nodes = _.values(Metamaps.Visualize.mGraph.graph.nodes) + } + + if (nodes.length > 1) { + nodes.forEach(function (n) { + var x = n.pos.x, + y = n.pos.y + + if (counter == 0 && n.getData('alpha') == 1) { + maxX = x + minX = x + maxY = y + minY = y + } + + var arrayOfLabelLines = Metamaps.Util.splitLine(n.name, 30).split('\n'), + dim = n.getData('dim'), + ctx = canvas.getCtx() + + var height = 25 * arrayOfLabelLines.length + + var index, lineWidths = [] + for (index = 0; index < arrayOfLabelLines.length; ++index) { + lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) + } + var width = Math.max.apply(null, lineWidths) + 8 + + // only adjust these values if the node is not filtered + if (n.getData('alpha') == 1) { + maxX = Math.max(x + width / 2, maxX) + maxY = Math.max(y + n.getData('height') + 5 + height, maxY) + minX = Math.min(x - width / 2, minX) + minY = Math.min(y - dim, minY) + + counter++ + } + }) + + var spanX = maxX - minX + var spanY = maxY - minY + var ratioX = spanX / width + var ratioY = spanY / height + + var cogX = (maxX + minX) / 2 + var cogY = (maxY + minY) / 2 + + canvas.translate(-1 * cogX, -1 * cogY) + + var newRatio = Math.max(ratioX, ratioY) + var scaleMultiplier = 1 / newRatio * 0.9 + + if (canvas.scaleOffsetX * scaleMultiplier <= 3 && canvas.scaleOffsetX * scaleMultiplier >= 0.2) { + canvas.scale(scaleMultiplier, scaleMultiplier) + } + else if (canvas.scaleOffsetX * scaleMultiplier > 3) { + scaleMultiplier = 3 / canvas.scaleOffsetX + canvas.scale(scaleMultiplier, scaleMultiplier) + } else { + scaleMultiplier = 0.2 / canvas.scaleOffsetX + canvas.scale(scaleMultiplier, scaleMultiplier) + } + + $(document).trigger(Metamaps.JIT.events.zoom, [event]) + } + else if (nodes.length == 1) { + nodes.forEach(function (n) { + var x = n.pos.x, + y = n.pos.y + + canvas.translate(-1 * x, -1 * y) + $(document).trigger(Metamaps.JIT.events.zoom, [event]) + }) + } + } +} diff --git a/app/assets/javascripts/src/Metamaps.JIT.js.erb b/app/assets/javascripts/src/Metamaps.JIT.js.erb deleted file mode 100644 index 744a78c3..00000000 --- a/app/assets/javascripts/src/Metamaps.JIT.js.erb +++ /dev/null @@ -1,1949 +0,0 @@ -Metamaps.JIT = { - events: { - topicDrag: 'Metamaps:JIT:events:topicDrag', - newTopic: 'Metamaps:JIT:events:newTopic', - deleteTopic: 'Metamaps:JIT:events:deleteTopic', - removeTopic: 'Metamaps:JIT:events:removeTopic', - newSynapse: 'Metamaps:JIT:events:newSynapse', - deleteSynapse: 'Metamaps:JIT:events:deleteSynapse', - removeSynapse: 'Metamaps:JIT:events:removeSynapse', - pan: 'Metamaps:JIT:events:pan', - zoom: 'Metamaps:JIT:events:zoom', - animationDone: 'Metamaps:JIT:events:animationDone', - }, - vizData: [], // contains the visualization-compatible graph - /** - * This method will bind the event handlers it is interested and initialize the class. - */ - init: function () { - var self = Metamaps.JIT; - - $(".zoomIn").click(self.zoomIn); - $(".zoomOut").click(self.zoomOut); - - var zoomExtents = function (event) { - self.zoomExtents(event, Metamaps.Visualize.mGraph.canvas); - }; - $(".zoomExtents").click(zoomExtents); - - $(".takeScreenshot").click(Metamaps.Map.exportImage); - - self.topicDescImage = new Image(); - self.topicDescImage.src = '<%= asset_path('topic_description_signifier.png') %>'; - - self.topicLinkImage = new Image(); - self.topicLinkImage.src = '<%= asset_path('topic_link_signifier.png') %>'; - }, - /** - * convert our topic JSON into something JIT can use - */ - convertModelsToJIT: function(topics, synapses) { - var jitReady = []; - - var synapsesToRemove = []; - var topic; - var mapping; - var node; - var nodes = {}; - var existingEdge; - var edge; - var edges = []; - - topics.each(function (t) { - node = t.createNode(); - nodes[node.id] = node; - }); - synapses.each(function (s) { - edge = s.createEdge(); - - if (topics.get(s.get('node1_id')) === undefined || topics.get(s.get('node2_id')) === undefined) { - // this means it's an invalid synapse - synapsesToRemove.push(s); - } - else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { - - existingEdge = _.findWhere(edges, { - nodeFrom: edge.nodeFrom, - nodeTo: edge.nodeTo - }) || - _.findWhere(edges, { - nodeFrom: edge.nodeTo, - nodeTo: edge.nodeFrom - }); - - if (existingEdge) { - // for when you're dealing with multiple relationships between the same two topics - if (Metamaps.Active.Map) { - mapping = s.getMapping(); - existingEdge.data['$mappingIDs'].push(mapping.id); - } - existingEdge.data['$synapseIDs'].push(s.id); - } else { - // for when you're dealing with a topic that has relationships to many different nodes - nodes[edge.nodeFrom].adjacencies.push(edge); - edges.push(edge); - } - } - }); - - _.each(nodes, function (node) { - jitReady.push(node); - }); - - return [jitReady, synapsesToRemove]; - }, - prepareVizData: function () { - var self = Metamaps.JIT; - var mapping; - - // reset/empty vizData - self.vizData = []; - Metamaps.Visualize.loadLater = false; - - var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses); - - self.vizData = results[0]; - - // clean up the synapses array in case of any faulty data - _.each(results[1], function (synapse) { - mapping = synapse.getMapping(); - Metamaps.Synapses.remove(synapse); - if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping); - }); - - if (self.vizData.length == 0) { - Metamaps.Famous.viz.showInstructions(); - Metamaps.Visualize.loadLater = true; - } - else Metamaps.Famous.viz.hideInstructions(); - - Metamaps.Visualize.render(); - }, // prepareVizData - edgeRender: function (adj, canvas) { - //get nodes cartesian coordinates - var pos = adj.nodeFrom.pos.getc(true); - var posChild = adj.nodeTo.pos.getc(true); - - var synapse; - if(adj.getData("displayIndex")) { - synapse = adj.getData("synapses")[adj.getData("displayIndex")]; - if (!synapse) { - delete adj.data.$displayIndex; - synapse = adj.getData("synapses")[0]; - } - } - else { - synapse = adj.getData("synapses")[0]; - } - - if (!synapse) return; // this means there are no corresponding synapses for - // this edge, don't render it - - var directionCat = synapse.get("category"); - - //label placement on edges - if (canvas.denySelected) { - var color = Metamaps.Settings.colors.synapses.normal; - canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color; - } - Metamaps.JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas); - - //check for edge label in data - var desc = synapse.get("desc"); - - var showDesc = adj.getData("showDesc"); - - var drawSynapseCount = function (context, x, y, count) { - /* - circle size: 16x16px - positioning: overlay and center on top right corner of synapse label - 8px left and 8px down - color: #dab539 - border color: #424242 - border size: 1.5px - font: DIN medium - font-size: 14pt - font-color: #424242 - */ - context.beginPath(); - context.arc(x, y, 8, 0, 2 * Math.PI, false); - context.fillStyle = '#DAB539'; - context.strokeStyle = '#424242'; - context.lineWidth = 1.5; - context.closePath(); - context.fill(); - context.stroke(); - - // add the synapse count - context.fillStyle = '#424242'; - context.textAlign = 'center'; - context.font = '14px din-medium'; - - context.fillText(count, x, y + 5); - }; - - if (!canvas.denySelected && desc != "" && showDesc) { - // '&' to '&' - desc = Metamaps.Util.decodeEntities(desc); - - //now adjust the label placement - var ctx = canvas.getCtx(); - ctx.font = 'bold 14px arial'; - ctx.fillStyle = '#FFF'; - ctx.textBaseline = 'alphabetic'; - - var arrayOfLabelLines = Metamaps.Util.splitLine(desc, 30).split('\n'); - var index, lineWidths = []; - for (index = 0; index < arrayOfLabelLines.length; ++index) { - lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) - } - var width = Math.max.apply(null, lineWidths) + 16; - var height = (16 * arrayOfLabelLines.length) + 8; - - var x = (pos.x + posChild.x - width) / 2; - var y = ((pos.y + posChild.y) / 2) - height / 2; - - var radius = 5; - - //render background - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - ctx.fill(); - - // get number of synapses - var synapseNum = adj.getData("synapses").length; - - //render text - ctx.fillStyle = '#424242'; - ctx.textAlign = 'center'; - for (index = 0; index < arrayOfLabelLines.length; ++index) { - ctx.fillText(arrayOfLabelLines[index], x + (width / 2), y + 18 + (16 * index)); - } - - if (synapseNum > 1) { - drawSynapseCount(ctx, x + width, y, synapseNum); - } - } - else if (!canvas.denySelected && showDesc) { - // get number of synapses - var synapseNum = adj.getData("synapses").length; - - if (synapseNum > 1) { - var ctx = canvas.getCtx(); - var x = (pos.x + posChild.x) / 2; - var y = (pos.y + posChild.y) / 2; - drawSynapseCount(ctx, x, y, synapseNum); - } - } - - }, // edgeRender - ForceDirected: { - animateSavedLayout: { - modes: ['linear'], - transition: $jit.Trans.Quad.easeInOut, - duration: 800, - onComplete: function () { - Metamaps.Visualize.mGraph.busy = false; - $(document).trigger(Metamaps.JIT.events.animationDone); - } - }, - animateFDLayout: { - modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, - duration: 800, - onComplete: function () { - Metamaps.Visualize.mGraph.busy = false; - } - }, - graphSettings: { - //id of the visualization container - injectInto: 'infovis', - //Enable zooming and panning - //by scrolling and DnD - Navigation: { - enable: true, - //Enable panning events only if we're dragging the empty - //canvas (and not a node). - panning: 'avoid nodes', - zooming: 28 //zoom speed. higher is more sensible - }, - //background: { - // type: 'Metamaps' - //}, - //NodeStyles: { - // enable: true, - // type: 'Native', - // stylesHover: { - // dim: 30 - // }, - // duration: 300 - //}, - // Change node and edge styles such as - // color and width. - // These properties are also set per node - // with dollar prefixed data-properties in the - // JSON structure. - Node: { - overridable: true, - color: '#2D6A5D', - type: 'customNode', - dim: 25 - }, - Edge: { - overridable: true, - color: Metamaps.Settings.colors.synapses.normal, - type: 'customEdge', - lineWidth: 2, - alpha: 1 - }, - //Native canvas text styling - Label: { - type: 'Native', //Native or HTML - size: 20, - family: 'arial', - textBaseline: 'alphabetic', - color: Metamaps.Settings.colors.labels.text - }, - //Add Tips - Tips: { - enable: false, - onShow: function (tip, node) {} - }, - // Add node events - Events: { - enable: true, - enableForEdges: true, - onMouseMove: function (node, eventInfo, e) { - Metamaps.JIT.onMouseMoveHandler(node, eventInfo, e); - //console.log('called mouse move handler'); - }, - //Update node positions when dragged - onDragMove: function (node, eventInfo, e) { - Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e); - //console.log('called drag move handler'); - }, - onDragEnd: function (node, eventInfo, e) { - Metamaps.JIT.onDragEndTopicHandler(node, eventInfo, e, false); - //console.log('called drag end handler'); - }, - onDragCancel: function (node, eventInfo, e) { - Metamaps.JIT.onDragCancelHandler(node, eventInfo, e, false); - }, - //Implement the same handler for touchscreens - onTouchStart: function (node, eventInfo, e) { - //$jit.util.event.stop(e); //stop default touchmove event - //Metamaps.Visualize.mGraph.events.onMouseDown(e, null, eventInfo); - Metamaps.Visualize.mGraph.events.touched = true; - Metamaps.Touch.touchPos = eventInfo.getPos(); - var canvas = Metamaps.Visualize.mGraph.canvas, - ox = canvas.translateOffsetX; - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - Metamaps.Touch.touchPos.x *= sx; - Metamaps.Touch.touchPos.y *= sy; - Metamaps.Touch.touchPos.x += ox; - Metamaps.Touch.touchPos.y += oy; - - touchDragNode = node; - }, - //Implement the same handler for touchscreens - onTouchMove: function (node, eventInfo, e) { - if (Metamaps.Touch.touchDragNode) Metamaps.JIT.onDragMoveTopicHandler(Metamaps.Touch.touchDragNode, eventInfo, e); - else { - Metamaps.JIT.touchPanZoomHandler(eventInfo, e); - } - }, - //Implement the same handler for touchscreens - onTouchEnd: function (node, eventInfo, e) { - - }, - //Implement the same handler for touchscreens - onTouchCancel: function (node, eventInfo, e) { - - }, - //Add also a click handler to nodes - onClick: function (node, eventInfo, e) { - - // remove the rightclickmenu - $('.rightclickmenu').remove(); - - if (Metamaps.Mouse.boxStartCoordinates) { - if(e.ctrlKey){ - Metamaps.Visualize.mGraph.busy = false; - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); - - var bS = Metamaps.Mouse.boxStartCoordinates; - var bE = Metamaps.Mouse.boxEndCoordinates; - if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { - Metamaps.JIT.zoomToBox(e); - return; - } - else { - Metamaps.Mouse.boxStartCoordinates = null; - Metamaps.Mouse.boxEndCoordinates = null; - } - //console.log('called zoom to box'); - } - - if (e.shiftKey) { - Metamaps.Visualize.mGraph.busy = false; - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); - Metamaps.JIT.selectWithBox(e); - //console.log('called select with box'); - return; - }; - } - - if (e.target.id != "infovis-canvas") return false; - - //clicking on a edge, node, or clicking on blank part of canvas? - if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnClickHandler(node, e); - //console.log('called selectEdgeOnClickHandler'); - } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnClickHandler(node, e); - //console.log('called selectNodeOnClickHandler'); - } else { - Metamaps.JIT.canvasClickHandler(eventInfo.getPos(), e); - //console.log('called canvasClickHandler'); - } //if - }, - //Add also a click handler to nodes - onRightClick: function (node, eventInfo, e) { - - // remove the rightclickmenu - $('.rightclickmenu').remove(); - - if (Metamaps.Mouse.boxStartCoordinates) { - Metamaps.Visualize.mGraph.busy = false; - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); - Metamaps.JIT.selectWithBox(e); - return; - } - - if (e.target.id != "infovis-canvas") return false; - - //clicking on a edge, node, or clicking on blank part of canvas? - if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnRightClickHandler(node, e); - } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnRightClickHandler(node, e); - } else { - //console.log('right clicked on open space'); - } - } - }, - //Number of iterations for the FD algorithm - iterations: 200, - //Edge length - levelDistance: 200, - }, - nodeSettings: { - 'customNode': { - 'render': function (node, canvas) { - var pos = node.pos.getc(true), - dim = node.getData('dim'), - topic = node.getData('topic'), - metacode = topic ? topic.getMetacode() : false, - ctx = canvas.getCtx(); - - // if the topic is selected draw a circle around it - if (!canvas.denySelected && node.selected) { - ctx.beginPath(); - ctx.arc(pos.x, pos.y, dim + 3, 0, 2 * Math.PI, false); - ctx.strokeStyle = Metamaps.Settings.colors.topics.selected; - ctx.lineWidth = 2; - ctx.stroke(); - } - - if (!metacode || - !metacode.get('image') || - !metacode.get('image').complete || - (typeof metacode.get('image').naturalWidth !== "undefined" && - metacode.get('image').naturalWidth === 0)) { - ctx.beginPath(); - ctx.arc(pos.x, pos.y, dim, 0, 2 * Math.PI, false); - ctx.fillStyle = '#B6B2FD'; - ctx.fill(); - } else { - ctx.drawImage(metacode.get('image'), pos.x - dim, pos.y - dim, dim * 2, dim * 2); - } - - // if the topic has a link, draw a small image to indicate that - var hasLink = topic && topic.get('link') !== "" && topic.get('link') !== null; - var linkImage = Metamaps.JIT.topicLinkImage; - var linkImageLoaded = linkImage.complete || - (typeof linkImage.naturalWidth !== "undefined" && - linkImage.naturalWidth !== 0) - if (hasLink && linkImageLoaded) { - ctx.drawImage(linkImage, pos.x - dim - 8, pos.y - dim - 8, 16, 16); - } - - // if the topic has a desc, draw a small image to indicate that - var hasDesc = topic && topic.get('desc') !== "" && topic.get('desc') !== null; - var descImage = Metamaps.JIT.topicDescImage; - var descImageLoaded = descImage.complete || - (typeof descImage.naturalWidth !== "undefined" && - descImage.naturalWidth !== 0) - if (hasDesc && descImageLoaded) { - ctx.drawImage(descImage, pos.x + dim - 8, pos.y - dim - 8, 16, 16); - } - }, - 'contains': function (node, pos) { - var npos = node.pos.getc(true), - dim = node.getData('dim'), - arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 30).split('\n'), - ctx = Metamaps.Visualize.mGraph.canvas.getCtx(); - - var height = 25 * arrayOfLabelLines.length; - - var index, lineWidths = []; - for (index = 0; index < arrayOfLabelLines.length; ++index) { - lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) - } - var width = Math.max.apply(null, lineWidths) + 8; - var labely = npos.y + node.getData("height") + 5 + height / 2; - - var overLabel = this.nodeHelper.rectangle.contains({ - x: npos.x, - y: labely - }, pos, width, height); - - return this.nodeHelper.circle.contains(npos, pos, dim) || overLabel; - } - } - }, - edgeSettings: { - 'customEdge': { - 'render': function (adj, canvas) { - Metamaps.JIT.edgeRender(adj, canvas) - }, - 'contains': function (adj, pos) { - var from = adj.nodeFrom.pos.getc(), - to = adj.nodeTo.pos.getc(); - - // this fixes an issue where when edges are perfectly horizontal or perfectly vertical - // it becomes incredibly difficult to hover over them - if (-1 < pos.x && pos.x < 1) pos.x = 0; - if (-1 < pos.y && pos.y < 1) pos.y = 0; - - return $jit.Graph.Plot.edgeHelper.line.contains(from, to, pos, adj.Edge.epsilon + 5); - } - } - } - }, // ForceDirected - ForceDirected3D: { - animate: { - modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, - duration: 2500, - onComplete: function () { - Metamaps.Visualize.mGraph.busy = false; - } - }, - graphSettings: { - //id of the visualization container - injectInto: 'infovis', - type: '3D', - Scene: { - Lighting: { - enable: false, - ambient: [0.5, 0.5, 0.5], - directional: { - direction: { - x: 1, - y: 0, - z: -1 - }, - color: [0.9, 0.9, 0.9] - } - } - }, - //Enable zooming and panning - //by scrolling and DnD - Navigation: { - enable: false, - //Enable panning events only if we're dragging the empty - //canvas (and not a node). - panning: 'avoid nodes', - zooming: 10 //zoom speed. higher is more sensible - }, - // Change node and edge styles such as - // color and width. - // These properties are also set per node - // with dollar prefixed data-properties in the - // JSON structure. - Node: { - overridable: true, - type: 'sphere', - dim: 15, - color: '#ffffff' - }, - Edge: { - overridable: false, - type: 'tube', - color: '#111', - lineWidth: 3 - }, - //Native canvas text styling - Label: { - type: 'HTML', //Native or HTML - size: 10, - style: 'bold' - }, - // Add node events - Events: { - enable: true, - type: 'Native', - i: 0, - onMouseMove: function (node, eventInfo, e) { - //if(this.i++ % 3) return; - var pos = eventInfo.getPos(); - Metamaps.Visualize.cameraPosition.x += (pos.x - Metamaps.Visualize.cameraPosition.x) * 0.5; - Metamaps.Visualize.cameraPosition.y += (-pos.y - Metamaps.Visualize.cameraPosition.y) * 0.5; - Metamaps.Visualize.mGraph.plot(); - }, - onMouseWheel: function (delta) { - Metamaps.Visualize.cameraPosition.z += -delta * 20; - Metamaps.Visualize.mGraph.plot(); - }, - onClick: function () {} - }, - //Number of iterations for the FD algorithm - iterations: 200, - //Edge length - levelDistance: 100 - }, - nodeSettings: { - - }, - edgeSettings: { - - } - }, // ForceDirected3D - RGraph: { - animate: { - modes: ['polar'], - duration: 800, - onComplete: function () { - Metamaps.Visualize.mGraph.busy = false; - } - }, - // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ - background: { - //type: 'Metamaps', - levelDistance: 200, - numberOfCircles: 4, - CanvasStyles: { - strokeStyle: '#333', - lineWidth: 1.5 - } - }, - levelDistance: 200 - }, - onMouseEnter: function (edge) { - var filtered = edge.getData('alpha') === 0; - - // don't do anything if the edge is filtered - // or if the canvas is animating - if (filtered || Metamaps.Visualize.mGraph.busy) return; - - $('canvas').css('cursor', 'pointer'); - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge); - //following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { - edge.setData('showDesc', true, 'current'); - } - - edge.setDataset('end', { - lineWidth: 4 - }); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['edge-property:lineWidth'], - duration: 100 - }); - Metamaps.Visualize.mGraph.plot(); - }, // onMouseEnter - onMouseLeave: function (edge) { - if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered - $('canvas').css('cursor', 'default'); - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge); - //following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { - edge.setData('showDesc', false, 'current'); - } - - edge.setDataset('end', { - lineWidth: 2 - }); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['edge-property:lineWidth'], - duration: 100 - }); - Metamaps.Visualize.mGraph.plot(); - }, // onMouseLeave - onMouseMoveHandler: function (node, eventInfo, e) { - - var self = Metamaps.JIT; - - if (Metamaps.Visualize.mGraph.busy) return; - - var node = eventInfo.getNode(); - var edge = eventInfo.getEdge(); - - //if we're on top of a node object, act like there aren't edges under it - if (node != false) { - if (Metamaps.Mouse.edgeHoveringOver) { - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver); - } - $('canvas').css('cursor', 'pointer'); - return; - } - - if (edge == false && Metamaps.Mouse.edgeHoveringOver != false) { - //mouse not on an edge, but we were on an edge previously - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver); - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver == false) { - //mouse is on an edge, but there isn't a stored edge - self.onMouseEnter(edge); - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver != edge) { - //mouse is on an edge, but a different edge is stored - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) - self.onMouseEnter(edge); - } - - //could be false - Metamaps.Mouse.edgeHoveringOver = edge; - - if (!node && !edge) { - $('canvas').css('cursor', 'default'); - } - }, // onMouseMoveHandler - enterKeyHandler: function () { - var creatingMap = Metamaps.GlobalUI.lightbox; - if (creatingMap === "newmap" || creatingMap === "forkmap") { - Metamaps.GlobalUI.CreateMap.submit(); - } - // this is to submit new topic creation - else if (Metamaps.Create.newTopic.beingCreated) { - Metamaps.Topic.createTopicLocally(); - } - // to submit new synapse creation - else if (Metamaps.Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally(); - } - }, //enterKeyHandler - escKeyHandler: function () { - Metamaps.Control.deselectAllEdges(); - Metamaps.Control.deselectAllNodes(); - }, //escKeyHandler - touchPanZoomHandler: function (eventInfo, e) { - if (e.touches.length == 1) { - var thispos = Metamaps.Touch.touchPos, - currentPos = eventInfo.getPos(), - canvas = Metamaps.Visualize.mGraph.canvas, - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - currentPos.x *= sx; - currentPos.y *= sy; - currentPos.x += ox; - currentPos.y += oy; - //var x = currentPos.x - thispos.x, - // y = currentPos.y - thispos.y; - var x = currentPos.x - thispos.x, - y = currentPos.y - thispos.y; - Metamaps.Touch.touchPos = currentPos; - Metamaps.Visualize.mGraph.canvas.translate(x * 1 / sx, y * 1 / sy); - } else if (e.touches.length == 2) { - var touch1 = e.touches[0]; - var touch2 = e.touches[1]; - - var dist = Metamaps.Util.getDistance({ - x: touch1.clientX, - y: touch1.clientY - }, { - x: touch2.clientX, - y: touch2.clientY - }); - - if (!lastDist) { - lastDist = dist; - } - - var scale = dist / lastDist; - - if (8 >= Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale && Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale >= 1) { - Metamaps.Visualize.mGraph.canvas.scale(scale, scale); - } - if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX < 0.5) { - Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(true); - } else if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX > 0.5) { - Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(false); - } - lastDist = dist; - } - - }, // touchPanZoomHandler - onDragMoveTopicHandler: function (node, eventInfo, e) { - - var self = Metamaps.JIT; - - // this is used to send nodes that are moving to - // other realtime collaborators on the same map - var positionsToSend = {}; - var topic; - - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (node && !node.nodeFrom) { - var pos = eventInfo.getPos(); - // if it's a left click, or a touch, move the node - if (e.touches || (e.button == 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons == 0 || e.buttons == 1 || e.buttons == undefined))) { - //if the node dragged isn't already selected, select it - var whatToDo = self.handleSelectionBeforeDragging(node, e); - if (node.pos.rho || node.pos.rho === 0) { - // this means we're in topic view - var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y); - var theta = Math.atan2(pos.y, pos.x); - node.pos.setp(theta, rho); - } else if (whatToDo == 'only-drag-this-one') { - node.pos.setc(pos.x, pos.y); - - if (Metamaps.Active.Map) { - topic = node.getData('topic'); - // we use the topic ID not the node id - // because we can't depend on the node id - // to be the same as on other collaborators - // maps - positionsToSend[topic.id] = pos; - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]); - } - } else { - var len = Metamaps.Selected.Nodes.length; - - //first define offset for each node - var xOffset = new Array(); - var yOffset = new Array(); - for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i]; - xOffset[i] = n.pos.x - node.pos.x; - yOffset[i] = n.pos.y - node.pos.y; - } //for - - for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i]; - var x = pos.x + xOffset[i]; - var y = pos.y + yOffset[i]; - n.pos.setc(x, y); - - if (Metamaps.Active.Map) { - topic = n.getData('topic'); - // we use the topic ID not the node id - // because we can't depend on the node id - // to be the same as on other collaborators - // maps - positionsToSend[topic.id] = n.pos; - } - } //for - - if (Metamaps.Active.Map) { - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]); - } - } //if - - if (whatToDo == 'deselect') { - Metamaps.Control.deselectNode(node); - } - Metamaps.Visualize.mGraph.plot(); - } - // if it's a right click or holding down alt, start synapse creation ->third option is for firefox - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { - if (Metamaps.tempInit == false) { - Metamaps.tempNode = node; - Metamaps.tempInit = true; - - Metamaps.Create.newTopic.hide(); - Metamaps.Create.newSynapse.hide(); - // set the draw synapse start positions - var l = Metamaps.Selected.Nodes.length; - if (l > 0) { - for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i]; - Metamaps.Mouse.synapseStartCoordinates.push({ - x: n.pos.getc().x, - y: n.pos.getc().y - }); - } - } else { - Metamaps.Mouse.synapseStartCoordinates = [{ - x: Metamaps.tempNode.pos.getc().x, - y: Metamaps.tempNode.pos.getc().y - }]; - } - Metamaps.Mouse.synapseEndCoordinates = { - x: pos.x, - y: pos.y - }; - } - // - temp = eventInfo.getNode(); - if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned - Metamaps.tempNode2 = temp; - - Metamaps.Mouse.synapseEndCoordinates = { - x: Metamaps.tempNode2.pos.getc().x, - y: Metamaps.tempNode2.pos.getc().y - }; - - // before making the highlighted one bigger, make sure all the others are regular size - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - n.setData('dim', 25, 'current'); - }); - temp.setData('dim', 35, 'current'); - Metamaps.Visualize.mGraph.plot(); - } else if (!temp) { - Metamaps.tempNode2 = null; - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - n.setData('dim', 25, 'current'); - }); - //pop up node creation :) - var myX = e.clientX - 110; - var myY = e.clientY - 30; - $('#new_topic').css('left', myX + "px"); - $('#new_topic').css('top', myY + "px"); - Metamaps.Create.newTopic.x = eventInfo.getPos().x; - Metamaps.Create.newTopic.y = eventInfo.getPos().y; - Metamaps.Visualize.mGraph.plot(); - - Metamaps.Mouse.synapseEndCoordinates = { - x: pos.x, - y: pos.y - }; - } - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser("Cannot create in Topic view."); - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - } - } - }, // onDragMoveTopicHandler - onDragCancelHandler: function (node, eventInfo, e) { - Metamaps.tempNode = null; - if (Metamaps.tempNode2) Metamaps.tempNode2.setData('dim', 25, 'current'); - Metamaps.tempNode2 = null; - Metamaps.tempInit = false; - // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = []; - Metamaps.Mouse.synapseEndCoordinates = null; - Metamaps.Visualize.mGraph.plot(); - }, // onDragCancelHandler - onDragEndTopicHandler: function (node, eventInfo, e) { - var midpoint = {}, pixelPos, mapping; - - if (Metamaps.tempInit && Metamaps.tempNode2 == null) { - // this means you want to add a new topic, and then a synapse - Metamaps.Create.newTopic.addSynapse = true; - Metamaps.Create.newTopic.open(); - } else if (Metamaps.tempInit && Metamaps.tempNode2 != null) { - // this means you want to create a synapse between two existing topics - Metamaps.Create.newTopic.addSynapse = false; - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id; - Metamaps.Create.newSynapse.topic2id = Metamaps.tempNode2.getData('topic').id; - Metamaps.tempNode2.setData('dim', 25, 'current'); - Metamaps.Visualize.mGraph.plot(); - midpoint.x = Metamaps.tempNode.pos.getc().x + (Metamaps.tempNode2.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2; - midpoint.y = Metamaps.tempNode.pos.getc().y + (Metamaps.tempNode2.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2; - pixelPos = Metamaps.Util.coordsToPixels(midpoint); - $('#new_synapse').css('left', pixelPos.x + "px"); - $('#new_synapse').css('top', pixelPos.y + "px"); - Metamaps.Create.newSynapse.open(); - Metamaps.tempNode = null; - Metamaps.tempNode2 = null; - Metamaps.tempInit = false; - } else if (!Metamaps.tempInit && node && !node.nodeFrom) { - // this means you dragged an existing node, autosave that to the database - - // check whether to save mappings - var checkWhetherToSave = function() { - var map = Metamaps.Active.Map; - - if (!map) return false; - - var mapper = Metamaps.Active.Mapper; - // this case - // covers when it is a public map owned by you - // and also when it's a private map - var activeMappersMap = map.authorizePermissionChange(mapper); - var commonsMap = map.get('permission') === 'commons'; - var realtimeOn = Metamaps.Realtime.status; - - // don't save if commons map, and you have realtime off, - // even if you're map creator - return map && mapper && ((commonsMap && realtimeOn) || (activeMappersMap && !commonsMap)); - } - - if (checkWhetherToSave()) { - mapping = node.getData('mapping'); - mapping.save({ - xloc: node.getPos().x, - yloc: node.getPos().y - }); - // also save any other selected nodes that also got dragged along - var l = Metamaps.Selected.Nodes.length; - for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i]; - if (n !== node) { - mapping = n.getData('mapping'); - mapping.save({ - xloc: n.getPos().x, - yloc: n.getPos().y - }); - } - }; - } - } - }, //onDragEndTopicHandler - canvasClickHandler: function (canvasLoc, e) { - //grab the location and timestamp of the click - var storedTime = Metamaps.Mouse.lastCanvasClick; - var now = Date.now(); //not compatible with IE8 FYI - Metamaps.Mouse.lastCanvasClick = now; - - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE && !Metamaps.Mouse.didPan) { - if (Metamaps.Active.Map && !authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - else if (Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser("Cannot create in Topic view."); - return; - } - // DOUBLE CLICK - //pop up node creation :) - Metamaps.Create.newTopic.addSynapse = false; - Metamaps.Create.newTopic.x = canvasLoc.x; - Metamaps.Create.newTopic.y = canvasLoc.y; - $('#new_topic').css('left', e.clientX + "px"); - $('#new_topic').css('top', e.clientY + "px"); - Metamaps.Create.newTopic.open(); - } else if (!Metamaps.Mouse.didPan) { - // SINGLE CLICK, no pan - Metamaps.Filter.close(); - Metamaps.TopicCard.hideCard(); - Metamaps.SynapseCard.hideCard(); - Metamaps.Create.newTopic.hide(); - $('.rightclickmenu').remove(); - // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = []; - Metamaps.Mouse.synapseEndCoordinates = null; - Metamaps.tempInit = false; - Metamaps.tempNode = null; - Metamaps.tempNode2 = null; - if (!e.ctrlKey && !e.shiftKey) { - Metamaps.Control.deselectAllEdges(); - Metamaps.Control.deselectAllNodes(); - } - } - }, //canvasClickHandler - nodeDoubleClickHandler: function (node, e) { - - Metamaps.TopicCard.showCard(node); - - }, // nodeDoubleClickHandler - edgeDoubleClickHandler: function (adj, e) { - - Metamaps.SynapseCard.showCard(adj, e); - - }, // nodeDoubleClickHandler - nodeWasDoubleClicked: function () { - //grab the timestamp of the click - var storedTime = Metamaps.Mouse.lastNodeClick; - var now = Date.now(); //not compatible with IE8 FYI - Metamaps.Mouse.lastNodeClick = now; - - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) { - return true; - } else { - return false; - } - }, //nodeWasDoubleClicked; - handleSelectionBeforeDragging: function (node, e) { - // four cases: - // 1 nothing is selected, so pretend you aren't selecting - // 2 others are selected only and shift, so additionally select this one - // 3 others are selected only, no shift: drag only this one - // 4 this node and others were selected, so drag them (just return false) - //return value: deselect node again after? - if (Metamaps.Selected.Nodes.length == 0) { - return 'only-drag-this-one'; - } - if (Metamaps.Selected.Nodes.indexOf(node) == -1) { - if (e.shiftKey) { - Metamaps.Control.selectNode(node,e); - return 'nothing'; - } else { - return 'only-drag-this-one'; - } - } - return 'nothing'; //case 4? - }, // handleSelectionBeforeDragging - selectWithBox: function (e) { - - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y; - - if(!e.shiftKey){ - Metamaps.Control.deselectAllNodes(); - Metamaps.Control.deselectAllEdges(); - } - - //select all nodes that are within the box - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - var x = n.pos.x, - y = n.pos.y; - - if ((sX < x && x < eX && sY < y && y < eY) || (sX > x && x > eX && sY > y && y > eY) || (sX > x && x > eX && sY < y && y < eY) || (sX < x && x < eX && sY > y && y > eY)) { - if(e.shiftKey){ - if(n.selected){ - Metamaps.Control.deselectNode(n); - } - else{ - Metamaps.Control.selectNode(n,e); - } - } - else{ - Metamaps.Control.selectNode(n,e); - } - } - }); - - //Convert selection box coordinates to traditional coordinates (+,+) in upper right - sY = -1 * sY; - eY = -1 * eY - - var edgesToToggle = []; - Metamaps.Synapses.each(function(synapse) { - var e = synapse.get('edge'); - if (edgesToToggle.indexOf(e) === -1) { - edgesToToggle.push(e); - } - }); - edgesToToggle.forEach(function(edge) { - var fromNodeX = edge.nodeFrom.pos.x; - var fromNodeY = -1 * edge.nodeFrom.pos.y; - var toNodeX = edge.nodeTo.pos.x; - var toNodeY = -1 * edge.nodeTo.pos.y; - - var maxX = fromNodeX; - var maxY = fromNodeY; - var minX = fromNodeX; - var minY = fromNodeY; - - //Correct maxX, MaxY values - (toNodeX > maxX) ? (maxX = toNodeX):(minX = toNodeX); - (toNodeY > maxY) ? (maxY = toNodeY):(minY = toNodeY); - - var maxBoxX = sX; - var maxBoxY = sY; - var minBoxX = sX; - var minBoxY = sY; - - //Correct maxBoxX, maxBoxY values - (eX > maxBoxX) ? (maxBoxX = eX):(minBoxX = eX); - (eY > maxBoxY) ? (maxBoxY = eY):(minBoxY = eY); - - //Find the slopes from the synapse fromNode to the 4 corners of the selection box - var slopes = []; - slopes.push( (sY - fromNodeY) / (sX - fromNodeX) ); - slopes.push( (sY - fromNodeY) / (eX - fromNodeX) ); - slopes.push( (eY - fromNodeY) / (eX - fromNodeX) ); - slopes.push( (eY - fromNodeY) / (sX - fromNodeX) ); - - var minSlope = slopes[0]; - var maxSlope = slopes[0]; - slopes.forEach(function(entry){ - if(entry > maxSlope) maxSlope = entry; - if(entry < minSlope) minSlope = entry; - }); - - //Find synapse-in-question's slope - var synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX); - var b = fromNodeY - synSlope * fromNodeX; - - //Use the selection box edges as test cases for synapse intersection - var testX = sX; - var testY = synSlope * testX + b; - - var selectTest; - - if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY){ - selectTest = true; - } - - testX = eX; - testY = synSlope * testX + b; - - if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY){ - selectTest = true; - } - - testY = sY; - testX = (testY - b)/synSlope; - - if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX){ - selectTest = true; - } - - testY = eY; - testX = (testY - b)/synSlope; - - if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX){ - selectTest = true; - } - - //Case where the synapse is wholly enclosed in the seldction box - if(fromNodeX >= minBoxX && fromNodeX <= maxBoxX && fromNodeY >= minBoxY && fromNodeY <= maxBoxY && toNodeX >= minBoxX && toNodeX <= maxBoxX && toNodeY >= minBoxY && toNodeY <= maxBoxY){ - selectTest = true; - } - - //The test synapse was selected! - - if(selectTest){ - // shiftKey = toggleSelect, otherwise - if(e.shiftKey){ - if(Metamaps.Selected.Edges.indexOf(edge) != -1 ){ - Metamaps.Control.deselectEdge(edge); - } - else{ - Metamaps.Control.selectEdge(edge); - } - } - else{ - Metamaps.Control.selectEdge(edge); - } - } - }); - Metamaps.Mouse.boxStartCoordinates = false; - Metamaps.Mouse.boxEndCoordinates = false; - Metamaps.Visualize.mGraph.plot(); - }, // selectWithBox - drawSelectBox: function (eventInfo, e) { - var ctx = Metamaps.Visualize.mGraph.canvas.getCtx(); - - var startX = Metamaps.Mouse.boxStartCoordinates.x, - startY = Metamaps.Mouse.boxStartCoordinates.y, - currX = eventInfo.getPos().x, - currY = eventInfo.getPos().y; - - Metamaps.Visualize.mGraph.canvas.clear(); - Metamaps.Visualize.mGraph.plot(); - - ctx.beginPath(); - ctx.moveTo(startX, startY); - ctx.lineTo(startX, currY); - ctx.lineTo(currX, currY); - ctx.lineTo(currX, startY); - ctx.lineTo(startX, startY); - ctx.strokeStyle = "black"; - ctx.stroke(); - }, // drawSelectBox - selectNodeOnClickHandler: function (node, e) { - if (Metamaps.Visualize.mGraph.busy) return; - - var self = Metamaps.JIT; - - // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey) { - self.selectNodeOnRightClickHandler(node, e) - return; - } - - // if on a topic page, let alt+click center you on a new topic - if (Metamaps.Active.Topic && e.altKey) { - Metamaps.RGraph.centerOn(node.id); - return; - } - - var check = self.nodeWasDoubleClicked(); - if (check) { - self.nodeDoubleClickHandler(node, e); - return; - } else { - // wait a certain length of time, then check again, then run this code - setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { - - var nodeAlreadySelected = node.selected; - - if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes(); - Metamaps.Control.deselectAllEdges(); - } - - if (nodeAlreadySelected) { - Metamaps.Control.deselectNode(node); - } else { - Metamaps.Control.selectNode(node,e); - } - - //trigger animation to final styles - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['edge-property:lineWidth:color:alpha'], - duration: 500 - }); - Metamaps.Visualize.mGraph.plot(); - } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE); - } - }, //selectNodeOnClickHandler - selectNodeOnRightClickHandler: function (node, e) { - // the 'node' variable is a JIT node, the one that was clicked on - // the 'e' variable is the click event - - e.preventDefault(); - e.stopPropagation(); - - if (Metamaps.Visualize.mGraph.busy) return; - - // select the node - Metamaps.Control.selectNode(node, e); - - // delete old right click menu - $('.rightclickmenu').remove(); - // create new menu for clicked on node - var rightclickmenu = document.createElement("div"); - rightclickmenu.className = "rightclickmenu"; - // add the proper options to the menu - var menustring = ''; - rightclickmenu.innerHTML = menustring; - - // position the menu where the click happened - var position = {}; - var RIGHTCLICK_WIDTH = 300; - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256; - var MAX_SUBMENU_HEIGHT = 270; - var windowWidth = $(window).width(); - var windowHeight = $(window).height(); - - if (windowWidth - e.clientX < SUBMENUS_WIDTH) { - position.right = windowWidth - e.clientX; - $(rightclickmenu).addClass('moveMenusToLeft'); - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { - position.right = windowWidth - e.clientX; - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { - position.left = e.clientX; - $(rightclickmenu).addClass('moveMenusToLeft'); - } - else position.left = e.clientX; - - if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { - position.bottom = windowHeight - e.clientY; - $(rightclickmenu).addClass('moveMenusUp'); - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { - position.top = e.clientY; - $(rightclickmenu).addClass('moveMenusUp'); - } - else position.top = e.clientY; - - $(rightclickmenu).css(position); - //add the menu to the page - $('#wrapper').append(rightclickmenu); - - // attach events to clicks on the list items - - // delete the selected things from the database - if (authorized) { - $('.rc-delete').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.deleteSelected(); - }); - } - - // remove the selected things from the map - if (authorized) { - $('.rc-remove').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.removeSelectedEdges(); - Metamaps.Control.removeSelectedNodes(); - }); - } - - // hide selected nodes and synapses until refresh - $('.rc-hide').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.hideSelectedEdges(); - Metamaps.Control.hideSelectedNodes(); - }); - - // when in radial, center on the topic you picked - $('.rc-center').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Topic.centerOn(node.id); - }); - - // open the entity in a new tab - $('.rc-popout').click(function () { - $('.rightclickmenu').remove(); - var win = window.open('/topics/' + node.id, '_blank'); - win.focus(); - }); - - // change the permission of all the selected nodes and synapses that you were the originator of - $('.rc-permission li').click(function () { - $('.rightclickmenu').remove(); - // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()); - }); - - // change the metacode of all the selected nodes that you have edit permission for - $('.rc-metacode li li').click(function () { - $('.rightclickmenu').remove(); - // - Metamaps.Control.updateSelectedMetacodes($(this).attr('data-id')); - }); - - - // fetch relatives - var fetched = false; - $('.rc-siblings').hover(function () { - if (!fetched) { - Metamaps.JIT.populateRightClickSiblings(node); - fetched = true; - } - }); - $('.rc-siblings .fetchAll').click(function () { - $('.rightclickmenu').remove(); - // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node); - }); - }, //selectNodeOnRightClickHandler, - populateRightClickSiblings: function(node) { - var self = Metamaps.JIT; - - // depending on how many topics are selected, do different things - /*if (Metamaps.Selected.Nodes.length > 1) { - // we don't bother filling the submenu with - // specific numbers, because there are too many topics - // selected to find those numbers - $('#loadingSiblings').remove(); - return; - }*/ - - var topic = node.getData('topic'); - - // add a loading icon for now - var loader = new CanvasLoader('loadingSiblings'); - loader.setColor('#4FC059'); // default is '#000000' - loader.setDiameter(15); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - - var topics = Metamaps.Topics.map(function(t){ return t.id }); - var topics_string = topics.join(); - - var successCallback = function(data) { - $('#loadingSiblings').remove(); - - for (var key in data) { - var string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')'; - $('#fetchSiblingList').append('
  • ' + string + '
  • '); - } - - $('.rc-siblings .getSiblings').click(function () { - $('.rightclickmenu').remove(); - // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node, $(this).attr('data-id')); - }); - }; - - $.ajax({ - type: "Get", - url: "/topics/" + topic.id + "/relative_numbers.json?network=" + topics_string, - success: successCallback, - error: function () { - - } - }); - }, - selectEdgeOnClickHandler: function (adj, e) { - if (Metamaps.Visualize.mGraph.busy) return; - - var self = Metamaps.JIT; - - // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey) { - self.selectEdgeOnRightClickHandler(adj, e) - return; - } - - var check = self.nodeWasDoubleClicked(); - if (check) { - self.edgeDoubleClickHandler(adj, e); - return; - } else { - // wait a certain length of time, then check again, then run this code - setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { - - var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1; - - if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes(); - Metamaps.Control.deselectAllEdges(); - } - - if (edgeAlreadySelected) { - Metamaps.Control.deselectEdge(adj); - } else { - Metamaps.Control.selectEdge(adj); - } - - Metamaps.Visualize.mGraph.plot(); - } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE); - } - }, //selectEdgeOnClickHandler - selectEdgeOnRightClickHandler: function (adj, e) { - // the 'node' variable is a JIT node, the one that was clicked on - // the 'e' variable is the click event - - if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered - - var authorized; - - e.preventDefault(); - e.stopPropagation(); - - if (Metamaps.Visualize.mGraph.busy) return; - - Metamaps.Control.selectEdge(adj); - - // delete old right click menu - $('.rightclickmenu').remove(); - // create new menu for clicked on node - var rightclickmenu = document.createElement("div"); - rightclickmenu.className = "rightclickmenu"; - - // add the proper options to the menu - var menustring = ''; - rightclickmenu.innerHTML = menustring; - - // position the menu where the click happened - var position = {}; - var RIGHTCLICK_WIDTH = 300; - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256; - var MAX_SUBMENU_HEIGHT = 270; - var windowWidth = $(window).width(); - var windowHeight = $(window).height(); - - if (windowWidth - e.clientX < SUBMENUS_WIDTH) { - position.right = windowWidth - e.clientX; - $(rightclickmenu).addClass('moveMenusToLeft'); - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { - position.right = windowWidth - e.clientX; - } - else position.left = e.clientX; - - if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { - position.bottom = windowHeight - e.clientY; - $(rightclickmenu).addClass('moveMenusUp'); - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { - position.top = e.clientY; - $(rightclickmenu).addClass('moveMenusUp'); - } - else position.top = e.clientY; - - $(rightclickmenu).css(position); - - //add the menu to the page - $('#wrapper').append(rightclickmenu); - - - // attach events to clicks on the list items - - // delete the selected things from the database - if (authorized) { - $('.rc-delete').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.deleteSelected(); - }); - } - - // remove the selected things from the map - if (authorized) { - $('.rc-remove').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.removeSelectedEdges(); - Metamaps.Control.removeSelectedNodes(); - }); - } - - // hide selected nodes and synapses until refresh - $('.rc-hide').click(function () { - $('.rightclickmenu').remove(); - Metamaps.Control.hideSelectedEdges(); - Metamaps.Control.hideSelectedNodes(); - }); - - // change the permission of all the selected nodes and synapses that you were the originator of - $('.rc-permission li').click(function () { - $('.rightclickmenu').remove(); - // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()); - }); - - }, //selectEdgeOnRightClickHandler - SmoothPanning: function () { - - var sx = Metamaps.Visualize.mGraph.canvas.scaleOffsetX, - sy = Metamaps.Visualize.mGraph.canvas.scaleOffsetY, - y_velocity = Metamaps.Mouse.changeInY, // initial y velocity - x_velocity = Metamaps.Mouse.changeInX, // initial x velocity - easing = 1; // frictional value - - easing = 1; - window.clearInterval(Metamaps.panningInt) - Metamaps.panningInt = setInterval(function () { - myTimer() - }, 1); - - function myTimer() { - Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy); - $(document).trigger(Metamaps.JIT.events.pan); - easing = easing * 0.75; - - if (easing < 0.1) window.clearInterval(Metamaps.panningInt); - } - }, // SmoothPanning - renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { - var ctx = canvas.getCtx(); - // invert edge direction - if (swap) { - var tmp = from; - from = to; - to = tmp; - } - // vect represents a line from tip to tail of the arrow - var vect = new $jit.Complex(to.x - from.x, to.y - from.y); - // scale it - vect.$scale(dim / vect.norm()); - // compute the midpoint of the edge line - var newX = (to.x - from.x) * placement + from.x; - var newY = (to.y - from.y) * placement + from.y; - var midPoint = new $jit.Complex(newX, newY); - - // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint - var arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y); - // compute the tail intersection point with the edge line - var intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y); - // vector perpendicular to vect - var normal = new $jit.Complex(-vect.y / 2, vect.x / 2); - var v1 = intermediatePoint.add(normal); - var v2 = intermediatePoint.$add(normal.$scale(-1)); - - if (newSynapse) { - ctx.strokeStyle = "#4fc059"; - ctx.lineWidth = 2; - ctx.globalAlpha = 1; - } - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(v1.x, v1.y); - ctx.lineTo(arrowPoint.x, arrowPoint.y); - ctx.lineTo(v2.x, v2.y); - ctx.stroke(); - }, // renderMidArrow - renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { - - var self = Metamaps.JIT; - - var directionCat = synapse.get('category'); - var direction = synapse.getDirection(); - - var pos = adj.nodeFrom.pos.getc(true); - var posChild = adj.nodeTo.pos.getc(true); - - //plot arrow edge - if (!direction) { - // render nothing for this arrow if the direction couldn't be retrieved - } else if (directionCat == "none") { - edgeHelper.line.render({ - x: pos.x, - y: pos.y - }, { - x: posChild.x, - y: posChild.y - }, canvas); - } else if (directionCat == "both") { - self.renderMidArrow({ - x: pos.x, - y: pos.y - }, { - x: posChild.x, - y: posChild.y - }, 13, true, canvas, 0.7); - self.renderMidArrow({ - x: pos.x, - y: pos.y - }, { - x: posChild.x, - y: posChild.y - }, 13, false, canvas, 0.7); - } else if (directionCat == "from-to") { - var inv = (direction[0] != adj.nodeFrom.id); - self.renderMidArrow({ - x: pos.x, - y: pos.y - }, { - x: posChild.x, - y: posChild.y - }, 13, inv, canvas, 0.7); - self.renderMidArrow({ - x: pos.x, - y: pos.y - }, { - x: posChild.x, - y: posChild.y - }, 13, inv, canvas, 0.3); - } - }, //renderEdgeArrows - zoomIn: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(1.25,1.25); - $(document).trigger(Metamaps.JIT.events.zoom, [event]); - }, - zoomOut: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(0.8,0.8); - $(document).trigger(Metamaps.JIT.events.zoom, [event]); - }, - centerMap: function (canvas) { - var offsetScale = canvas.scaleOffsetX; - - canvas.scale(1/offsetScale,1/offsetScale); - - var offsetX = canvas.translateOffsetX; - var offsetY = canvas.translateOffsetY; - - canvas.translate(-1*offsetX,-1*offsetY); - }, - zoomToBox: function (event) { - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y; - - var canvas = Metamaps.Visualize.mGraph.canvas; - Metamaps.JIT.centerMap(canvas); - - var height = $(document).height(), - width = $(document).width(); - - var spanX = Math.abs(sX - eX); - var spanY = Math.abs(sY - eY); - var ratioX = width / spanX; - var ratioY = height / spanY; - - var newRatio = Math.min(ratioX,ratioY); - - if(canvas.scaleOffsetX *newRatio<= 5 && canvas.scaleOffsetX*newRatio >= 0.2){ - canvas.scale(newRatio,newRatio); - } - else if(canvas.scaleOffsetX * newRatio > 5){ - newRatio = 5/ canvas.scaleOffsetX; - canvas.scale(newRatio,newRatio); - } - else{ - newRatio = 0.2/ canvas.scaleOffsetX; - canvas.scale(newRatio,newRatio); - } - - var cogX = (sX + eX)/2; - var cogY = (sY + eY)/2; - - canvas.translate(-1* cogX, -1* cogY); - $(document).trigger(Metamaps.JIT.events.zoom, [event]); - - Metamaps.Mouse.boxStartCoordinates = false; - Metamaps.Mouse.boxEndCoordinates = false; - Metamaps.Visualize.mGraph.plot(); - - }, - zoomExtents: function (event, canvas, denySelected) { - Metamaps.JIT.centerMap(canvas); - var height = canvas.getSize().height, - width = canvas.getSize().width, - maxX, minX, maxY, minY, counter = 0; - - - if (!denySelected && Metamaps.Selected.Nodes.length > 0) { - var nodes = Metamaps.Selected.Nodes; - } - else { - var nodes = _.values(Metamaps.Visualize.mGraph.graph.nodes); - } - - if(nodes.length > 1){ - nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y; - - if (counter == 0 && n.getData('alpha') == 1){ - maxX = x; - minX = x; - maxY = y; - minY = y; - } - - var arrayOfLabelLines = Metamaps.Util.splitLine(n.name, 30).split('\n'), - dim = n.getData('dim'), - ctx = canvas.getCtx(); - - var height = 25 * arrayOfLabelLines.length; - - var index, lineWidths = []; - for (index = 0; index < arrayOfLabelLines.length; ++index) { - lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) - } - var width = Math.max.apply(null, lineWidths) + 8; - - // only adjust these values if the node is not filtered - if (n.getData('alpha') == 1) { - maxX = Math.max(x + width /2,maxX); - maxY = Math.max(y + n.getData("height") + 5 + height,maxY); - minX = Math.min(x - width /2,minX); - minY = Math.min(y - dim,minY); - - counter++; - } - }); - - var spanX = maxX - minX; - var spanY = maxY - minY; - var ratioX = spanX / width; - var ratioY = spanY / height; - - var cogX = (maxX + minX)/2; - var cogY = (maxY + minY)/2; - - canvas.translate(-1* cogX, -1* cogY); - - var newRatio = Math.max(ratioX,ratioY); - var scaleMultiplier = 1/newRatio*0.9; - - if(canvas.scaleOffsetX *scaleMultiplier<= 3 && canvas.scaleOffsetX*scaleMultiplier >= 0.2){ - canvas.scale(scaleMultiplier,scaleMultiplier); - } - else if(canvas.scaleOffsetX * scaleMultiplier > 3){ - scaleMultiplier = 3/ canvas.scaleOffsetX; - canvas.scale(scaleMultiplier,scaleMultiplier); - } - else{ - scaleMultiplier = 0.2/ canvas.scaleOffsetX; - canvas.scale(scaleMultiplier,scaleMultiplier); - } - - $(document).trigger(Metamaps.JIT.events.zoom, [event]); - } - else if(nodes.length == 1){ - nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y; - - canvas.translate(-1* x, -1* y); - $(document).trigger(Metamaps.JIT.events.zoom, [event]); - }); - } - } -}; diff --git a/app/assets/javascripts/src/Metamaps.Listeners.js b/app/assets/javascripts/src/Metamaps.Listeners.js new file mode 100644 index 00000000..83204eee --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Listeners.js @@ -0,0 +1,78 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Listeners.js.erb + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Control + * - Metamaps.JIT + * - Metamaps.Visualize + */ +Metamaps.Listeners = { + init: function () { + $(document).on('keydown', function (e) { + if (!(Metamaps.Active.Map || Metamaps.Active.Topic)) return + + switch (e.which) { + case 13: // if enter key is pressed + Metamaps.JIT.enterKeyHandler() + e.preventDefault() + break + case 27: // if esc key is pressed + Metamaps.JIT.escKeyHandler() + break + case 65: // if a or A is pressed + if (e.ctrlKey) { + Metamaps.Control.deselectAllNodes() + Metamaps.Control.deselectAllEdges() + + e.preventDefault() + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Metamaps.Control.selectNode(n, e) + }) + + Metamaps.Visualize.mGraph.plot() + } + + break + case 69: // if e or E is pressed + if (e.ctrlKey) { + e.preventDefault() + if (Metamaps.Active.Map) { + Metamaps.JIT.zoomExtents(null, Metamaps.Visualize.mGraph.canvas) + } + } + break + case 77: // if m or M is pressed + if (e.ctrlKey) { + e.preventDefault() + Metamaps.Control.removeSelectedNodes() + Metamaps.Control.removeSelectedEdges() + } + break + case 68: // if d or D is pressed + if (e.ctrlKey) { + e.preventDefault() + Metamaps.Control.deleteSelected() + } + break + case 72: // if h or H is pressed + if (e.ctrlKey) { + e.preventDefault() + Metamaps.Control.hideSelectedNodes() + Metamaps.Control.hideSelectedEdges() + } + break + default: + break; // alert(e.which) + } + }) + + $(window).resize(function () { + if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition() + if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos() + }) + } +}; // end Metamaps.Listeners diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/app/assets/javascripts/src/Metamaps.Map.js new file mode 100644 index 00000000..34374614 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Map.js @@ -0,0 +1,763 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Map.js.erb + * + * Dependencies: + * - Metamaps.Create + * - Metamaps.Erb + * - Metamaps.Filter + * - Metamaps.JIT + * - Metamaps.Loading + * - Metamaps.Maps + * - Metamaps.Realtime + * - Metamaps.Router + * - Metamaps.Selected + * - Metamaps.SynapseCard + * - Metamaps.TopicCard + * - Metamaps.Visualize + * - Metamaps.Active + * - Metamaps.Backbone + * - Metamaps.GlobalUI + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Messages + * - Metamaps.Synapses + * - Metamaps.Topics + * + * Major sub-modules: + * - Metamaps.Map.CheatSheet + * - Metamaps.Map.InfoBox + */ + +Metamaps.Map = { + events: { + editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' + }, + nextX: 0, + nextY: 0, + sideLength: 1, + turnCount: 0, + nextXshift: 1, + nextYshift: 0, + timeToTurn: 0, + init: function () { + var self = Metamaps.Map + + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#center-container').bind('contextmenu', function (e) { + return false + }) + + $('.sidebarFork').click(function () { + self.fork() + }) + + Metamaps.GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() + + self.InfoBox.init() + self.CheatSheet.init() + + $(document).on(Metamaps.Map.events.editedByActiveMapper, self.editedByActiveMapper) + }, + launch: function (id) { + var bb = Metamaps.Backbone + var start = function (data) { + Metamaps.Active.Map = new bb.Map(data.map) + Metamaps.Mappers = new bb.MapperCollection(data.mappers) + Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) + Metamaps.Topics = new bb.TopicCollection(data.topics) + Metamaps.Synapses = new bb.SynapseCollection(data.synapses) + Metamaps.Mappings = new bb.MappingCollection(data.mappings) + Metamaps.Messages = data.messages + Metamaps.Backbone.attachCollectionEvents() + + var map = Metamaps.Active.Map + var mapper = Metamaps.Active.Mapper + + // add class to .wrapper for specifying whether you can edit the map + if (map.authorizeToEdit(mapper)) { + $('.wrapper').addClass('canEditMap') + } + + // add class to .wrapper for specifying if the map can + // be collaborated on + if (map.get('permission') === 'commons') { + $('.wrapper').addClass('commonsMap') + } + + // set filter mapper H3 text + $('#filter_by_mapper h3').html('MAPPERS') + + // build and render the visualization + Metamaps.Visualize.type = 'ForceDirected' + Metamaps.JIT.prepareVizData() + + // update filters + Metamaps.Filter.reset() + + // reset selected arrays + Metamaps.Selected.reset() + + // set the proper mapinfobox content + Metamaps.Map.InfoBox.load() + + // these three update the actual filter box with the right list items + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMappers() + + Metamaps.Realtime.startActiveMap() + Metamaps.Loading.hide() + } + + $.ajax({ + url: '/maps/' + id + '/contains.json', + success: start + }) + }, + end: function () { + if (Metamaps.Active.Map) { + $('.wrapper').removeClass('canEditMap commonsMap') + Metamaps.Map.resetSpiral() + + $('.rightclickmenu').remove() + Metamaps.TopicCard.hideCard() + Metamaps.SynapseCard.hideCard() + Metamaps.Create.newTopic.hide() + Metamaps.Create.newSynapse.hide() + Metamaps.Filter.close() + Metamaps.Map.InfoBox.close() + Metamaps.Realtime.endActiveMap() + } + }, + fork: function () { + Metamaps.GlobalUI.openLightbox('forkmap') + + var nodes_data = '', + synapses_data = '' + var nodes_array = [] + var synapses_array = [] + // collect the unfiltered topics + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + // if the opacity is less than 1 then it's filtered + if (n.getData('alpha') === 1) { + var id = n.getData('topic').id + nodes_array.push(id) + var x, y + if (n.pos.x && n.pos.y) { + x = n.pos.x + y = n.pos.y + } else { + var x = Math.cos(n.pos.theta) * n.pos.rho + var y = Math.sin(n.pos.theta) * n.pos.rho + } + nodes_data += id + '/' + x + '/' + y + ',' + } + }) + // collect the unfiltered synapses + Metamaps.Synapses.each(function (synapse) { + var desc = synapse.get('desc') + + var descNotFiltered = Metamaps.Filter.visible.synapses.indexOf(desc) > -1 + // make sure that both topics are being added, otherwise, it + // doesn't make sense to add the synapse + var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 + if (descNotFiltered && topicsNotFiltered) { + synapses_array.push(synapse.id) + } + }) + + synapses_data = synapses_array.join() + nodes_data = nodes_data.slice(0, -1) + + Metamaps.GlobalUI.CreateMap.topicsToMap = nodes_data + Metamaps.GlobalUI.CreateMap.synapsesToMap = synapses_data + }, + leavePrivateMap: function () { + var map = Metamaps.Active.Map + Metamaps.Maps.Active.remove(map) + Metamaps.Maps.Featured.remove(map) + Metamaps.Router.home() + Metamaps.GlobalUI.notifyUser('Sorry! That map has been changed to Private.') + }, + cantEditNow: function () { + Metamaps.Realtime.turnOff(true); // true is for 'silence' + Metamaps.GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') + Metamaps.Active.Map.trigger('changeByOther') + }, + canEditNow: function () { + var confirmString = "You've been granted permission to edit this map. " + confirmString += 'Do you want to reload and enable realtime collaboration?' + var c = confirm(confirmString) + if (c) { + Metamaps.Router.maps(Metamaps.Active.Map.id) + } + }, + editedByActiveMapper: function () { + if (Metamaps.Active.Mapper) { + Metamaps.Mappers.add(Metamaps.Active.Mapper) + } + }, + getNextCoord: function () { + var self = Metamaps.Map + var nextX = self.nextX + var nextY = self.nextY + + var DISTANCE_BETWEEN = 120 + + self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift + self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift + + self.timeToTurn += 1 + // if true, it's time to turn + if (self.timeToTurn === self.sideLength) { + self.turnCount += 1 + // if true, it's time to increase side length + if (self.turnCount % 2 === 0) { + self.sideLength += 1 + } + self.timeToTurn = 0 + + // going right? turn down + if (self.nextXshift == 1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = 1 + } + // going down? turn left + else if (self.nextXshift == 0 && self.nextYshift == 1) { + self.nextXshift = -1 + self.nextYshift = 0 + } + // going left? turn up + else if (self.nextXshift == -1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = -1 + } + // going up? turn right + else if (self.nextXshift == 0 && self.nextYshift == -1) { + self.nextXshift = 1 + self.nextYshift = 0 + } + } + + return { + x: nextX, + y: nextY + } + }, + resetSpiral: function () { + Metamaps.Map.nextX = 0 + Metamaps.Map.nextY = 0 + Metamaps.Map.nextXshift = 1 + Metamaps.Map.nextYshift = 0 + Metamaps.Map.sideLength = 1 + Metamaps.Map.timeToTurn = 0 + Metamaps.Map.turnCount = 0 + }, + exportImage: function () { + var canvas = {} + + canvas.canvas = document.createElement('canvas') + canvas.canvas.width = 1880 // 960 + canvas.canvas.height = 1260 // 630 + + canvas.scaleOffsetX = 1 + canvas.scaleOffsetY = 1 + canvas.translateOffsetY = 0 + canvas.translateOffsetX = 0 + canvas.denySelected = true + + canvas.getSize = function () { + if (this.size) return this.size + var canvas = this.canvas + return this.size = { + width: canvas.width, + height: canvas.height + } + } + canvas.scale = function (x, y) { + var px = this.scaleOffsetX * x, + py = this.scaleOffsetY * y + var dx = this.translateOffsetX * (x - 1) / px, + dy = this.translateOffsetY * (y - 1) / py + this.scaleOffsetX = px + this.scaleOffsetY = py + this.getCtx().scale(x, y) + this.translate(dx, dy) + } + canvas.translate = function (x, y) { + var sx = this.scaleOffsetX, + sy = this.scaleOffsetY + this.translateOffsetX += x * sx + this.translateOffsetY += y * sy + this.getCtx().translate(x, y) + } + canvas.getCtx = function () { + return this.canvas.getContext('2d') + } + // center it + canvas.getCtx().translate(1880 / 2, 1260 / 2) + + var mGraph = Metamaps.Visualize.mGraph + + var id = mGraph.root + var root = mGraph.graph.getNode(id) + var T = !!root.visited + + // pass true to avoid basing it on a selection + Metamaps.JIT.zoomExtents(null, canvas, true) + + var c = canvas.canvas, + ctx = canvas.getCtx(), + scale = canvas.scaleOffsetX + + // draw a grey background + ctx.fillStyle = '#d8d9da' + var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), + yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) + ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) + + // draw the graph + mGraph.graph.eachNode(function (node) { + var nodeAlpha = node.getData('alpha') + node.eachAdjacency(function (adj) { + var nodeTo = adj.nodeTo + if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { + mGraph.fx.plotLine(adj, canvas) + } + }) + if (node.drawn) { + mGraph.fx.plotNode(node, canvas) + } + if (!mGraph.labelsHidden) { + if (node.drawn && nodeAlpha >= 0.95) { + mGraph.labels.plotLabel(canvas, node) + } else { + mGraph.labels.hideLabel(node, false) + } + } + node.visited = !T + }) + + var imageData = { + encoded_image: canvas.canvas.toDataURL() + } + + var map = Metamaps.Active.Map + + var today = new Date() + var dd = today.getDate() + var mm = today.getMonth() + 1; // January is 0! + var yyyy = today.getFullYear() + if (dd < 10) { + dd = '0' + dd + } + if (mm < 10) { + mm = '0' + mm + } + today = mm + '/' + dd + '/' + yyyy + + var mapName = map.get('name').split(' ').join([separator = '-']) + var downloadMessage = '' + downloadMessage += 'Captured map screenshot! ' + downloadMessage += "DOWNLOAD" + Metamaps.GlobalUI.notifyUser(downloadMessage) + + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/maps/' + Metamaps.Active.Map.id + '/upload_screenshot', + data: imageData, + success: function (data) { + console.log('successfully uploaded map screenshot') + }, + error: function () { + console.log('failed to save map screenshot') + } + }) + } +} + +/* + * + * CHEATSHEET + * + */ +Metamaps.Map.CheatSheet = { + init: function () { + // tab the cheatsheet + $('#cheatSheet').tabs() + $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') + $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') + + // id = the id of a vimeo video + var switchVideo = function (element, id) { + $('.tutorialItem').removeClass('active') + $(element).addClass('active') + $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) + } + + $('#gettingStarted').click(function () { + // switchVideo(this,'88334167') + }) + $('#upYourSkillz').click(function () { + // switchVideo(this,'100118167') + }) + $('#advancedMapping').click(function () { + // switchVideo(this,'88334167') + }) + } +}; // end Metamaps.Map.CheatSheet + +/* + * + * INFOBOX + * + */ +Metamaps.Map.InfoBox = { + isOpen: false, + changing: false, + selectingPermission: false, + changePermissionText: "
    As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.
    ", + nameHTML: '{{name}}', + descHTML: '{{desc}}', + init: function () { + var self = Metamaps.Map.InfoBox + + $('.mapInfoIcon').click(self.toggleBox) + $('.mapInfoBox').click(function (event) { + event.stopPropagation() + }) + $('body').click(self.close) + + self.attachEventListeners() + + + + self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) + }, + toggleBox: function (event) { + var self = Metamaps.Map.InfoBox + + if (self.isOpen) self.close() + else self.open() + + event.stopPropagation() + }, + open: function () { + var self = Metamaps.Map.InfoBox + $('.mapInfoIcon div').addClass('hide') + if (!self.isOpen && !self.changing) { + self.changing = true + $('.mapInfoBox').fadeIn(200, function () { + self.changing = false + self.isOpen = true + }) + } + }, + close: function () { + var self = Metamaps.Map.InfoBox + + $('.mapInfoIcon div').removeClass('hide') + if (!self.changing) { + self.changing = true + $('.mapInfoBox').fadeOut(200, function () { + self.changing = false + self.isOpen = false + self.hidePermissionSelect() + $('.mapContributors .tip').hide() + }) + } + }, + load: function () { + var self = Metamaps.Map.InfoBox + + var map = Metamaps.Active.Map + + var obj = map.pick('permission', 'topic_count', 'synapse_count') + + var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) + var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) + var relevantPeople = map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var shareable = map.get('permission') !== 'private' + + obj['name'] = canEdit ? Hogan.compile(self.nameHTML).render({id: map.id, name: map.get('name')}) : map.get('name') + obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc') + obj['map_creator_tip'] = isCreator ? self.changePermissionText : '' + + obj['contributor_count'] = relevantPeople.length + obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : '' + obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : '' + obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : Metamaps.Erb['user.png'] + obj['contributor_list'] = self.createContributorList() + + obj['user_name'] = isCreator ? 'You' : map.get('user_name') + obj['created_at'] = map.get('created_at_clean') + obj['updated_at'] = map.get('updated_at_clean') + + var classes = isCreator ? 'yourMap' : '' + classes += canEdit ? ' canEdit' : '' + classes += shareable ? ' shareable' : '' + $('.mapInfoBox').removeClass('shareable yourMap canEdit') + .addClass(classes) + .html(self.generateBoxHTML.render(obj)) + + self.attachEventListeners() + }, + attachEventListeners: function () { + var self = Metamaps.Map.InfoBox + + $('.mapInfoBox.canEdit .best_in_place').best_in_place() + + // because anyone who can edit the map can change the map title + var bipName = $('.mapInfoBox .best_in_place_name') + bipName.unbind('best_in_place:activate').bind('best_in_place:activate', function () { + var $el = bipName.find('textarea') + var el = $el[0] + + $el.attr('maxlength', '140') + + $('.mapInfoName').append('
    ') + + var callback = function (data) { + $('.nameCounter.forMap').html(data.all + '/140') + } + Countable.live(el, callback) + }) + bipName.unbind('best_in_place:deactivate').bind('best_in_place:deactivate', function () { + $('.nameCounter.forMap').remove() + }) + + $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function () { + var name = $(this).html() + Metamaps.Active.Map.set('name', name) + Metamaps.Active.Map.trigger('saved') + }) + + $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { + var desc = $(this).html() + Metamaps.Active.Map.set('desc', desc) + Metamaps.Active.Map.trigger('saved') + }) + + $('.yourMap .mapPermission').unbind().click(self.onPermissionClick) + // .yourMap in the unbind/bind is just a namespace for the events + // not a reference to the class .yourMap on the .mapInfoBox + $('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect) + + $('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap) + + $('.mapContributors span, #mapContribs').unbind().click(function (event) { + $('.mapContributors .tip').toggle() + event.stopPropagation() + }) + $('.mapContributors .tip').unbind().click(function (event) { + event.stopPropagation() + }) + $('.mapContributors .tip li a').click(Metamaps.Router.intercept) + + $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { + $('.mapContributors .tip').hide() + }) + + self.addTypeahead() + }, + addTypeahead: function () { + var self = Metamaps.Map.InfoBox + + if (!Metamaps.Active.Map) return + + // for autocomplete + var collaborators = { + name: 'collaborators', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult", + profile: Metamaps.Erb['user.png'], + }); + }, + suggestion: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY', + }, + }) + } + + // for adding map collaborators, who will have edit rights + if (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) { + $('.collaboratorSearchField').typeahead( + { + highlight: false, + }, + [collaborators] + ) + $('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick) + $('.mapContributors .removeCollaborator').click(function () { + self.removeCollaborator(parseInt($(this).data('id'))) + }) + } + }, + removeCollaborator: function (collaboratorId) { + var self = Metamaps.Map.InfoBox + Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) + var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + self.updateNumbers() + }, + addCollaborator: function (newCollaboratorId) { + var self = Metamaps.Map.InfoBox + + if (Metamaps.Collaborators.get(newCollaboratorId)) { + Metamaps.GlobalUI.notifyUser('That user already has access') + return + } + + function callback(mapper) { + Metamaps.Collaborators.add(mapper) + var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + Metamaps.GlobalUI.notifyUser(name + ' will be notified by email') + self.updateNumbers() + } + + $.getJSON('/users/' + newCollaboratorId + '.json', callback) + }, + handleResultClick: function (event, item) { + var self = Metamaps.Map.InfoBox + + self.addCollaborator(item.id) + $('.collaboratorSearchField').typeahead('val', '') + }, + updateNameDescPerm: function (name, desc, perm) { + $('.mapInfoName .best_in_place_name').html(name) + $('.mapInfoDesc .best_in_place_desc').html(desc) + $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm) + }, + createContributorList: function () { + var self = Metamaps.Map.InfoBox + var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var activeMapperIsCreator = Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id') + var string = '' + string += '' + + if (activeMapperIsCreator) { + string += '
    ' + } + return string + }, + updateNumbers: function () { + var self = Metamaps.Map.InfoBox + var mapper = Metamaps.Active.Mapper + var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + + var contributors_class = '' + if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' + else if (relevantPeople.length > 2) contributors_class = 'multiple' + + var contributors_image = Metamaps.Erb['user.png'] + if (relevantPeople.length > 0) { + // get the first contributor and use their image + contributors_image = relevantPeople.models[0].get('image') + } + $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class) + $('.mapContributors span').text(relevantPeople.length) + $('.mapContributors .tip').html(self.createContributorList()) + self.addTypeahead() + $('.mapContributors .tip').unbind().click(function (event) { + event.stopPropagation() + }) + $('.mapTopics').text(Metamaps.Topics.length) + $('.mapSynapses').text(Metamaps.Synapses.length) + + $('.mapEditedAt').html('Last edited: ' + Metamaps.Util.nowDateFormatted()) + }, + onPermissionClick: function (event) { + var self = Metamaps.Map.InfoBox + + if (!self.selectingPermission) { + self.selectingPermission = true + $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow + if ($(this).hasClass('commons')) { + $(this).append('') + } else if ($(this).hasClass('public')) { + $(this).append('') + } else if ($(this).hasClass('private')) { + $(this).append('') + } + $('.mapPermission .permissionSelect li').click(self.selectPermission) + event.stopPropagation() + } + }, + hidePermissionSelect: function () { + var self = Metamaps.Map.InfoBox + + self.selectingPermission = false + $('.mapPermission').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow + $('.mapPermission .permissionSelect').remove() + }, + selectPermission: function (event) { + var self = Metamaps.Map.InfoBox + + self.selectingPermission = false + var permission = $(this).attr('class') + Metamaps.Active.Map.save({ + permission: permission + }) + Metamaps.Active.Map.updateMapWrapper() + shareable = permission === 'private' ? '' : 'shareable' + $('.mapPermission').removeClass('commons public private minimize').addClass(permission) + $('.mapPermission .permissionSelect').remove() + $('.mapInfoBox').removeClass('shareable').addClass(shareable) + event.stopPropagation() + }, + deleteActiveMap: function () { + var confirmString = 'Are you sure you want to delete this map? ' + confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' + + var doIt = confirm(confirmString) + var map = Metamaps.Active.Map + var mapper = Metamaps.Active.Mapper + var authorized = map.authorizePermissionChange(mapper) + + if (doIt && authorized) { + Metamaps.Map.InfoBox.close() + Metamaps.Maps.Active.remove(map) + Metamaps.Maps.Featured.remove(map) + Metamaps.Maps.Mine.remove(map) + Metamaps.Maps.Shared.remove(map) + map.destroy() + Metamaps.Router.home() + Metamaps.GlobalUI.notifyUser('Map eliminated!') + } + else if (!authorized) { + alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") + } + } +}; // end Metamaps.Map.InfoBox diff --git a/app/assets/javascripts/src/Metamaps.Mapper.js b/app/assets/javascripts/src/Metamaps.Mapper.js new file mode 100644 index 00000000..7d565479 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Mapper.js @@ -0,0 +1,20 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Mapper.js.erb + * + * Dependencies: none! + */ + +Metamaps.Mapper = { + // this function is to retrieve a mapper JSON object from the database + // @param id = the id of the mapper to retrieve + get: function (id, callback) { + return $.ajax({ + url: '/users/' + id + '.json', + success: function (data) { + callback(new Metamaps.Backbone.Mapper(data)) + } + }) + } +}; // end Metamaps.Mapper diff --git a/app/assets/javascripts/src/Metamaps.Organize.js b/app/assets/javascripts/src/Metamaps.Organize.js new file mode 100644 index 00000000..b2463280 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Organize.js @@ -0,0 +1,117 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Organize.js.erb + * + * Dependencies: + * - Metamaps.Visualize + */ +Metamaps.Organize = { + init: function () {}, + arrange: function (layout, centerNode) { + // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin + if (layout == 'grid') { + var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization + var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid + var GRIDSPACE = 400 + var row = 0 + var column = 0 + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + if (column == numColumns) { + column = 0 + row += 1 + } + var newPos = new $jit.Complex() + newPos.x = column * GRIDSPACE + newPos.y = row * GRIDSPACE + n.setPos(newPos, 'end') + column += 1 + }) + Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + } else if (layout == 'grid_full') { + // this will always be an integer, the # of nodes on your graph visualization + var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes) + // var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid + // var GRIDSPACE = 400 + var height = Metamaps.Visualize.mGraph.canvas.getSize(0).height + var width = Metamaps.Visualize.mGraph.canvas.getSize(0).width + var totalArea = height * width + var cellArea = totalArea / numNodes + var ratio = height / width + var cellWidth = sqrt(cellArea / ratio) + var cellHeight = cellArea / cellWidth + var row = floor(height / cellHeight) + var column = floor(width / cellWidth) + var totalCells = row * column + + if (totalCells) + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + if (column == numColumns) { + column = 0 + row += 1 + } + var newPos = new $jit.Complex() + newPos.x = column * GRIDSPACE + newPos.y = row * GRIDSPACE + n.setPos(newPos, 'end') + column += 1 + }) + Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + } else if (layout == 'radial') { + var centerX = centerNode.getPos().x + var centerY = centerNode.getPos().y + centerNode.setPos(centerNode.getPos(), 'end') + + console.log(centerNode.adjacencies) + var lineLength = 200 + var usedNodes = {} + usedNodes[centerNode.id] = centerNode + var radial = function (node, level, degree) { + if (level == 1) { + var numLinksTemp = _.size(node.adjacencies) + var angleTemp = 2 * Math.PI / numLinksTemp + } else { + angleTemp = 2 * Math.PI / 20 + } + node.eachAdjacency(function (a) { + var isSecondLevelNode = (centerNode.adjacencies[a.nodeTo.id] != undefined && level > 1) + if (usedNodes[a.nodeTo.id] == undefined && !isSecondLevelNode) { + var newPos = new $jit.Complex() + newPos.x = level * lineLength * Math.sin(degree) + centerX + newPos.y = level * lineLength * Math.cos(degree) + centerY + a.nodeTo.setPos(newPos, 'end') + usedNodes[a.nodeTo.id] = a.nodeTo + + radial(a.nodeTo, level + 1, degree) + degree += angleTemp + } + }) + } + radial(centerNode, 1, 0) + Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + } else if (layout == 'center_viewport') { + var lowX = 0, + lowY = 0, + highX = 0, + highY = 0 + var oldOriginX = Metamaps.Visualize.mGraph.canvas.translateOffsetX + var oldOriginY = Metamaps.Visualize.mGraph.canvas.translateOffsetY + + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + if (n.id === 1) { + lowX = n.getPos().x + lowY = n.getPos().y + highX = n.getPos().x + highY = n.getPos().y + } + if (n.getPos().x < lowX) lowX = n.getPos().x + if (n.getPos().y < lowY) lowY = n.getPos().y + if (n.getPos().x > highX) highX = n.getPos().x + if (n.getPos().y > highY) highY = n.getPos().y + }) + console.log(lowX, lowY, highX, highY) + var newOriginX = (lowX + highX) / 2 + var newOriginY = (lowY + highY) / 2 + } else alert('please call function with a valid layout dammit!') + } +}; // end Metamaps.Organize diff --git a/app/assets/javascripts/src/Metamaps.Realtime.js b/app/assets/javascripts/src/Metamaps.Realtime.js new file mode 100644 index 00000000..b43a9b96 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Realtime.js @@ -0,0 +1,1199 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Realtime.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Backbone + * - Metamaps.Backbone + * - Metamaps.Control + * - Metamaps.Erb + * - Metamaps.GlobalUI + * - Metamaps.JIT + * - Metamaps.Map + * - Metamaps.Mapper + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Messages + * - Metamaps.Synapses + * - Metamaps.Topic + * - Metamaps.Topics + * - Metamaps.Util + * - Metamaps.Views + * - Metamaps.Visualize + */ + +Metamaps.Realtime = { + videoId: 'video-wrapper', + socket: null, + webrtc: null, + readyToCall: false, + mappersOnMap: {}, + disconnected: false, + chatOpen: false, + status: true, // stores whether realtime is True/On or False/Off, + broadcastingStatus: false, + inConversation: false, + localVideo: null, + init: function () { + var self = Metamaps.Realtime + + self.addJuntoListeners() + + self.socket = new SocketIoConnection({ url: Metamaps.Erb['REALTIME_SERVER']}) + self.socket.on('connect', function () { + console.log('connected') + if (!self.disconnected) { + self.startActiveMap() + } else self.disconnected = false + }) + self.socket.on('disconnect', function () { + self.disconnected = true + }) + + if (Metamaps.Active.Mapper) { + self.webrtc = new SimpleWebRTC({ + connection: self.socket, + localVideoEl: self.videoId, + remoteVideosEl: '', + detectSpeakingEvents: true, + autoAdjustMic: false, // true, + autoRequestMedia: false, + localVideo: { + autoplay: true, + mirror: true, + muted: true + }, + media: { + video: true, + audio: true + }, + nick: Metamaps.Active.Mapper.id + }) + + var $video = $('').attr('id', self.videoId) + self.localVideo = { + $video: $video, + view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { + DOUBLE_CLICK_TOLERANCE: 200, + avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' + }) + } + + self.room = new Metamaps.Views.room({ + webrtc: self.webrtc, + socket: self.socket, + username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', + image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', + room: 'global', + $video: self.localVideo.$video, + myVideoView: self.localVideo.view, + config: { DOUBLE_CLICK_TOLERANCE: 200 } + }) + self.room.videoAdded(self.handleVideoAdded) + + self.room.chat.$container.hide() + $('body').prepend(self.room.chat.$container) + } // if Metamaps.Active.Mapper + }, + addJuntoListeners: function () { + var self = Metamaps.Realtime + + $(document).on(Metamaps.Views.chatView.events.openTray, function () { + $('.main').addClass('compressed') + self.chatOpen = true + self.positionPeerIcons() + }) + $(document).on(Metamaps.Views.chatView.events.closeTray, function () { + $('.main').removeClass('compressed') + self.chatOpen = false + self.positionPeerIcons() + }) + $(document).on(Metamaps.Views.chatView.events.videosOn, function () { + $('#wrapper').removeClass('hideVideos') + }) + $(document).on(Metamaps.Views.chatView.events.videosOff, function () { + $('#wrapper').addClass('hideVideos') + }) + $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { + $('#wrapper').removeClass('hideCursors') + }) + $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { + $('#wrapper').addClass('hideCursors') + }) + }, + handleVideoAdded: function (v, id) { + var self = Metamaps.Realtime + self.positionVideos() + v.setParent($('#wrapper')) + v.$container.find('.video-cutoff').css({ + border: '4px solid ' + self.mappersOnMap[id].color + }) + $('#wrapper').append(v.$container) + }, + positionVideos: function () { + var self = Metamaps.Realtime + var videoIds = Object.keys(self.room.videos) + var numOfVideos = videoIds.length + var numOfVideosToPosition = _.filter(videoIds, function (id) { + return !self.room.videos[id].manuallyPositioned + }).length + + var screenHeight = $(document).height() + var screenWidth = $(document).width() + var topExtraPadding = 20 + var topPadding = 30 + var leftPadding = 30 + var videoHeight = 150 + var videoWidth = 180 + var column = 0 + var row = 0 + var yFormula = function () { + var y = topExtraPadding + (topPadding + videoHeight) * row + topPadding + if (y + videoHeight > screenHeight) { + row = 0 + column += 1 + y = yFormula() + } + row++ + return y + } + var xFormula = function () { + var x = (leftPadding + videoWidth) * column + leftPadding + return x + } + + // do self first + var myVideo = Metamaps.Realtime.localVideo.view + if (!myVideo.manuallyPositioned) { + myVideo.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }) + } + videoIds.forEach(function (id) { + var video = self.room.videos[id] + if (!video.manuallyPositioned) { + video.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }) + } + }) + }, + startActiveMap: function () { + var self = Metamaps.Realtime + + if (Metamaps.Active.Map && Metamaps.Active.Mapper) { + if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { + self.turnOn() + self.setupSocket() + } else { + self.attachMapListener() + } + self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true) + } + }, + endActiveMap: function () { + var self = Metamaps.Realtime + + $(document).off('mousemove') + self.socket.removeAllListeners() + if (self.inConversation) self.leaveCall() + self.socket.emit('endMapperNotify') + $('.collabCompass').remove() + self.status = false + self.room.leave() + self.room.chat.$container.hide() + self.room.chat.close() + }, + turnOn: function (notify) { + var self = Metamaps.Realtime + + if (notify) self.sendRealtimeOn() + self.status = true + $('.collabCompass').show() + self.room.chat.$container.show() + self.room.room = 'map-' + Metamaps.Active.Map.id + self.checkForACallToJoin() + + self.activeMapper = { + id: Metamaps.Active.Mapper.id, + name: Metamaps.Active.Mapper.get('name'), + username: Metamaps.Active.Mapper.get('name'), + image: Metamaps.Active.Mapper.get('image'), + color: Metamaps.Util.getPastelColor(), + self: true + } + self.localVideo.view.$container.find('.video-cutoff').css({ + border: '4px solid ' + self.activeMapper.color + }) + self.room.chat.addParticipant(self.activeMapper) + }, + checkForACallToJoin: function () { + var self = Metamaps.Realtime + self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) + }, + promptToJoin: function () { + var self = Metamaps.Realtime + + var notifyText = "There's a conversation happening, want to join?" + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + self.room.conversationInProgress() + }, + conversationHasBegun: function () { + var self = Metamaps.Realtime + + if (self.inConversation) return + var notifyText = "There's a conversation starting, want to join?" + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + self.room.conversationInProgress() + }, + countOthersInConversation: function () { + var self = Metamaps.Realtime + var count = 0 + + for (var key in self.mappersOnMap) { + if (self.mappersOnMap[key].inConversation) count++ + } + return count + }, + mapperJoinedCall: function (id) { + var self = Metamaps.Realtime + var mapper = self.mappersOnMap[id] + + if (mapper) { + if (self.inConversation) { + var username = mapper.name + var notifyText = username + ' joined the call' + Metamaps.GlobalUI.notifyUser(notifyText) + } + + mapper.inConversation = true + self.room.chat.mapperJoinedCall(id) + } + }, + mapperLeftCall: function (id) { + var self = Metamaps.Realtime + var mapper = self.mappersOnMap[id] + + if (mapper) { + if (self.inConversation) { + var username = mapper.name + var notifyText = username + ' left the call' + Metamaps.GlobalUI.notifyUser(notifyText) + } + + mapper.inConversation = false + self.room.chat.mapperLeftCall(id) + + if ((self.inConversation && self.countOthersInConversation() === 0) || + (!self.inConversation && self.countOthersInConversation() === 1)) { + self.callEnded() + } + } + }, + callEnded: function () { + var self = Metamaps.Realtime + + self.room.conversationEnding() + self.room.leaveVideoOnly() + self.inConversation = false + self.localVideo.view.$container.hide().css({ + top: '72px', + left: '30px' + }) + self.localVideo.view.audioOn() + self.localVideo.view.videoOn() + self.webrtc.webrtc.localStreams.forEach(function (stream) { + stream.getTracks().forEach(function (track) { + track.stop() + }) + }) + self.webrtc.webrtc.localStreams = [] + }, + invitedToCall: function (inviter) { + var self = Metamaps.Realtime + + self.room.chat.sound.stop('sessioninvite') + self.room.chat.sound.play('sessioninvite') + + var username = self.mappersOnMap[inviter].name + var notifyText = '' + notifyText += username + ' is inviting you to a conversation. Join live?' + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + }, + invitedToJoin: function (inviter) { + var self = Metamaps.Realtime + + self.room.chat.sound.stop('sessioninvite') + self.room.chat.sound.play('sessioninvite') + + var username = self.mappersOnMap[inviter].name + var notifyText = username + ' is inviting you to the conversation. Join?' + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + }, + acceptCall: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('callAccepted', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation') + self.joinCall() + Metamaps.GlobalUI.clearNotify() + }, + denyCall: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('callDenied', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + Metamaps.GlobalUI.clearNotify() + }, + denyInvite: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('inviteDenied', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + Metamaps.GlobalUI.clearNotify() + }, + inviteACall: function (userid) { + var self = Metamaps.Realtime + self.socket.emit('inviteACall', { + mapid: Metamaps.Active.Map.id, + inviter: Metamaps.Active.Mapper.id, + invited: userid + }) + self.room.chat.invitationPending(userid) + Metamaps.GlobalUI.clearNotify() + }, + inviteToJoin: function (userid) { + var self = Metamaps.Realtime + self.socket.emit('inviteToJoin', { + mapid: Metamaps.Active.Map.id, + inviter: Metamaps.Active.Mapper.id, + invited: userid + }) + self.room.chat.invitationPending(userid) + }, + callAccepted: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser('Conversation starting...') + self.joinCall() + self.room.chat.invitationAnswered(userid) + }, + callDenied: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + self.room.chat.invitationAnswered(userid) + }, + inviteDenied: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + self.room.chat.invitationAnswered(userid) + }, + joinCall: function () { + var self = Metamaps.Realtime + + self.webrtc.off('readyToCall') + self.webrtc.once('readyToCall', function () { + self.videoInitialized = true + self.readyToCall = true + self.localVideo.view.manuallyPositioned = false + self.positionVideos() + self.localVideo.view.$container.show() + if (self.localVideo && self.status) { + $('#wrapper').append(self.localVideo.view.$container) + } + self.room.join() + }) + self.inConversation = true + self.socket.emit('mapperJoinedCall', { + mapid: Metamaps.Active.Map.id, + id: Metamaps.Active.Mapper.id + }) + self.webrtc.startLocalVideo() + Metamaps.GlobalUI.clearNotify() + self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) + }, + leaveCall: function () { + var self = Metamaps.Realtime + + self.socket.emit('mapperLeftCall', { + mapid: Metamaps.Active.Map.id, + id: Metamaps.Active.Mapper.id + }) + + self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id) + self.room.leaveVideoOnly() + self.inConversation = false + self.localVideo.view.$container.hide() + + // if there's only two people in the room, and we're leaving + // we should shut down the call locally + if (self.countOthersInConversation() === 1) { + self.callEnded() + } + }, + turnOff: function (silent) { + var self = Metamaps.Realtime + + if (self.status) { + if (!silent) self.sendRealtimeOff() + // $(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff') + // $('.rtOn').removeClass('active') + // $('.rtOff').addClass('active') + self.status = false + // $(".sidebarCollaborateIcon").removeClass("blue") + $('.collabCompass').hide() + $('#' + self.videoId).remove() + } + }, + setupSocket: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + var myId = Metamaps.Active.Mapper.id + + socket.emit('newMapperNotify', { + userid: myId, + username: Metamaps.Active.Mapper.get('name'), + userimage: Metamaps.Active.Mapper.get('image'), + mapid: Metamaps.Active.Map.id + }) + + socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall) // new call + socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress + socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted) + socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied) + socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied) + + // receive word that there's a conversation in progress + socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin) + socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun) + + socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) + socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) + + // if you're the 'new guy' update your list with who's already online + socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList) + + // receive word that there's a new mapper on the map + socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap) + + // receive word that a mapper left the map + socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap) + + // receive word that there's a mapper turned on realtime + socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator) + + // receive word that there's a mapper turned on realtime + socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse) + + // update mapper compass position + socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) + + // deletions + socket.on('deleteTopicFromServer', self.removeTopic) + socket.on('deleteSynapseFromServer', self.removeSynapse) + + socket.on('topicChangeFromServer', self.topicChange) + socket.on('synapseChangeFromServer', self.synapseChange) + self.attachMapListener() + + // local event listeners that trigger events + var sendCoords = function (event) { + var pixels = { + x: event.pageX, + y: event.pageY + } + var coords = Metamaps.Util.pixelsToCoords(pixels) + self.sendCoords(coords) + } + $(document).mousemove(sendCoords) + + var zoom = function (event, e) { + if (e) { + var pixels = { + x: e.pageX, + y: e.pageY + } + var coords = Metamaps.Util.pixelsToCoords(pixels) + self.sendCoords(coords) + } + self.positionPeerIcons() + } + $(document).on(Metamaps.JIT.events.zoom, zoom) + + $(document).on(Metamaps.JIT.events.pan, self.positionPeerIcons) + + var sendTopicDrag = function (event, positions) { + self.sendTopicDrag(positions) + } + $(document).on(Metamaps.JIT.events.topicDrag, sendTopicDrag) + + var sendNewTopic = function (event, data) { + self.sendNewTopic(data) + } + $(document).on(Metamaps.JIT.events.newTopic, sendNewTopic) + + var sendDeleteTopic = function (event, data) { + self.sendDeleteTopic(data) + } + $(document).on(Metamaps.JIT.events.deleteTopic, sendDeleteTopic) + + var sendRemoveTopic = function (event, data) { + self.sendRemoveTopic(data) + } + $(document).on(Metamaps.JIT.events.removeTopic, sendRemoveTopic) + + var sendNewSynapse = function (event, data) { + self.sendNewSynapse(data) + } + $(document).on(Metamaps.JIT.events.newSynapse, sendNewSynapse) + + var sendDeleteSynapse = function (event, data) { + self.sendDeleteSynapse(data) + } + $(document).on(Metamaps.JIT.events.deleteSynapse, sendDeleteSynapse) + + var sendRemoveSynapse = function (event, data) { + self.sendRemoveSynapse(data) + } + $(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse) + + var sendNewMessage = function (event, data) { + self.sendNewMessage(data) + } + $(document).on(Metamaps.Views.room.events.newMessage, sendNewMessage) + }, + attachMapListener: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + socket.on('mapChangeFromServer', self.mapChange) + }, + sendRealtimeOn: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // send this new mapper back your details, and the awareness that you're online + var update = { + username: Metamaps.Active.Mapper.get('name'), + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('notifyStartRealtime', update) + }, + sendRealtimeOff: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // send this new mapper back your details, and the awareness that you're online + var update = { + username: Metamaps.Active.Mapper.get('name'), + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('notifyStopRealtime', update) + }, + updateMapperList: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + // data.userimage + // data.userrealtime + + self.mappersOnMap[data.userid] = { + id: data.userid, + name: data.username, + username: data.username, + image: data.userimage, + color: Metamaps.Util.getPastelColor(), + realtime: data.userrealtime, + inConversation: data.userinconversation, + coords: { + x: 0, + y: 0 + } + } + + if (data.userid !== Metamaps.Active.Mapper.id) { + self.room.chat.addParticipant(self.mappersOnMap[data.userid]) + if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) + + // create a div for the collaborators compass + self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status) + } + }, + newPeerOnMap: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + // data.userimage + // data.coords + var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0 + + self.mappersOnMap[data.userid] = { + id: data.userid, + name: data.username, + username: data.username, + image: data.userimage, + color: Metamaps.Util.getPastelColor(), + realtime: true, + coords: { + x: 0, + y: 0 + }, + } + + // create an item for them in the realtime box + if (data.userid !== Metamaps.Active.Mapper.id && self.status) { + self.room.chat.sound.play('joinmap') + self.room.chat.addParticipant(self.mappersOnMap[data.userid]) + + // create a div for the collaborators compass + self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status) + + var notifyMessage = data.username + ' just joined the map' + if (firstOtherPerson) { + notifyMessage += ' ' + } + Metamaps.GlobalUI.notifyUser(notifyMessage) + + // send this new mapper back your details, and the awareness that you've loaded the map + var update = { + userToNotify: data.userid, + username: Metamaps.Active.Mapper.get('name'), + userimage: Metamaps.Active.Mapper.get('image'), + userid: Metamaps.Active.Mapper.id, + userrealtime: self.status, + userinconversation: self.inConversation, + mapid: Metamaps.Active.Map.id + } + socket.emit('updateNewMapperList', update) + } + }, + createCompass: function (name, id, image, color, hide) { + var str = '

    ' + name + '

    ' + str += '
    ' + $('#compass' + id).remove() + $('
    ', { + id: 'compass' + id, + class: 'collabCompass' + }).html(str).appendTo('#wrapper') + if (hide) { + $('#compass' + id).hide() + } + $('#compass' + id + ' img').css({ + 'border': '2px solid ' + color + }) + $('#compass' + id + ' p').css({ + 'background-color': color + }) + }, + lostPeerOnMap: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + delete self.mappersOnMap[data.userid] + self.room.chat.sound.play('leavemap') + // $('#mapper' + data.userid).remove() + $('#compass' + data.userid).remove() + self.room.chat.removeParticipant(data.username) + + Metamaps.GlobalUI.notifyUser(data.username + ' just left the map') + + if ((self.inConversation && self.countOthersInConversation() === 0) || + (!self.inConversation && self.countOthersInConversation() === 1)) { + self.callEnded() + } + }, + newCollaborator: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + self.mappersOnMap[data.userid].realtime = true + + // $('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn') + $('#compass' + data.userid).show() + + Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') + }, + lostCollaborator: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + self.mappersOnMap[data.userid].realtime = false + + // $('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff') + $('#compass' + data.userid).hide() + + Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') + }, + updatePeerCoords: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + self.mappersOnMap[data.userid].coords = {x: data.usercoords.x,y: data.usercoords.y} + self.positionPeerIcon(data.userid) + }, + positionPeerIcons: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + if (self.status) { // if i have realtime turned on + for (var key in self.mappersOnMap) { + var mapper = self.mappersOnMap[key] + if (mapper.realtime) { + self.positionPeerIcon(key) + } + } + } + }, + positionPeerIcon: function (id) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var boundary = self.chatOpen ? '#wrapper' : document + var mapper = self.mappersOnMap[id] + var xMax = $(boundary).width() + var yMax = $(boundary).height() + var compassDiameter = 56 + var compassArrowSize = 24 + + var origPixels = Metamaps.Util.coordsToPixels(mapper.coords) + var pixels = self.limitPixelsToScreen(origPixels) + $('#compass' + id).css({ + left: pixels.x + 'px', + top: pixels.y + 'px' + }) + /* showing the arrow if the collaborator is off of the viewport screen */ + if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { + var dy = origPixels.y - pixels.y // opposite + var dx = origPixels.x - pixels.x // adjacent + var ratio = dy / dx + var angle = Math.atan2(dy, dx) + + $('#compassArrow' + id).show().css({ + transform: 'rotate(' + angle + 'rad)', + '-webkit-transform': 'rotate(' + angle + 'rad)', + }) + + if (dx > 0) { + $('#compass' + id).addClass('labelLeft') + } + } else { + $('#compassArrow' + id).hide() + $('#compass' + id).removeClass('labelLeft') + } + }, + limitPixelsToScreen: function (pixels) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var boundary = self.chatOpen ? '#wrapper' : document + var xLimit, yLimit + var xMax = $(boundary).width() + var yMax = $(boundary).height() + var compassDiameter = 56 + var compassArrowSize = 24 + + xLimit = Math.max(0 + compassArrowSize, pixels.x) + xLimit = Math.min(xLimit, xMax - compassDiameter) + yLimit = Math.max(0 + compassArrowSize, pixels.y) + yLimit = Math.min(yLimit, yMax - compassDiameter) + + return {x: xLimit,y: yLimit} + }, + sendCoords: function (coords) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var map = Metamaps.Active.Map + var mapper = Metamaps.Active.Mapper + + if (self.status && map.authorizeToEdit(mapper) && socket) { + var update = { + usercoords: coords, + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('updateMapperCoords', update) + } + }, + sendTopicDrag: function (positions) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map && self.status) { + positions.mapid = Metamaps.Active.Map.id + socket.emit('topicDrag', positions) + } + }, + topicDrag: function (positions) { + var self = Metamaps.Realtime + var socket = self.socket + + var topic + var node + + if (Metamaps.Active.Map && self.status) { + for (var key in positions) { + topic = Metamaps.Topics.get(key) + if (topic) node = topic.get('node') + if (node) node.pos.setc(positions[key].x, positions[key].y) + } // for + Metamaps.Visualize.mGraph.plot() + } + }, + sendTopicChange: function (topic) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + topicId: topic.id + } + + socket.emit('topicChangeFromClient', data) + }, + topicChange: function (data) { + var topic = Metamaps.Topics.get(data.topicId) + if (topic) { + var node = topic.get('node') + topic.fetch({ + success: function (model) { + model.set({ node: node }) + model.trigger('changeByOther') + } + }) + } + }, + sendSynapseChange: function (synapse) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + synapseId: synapse.id + } + + socket.emit('synapseChangeFromClient', data) + }, + synapseChange: function (data) { + var synapse = Metamaps.Synapses.get(data.synapseId) + if (synapse) { + // edge reset necessary because fetch causes model reset + var edge = synapse.get('edge') + synapse.fetch({ + success: function (model) { + model.set({ edge: edge }) + model.trigger('changeByOther') + } + }) + } + }, + sendMapChange: function (map) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + mapId: map.id + } + + socket.emit('mapChangeFromClient', data) + }, + mapChange: function (data) { + var map = Metamaps.Active.Map + var isActiveMap = map && data.mapId === map.id + if (isActiveMap) { + var couldEditBefore = map.authorizeToEdit(Metamaps.Active.Mapper) + var idBefore = map.id + map.fetch({ + success: function (model, response) { + var idNow = model.id + var canEditNow = model.authorizeToEdit(Metamaps.Active.Mapper) + if (idNow !== idBefore) { + Metamaps.Map.leavePrivateMap() // this means the map has been changed to private + } + else if (couldEditBefore && !canEditNow) { + Metamaps.Map.cantEditNow() + } + else if (!couldEditBefore && canEditNow) { + Metamaps.Map.canEditNow() + } else { + model.fetchContained() + model.trigger('changeByOther') + } + } + }) + } + }, + // newMessage + sendNewMessage: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + var message = data.attributes + message.mapid = Metamaps.Active.Map.id + socket.emit('newMessage', message) + }, + newMessage: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)) + }, + // newTopic + sendNewTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map && self.status) { + data.mapperid = Metamaps.Active.Mapper.id + data.mapid = Metamaps.Active.Map.id + socket.emit('newTopic', data) + } + }, + newTopic: function (data) { + var topic, mapping, mapper, mapperCallback, cancel + + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + function waitThenRenderTopic () { + if (topic && mapping && mapper) { + Metamaps.Topic.renderTopic(mapping, topic, false, false) + } + else if (!cancel) { + setTimeout(waitThenRenderTopic, 10) + } + } + + mapper = Metamaps.Mappers.get(data.mapperid) + if (mapper === undefined) { + mapperCallback = function (m) { + Metamaps.Mappers.add(m) + mapper = m + } + Metamaps.Mapper.get(data.mapperid, mapperCallback) + } + $.ajax({ + url: '/topics/' + data.mappableid + '.json', + success: function (response) { + Metamaps.Topics.add(response) + topic = Metamaps.Topics.get(response.id) + }, + error: function () { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function (response) { + Metamaps.Mappings.add(response) + mapping = Metamaps.Mappings.get(response.id) + }, + error: function () { + cancel = true + } + }) + + waitThenRenderTopic() + }, + // removeTopic + sendDeleteTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + socket.emit('deleteTopicFromClient', data) + } + }, + // removeTopic + sendRemoveTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('removeTopic', data) + } + }, + removeTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + var topic = Metamaps.Topics.get(data.mappableid) + if (topic) { + var node = topic.get('node') + var mapping = topic.getMapping() + Metamaps.Control.hideNode(node.id) + Metamaps.Topics.remove(topic) + Metamaps.Mappings.remove(mapping) + } + }, + // newSynapse + sendNewSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapperid = Metamaps.Active.Mapper.id + data.mapid = Metamaps.Active.Map.id + socket.emit('newSynapse', data) + } + }, + newSynapse: function (data) { + var topic1, topic2, node1, node2, synapse, mapping, cancel + + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + function waitThenRenderSynapse () { + if (synapse && mapping && mapper) { + topic1 = synapse.getTopic1() + node1 = topic1.get('node') + topic2 = synapse.getTopic2() + node2 = topic2.get('node') + + Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false) + } + else if (!cancel) { + setTimeout(waitThenRenderSynapse, 10) + } + } + + mapper = Metamaps.Mappers.get(data.mapperid) + if (mapper === undefined) { + mapperCallback = function (m) { + Metamaps.Mappers.add(m) + mapper = m + } + Metamaps.Mapper.get(data.mapperid, mapperCallback) + } + $.ajax({ + url: '/synapses/' + data.mappableid + '.json', + success: function (response) { + Metamaps.Synapses.add(response) + synapse = Metamaps.Synapses.get(response.id) + }, + error: function () { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function (response) { + Metamaps.Mappings.add(response) + mapping = Metamaps.Mappings.get(response.id) + }, + error: function () { + cancel = true + } + }) + waitThenRenderSynapse() + }, + // deleteSynapse + sendDeleteSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('deleteSynapseFromClient', data) + } + }, + // removeSynapse + sendRemoveSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('removeSynapse', data) + } + }, + removeSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + var synapse = Metamaps.Synapses.get(data.mappableid) + if (synapse) { + var edge = synapse.get('edge') + var mapping = synapse.getMapping() + if (edge.getData('mappings').length - 1 === 0) { + Metamaps.Control.hideEdge(edge) + } + + var index = _.indexOf(edge.getData('synapses'), synapse) + edge.getData('mappings').splice(index, 1) + edge.getData('synapses').splice(index, 1) + if (edge.getData('displayIndex')) { + delete edge.data.$displayIndex + } + Metamaps.Synapses.remove(synapse) + Metamaps.Mappings.remove(mapping) + } + }, +}; // end Metamaps.Realtime diff --git a/app/assets/javascripts/src/Metamaps.Router.js.erb b/app/assets/javascripts/src/Metamaps.Router.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Router.js.erb rename to app/assets/javascripts/src/Metamaps.Router.js index 6f673b61..5e574637 100644 --- a/app/assets/javascripts/src/Metamaps.Router.js.erb +++ b/app/assets/javascripts/src/Metamaps.Router.js @@ -87,7 +87,7 @@ // either 'featured', 'mapper', or 'active' var capitalize = section.charAt(0).toUpperCase() + section.slice(1) - if (section === 'featured' || section === 'active') { + if (section === 'shared' || section === 'featured' || section === 'active') { document.title = 'Explore ' + capitalize + ' Maps | Metamaps' } else if (section === 'mapper') { $.ajax({ diff --git a/app/assets/javascripts/src/Metamaps.Synapse.js b/app/assets/javascripts/src/Metamaps.Synapse.js new file mode 100644 index 00000000..ceed219d --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Synapse.js @@ -0,0 +1,169 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Synapse.js.erb + * + * Dependencies: + * - Metamaps.Backbone + * - Metamaps.Control + * - Metamaps.Create + * - Metamaps.JIT + * - Metamaps.Map + * - Metamaps.Mappings + * - Metamaps.Selected + * - Metamaps.Settings + * - Metamaps.Synapses + * - Metamaps.Topics + * - Metamaps.Visualize + */ + +Metamaps.Synapse = { + // this function is to retrieve a synapse JSON object from the database + // @param id = the id of the synapse to retrieve + get: function (id, callback) { + // if the desired topic is not yet in the local topic repository, fetch it + if (Metamaps.Synapses.get(id) == undefined) { + if (!callback) { + var e = $.ajax({ + url: '/synapses/' + id + '.json', + async: false + }) + Metamaps.Synapses.add($.parseJSON(e.responseText)) + return Metamaps.Synapses.get(id) + } else { + return $.ajax({ + url: '/synapses/' + id + '.json', + success: function (data) { + Metamaps.Synapses.add(data) + callback(Metamaps.Synapses.get(id)) + } + }) + } + } else { + if (!callback) { + return Metamaps.Synapses.get(id) + } else { + return callback(Metamaps.Synapses.get(id)) + } + } + }, + /* + * + * + */ + renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { + var self = Metamaps.Synapse + + var edgeOnViz + + var newedge = synapse.createEdge(mapping) + + Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data) + edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id) + synapse.set('edge', edgeOnViz) + synapse.updateEdge() // links the synapse and the mapping to the edge + + Metamaps.Control.selectEdge(edgeOnViz) + + var mappingSuccessCallback = function (mappingModel, response) { + var newSynapseData = { + mappingid: mappingModel.id, + mappableid: mappingModel.get('mappable_id') + } + + $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + } + var synapseSuccessCallback = function (synapseModel, response) { + if (Metamaps.Active.Map) { + mapping.save({ mappable_id: synapseModel.id }, { + success: mappingSuccessCallback + }) + } + } + + if (!Metamaps.Settings.sandbox && createNewInDB) { + if (synapse.isNew()) { + synapse.save(null, { + success: synapseSuccessCallback, + error: function (model, response) { + console.log('error saving synapse to database') + } + }) + } else if (!synapse.isNew() && Metamaps.Active.Map) { + mapping.save(null, { + success: mappingSuccessCallback + }) + } + } + }, + createSynapseLocally: function () { + var self = Metamaps.Synapse, + topic1, + topic2, + node1, + node2, + synapse, + mapping + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + // for each node in this array we will create a synapse going to the position2 node. + var synapsesToCreate = [] + + topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + node2 = topic2.get('node') + + var len = Metamaps.Selected.Nodes.length + if (len == 0) { + topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + synapsesToCreate[0] = topic1.get('node') + } else if (len > 0) { + synapsesToCreate = Metamaps.Selected.Nodes + } + + for (var i = 0; i < synapsesToCreate.length; i++) { + node1 = synapsesToCreate[i] + topic1 = node1.getData('topic') + synapse = new Metamaps.Backbone.Synapse({ + desc: Metamaps.Create.newSynapse.description, + node1_id: topic1.isNew() ? topic1.cid : topic1.id, + node2_id: topic2.isNew() ? topic2.cid : topic2.id, + }) + Metamaps.Synapses.add(synapse) + + mapping = new Metamaps.Backbone.Mapping({ + mappable_type: 'Synapse', + mappable_id: synapse.cid, + }) + Metamaps.Mappings.add(mapping) + + // this function also includes the creation of the synapse in the database + self.renderSynapse(mapping, synapse, node1, node2, true) + } // for each in synapsesToCreate + + Metamaps.Create.newSynapse.hide() + }, + getSynapseFromAutocomplete: function (id) { + var self = Metamaps.Synapse, + topic1, + topic2, + node1, + node2 + + var synapse = self.get(id) + + var mapping = new Metamaps.Backbone.Mapping({ + mappable_type: 'Synapse', + mappable_id: synapse.id, + }) + Metamaps.Mappings.add(mapping) + + topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + node1 = topic1.get('node') + topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + node2 = topic2.get('node') + Metamaps.Create.newSynapse.hide() + + self.renderSynapse(mapping, synapse, node1, node2, true) + } +}; // end Metamaps.Synapse diff --git a/app/assets/javascripts/src/Metamaps.SynapseCard.js b/app/assets/javascripts/src/Metamaps.SynapseCard.js new file mode 100644 index 00000000..f71601e5 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.SynapseCard.js @@ -0,0 +1,288 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.SynapseCard.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Control + * - Metamaps.Mapper + * - Metamaps.Visualize + */ +Metamaps.SynapseCard = { + openSynapseCard: null, + showCard: function (edge, e) { + var self = Metamaps.SynapseCard + + // reset so we don't interfere with other edges, but first, save its x and y + var myX = $('#edit_synapse').css('left') + var myY = $('#edit_synapse').css('top') + $('#edit_synapse').remove() + + // so label is missing while editing + Metamaps.Control.deselectEdge(edge) + + var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 + var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse + + // create the wrapper around the form elements, including permissions + // classes to make best_in_place happy + var edit_div = document.createElement('div') + edit_div.innerHTML = '
    ' + edit_div.setAttribute('id', 'edit_synapse') + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + edit_div.className = 'permission canEdit' + edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : '' + } else { + edit_div.className = 'permission cannotEdit' + } + $('#wrapper').append(edit_div) + + self.populateShowCard(edge, synapse) + + // drop it in the right spot, activate it + $('#edit_synapse').css('position', 'absolute') + if (e) { + $('#edit_synapse').css('left', e.clientX) + $('#edit_synapse').css('top', e.clientY) + } else { + $('#edit_synapse').css('left', myX) + $('#edit_synapse').css('top', myY) + } + // $('#edit_synapse_name').click() //required in case name is empty + // $('#edit_synapse_name input').focus() + $('#edit_synapse').show() + + self.openSynapseCard = edge + }, + + hideCard: function () { + $('#edit_synapse').remove() + Metamaps.SynapseCard.openSynapseCard = null + }, + + populateShowCard: function (edge, synapse) { + var self = Metamaps.SynapseCard + + self.add_synapse_count(edge) + self.add_desc_form(synapse) + self.add_drop_down(edge, synapse) + self.add_user_info(synapse) + self.add_perms_form(synapse) + self.add_direction_form(synapse) + }, + add_synapse_count: function (edge) { + var count = edge.getData('synapses').length + + $('#editSynUpperBar').append('
    ' + count + '
    ') + }, + add_desc_form: function (synapse) { + var data_nil = 'Click to add description.' + + // TODO make it so that this would work even in sandbox mode, + // currently with Best_in_place it won't + + // desc editing form + $('#editSynUpperBar').append('
    ') + $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc') + $('#edit_synapse_desc').attr('data-object', 'synapse') + $('#edit_synapse_desc').attr('data-attribute', 'desc') + $('#edit_synapse_desc').attr('data-type', 'textarea') + $('#edit_synapse_desc').attr('data-nil', data_nil) + $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').html(synapse.get('desc')) + + // if edge data is blank or just whitespace, populate it with data_nil + if ($('#edit_synapse_desc').html().trim() == '') { + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + $('#edit_synapse_desc').html(data_nil) + } else { + $('#edit_synapse_desc').html('(no description)') + } + } + + $('#edit_synapse_desc').bind('ajax:success', function () { + var desc = $(this).html() + if (desc == data_nil) { + synapse.set('desc', '') + } else { + synapse.set('desc', desc) + } + synapse.trigger('saved') + Metamaps.Control.selectEdge(synapse.get('edge')) + Metamaps.Visualize.mGraph.plot() + }) + }, + add_drop_down: function (edge, synapse) { + var list, i, synapses, l, desc + + synapses = edge.getData('synapses') + l = synapses.length + + if (l > 1) { + // append the element that you click to show dropdown select + $('#editSynUpperBar').append('') + $('#dropdownSynapses').click(function (e) { + e.preventDefault() + e.stopPropagation() // stop it from immediately closing it again + $('#switchSynapseList').toggle() + }) + // hide the dropdown again if you click anywhere else on the synapse card + $('#edit_synapse').click(function () { + $('#switchSynapseList').hide() + }) + + // generate the list of other synapses + list = '' + // add the list of the other synapses + $('#editSynLowerBar').append(list) + + // attach click listeners to list items that + // will cause it to switch the displayed synapse + // when you click it + $('#switchSynapseList li').click(function (e) { + e.stopPropagation() + var index = parseInt($(this).attr('data-synapse-index')) + edge.setData('displayIndex', index) + Metamaps.Visualize.mGraph.plot() + Metamaps.SynapseCard.showCard(edge, false) + }) + } + }, + add_user_info: function (synapse) { + var u = '
    ' + u += ' ' + u += '
    ' + synapse.get('user_name') + '
    ' + $('#editSynLowerBar').append(u) + + // get mapper image + var setMapperImage = function (mapper) { + $('#edgeUser img').attr('src', mapper.get('image')) + } + Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage) + }, + + add_perms_form: function (synapse) { + // permissions - if owner, also allow permission editing + $('#editSynLowerBar').append('
    ') + + // ability to change permission + var selectingPermission = false + var permissionLiClick = function (event) { + selectingPermission = false + var permission = $(this).attr('class') + synapse.save({ + permission: permission, + defer_to_map_id: null + }) + $('#edit_synapse .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)) + $('#edit_synapse .permissionSelect').remove() + event.stopPropagation() + } + + var openPermissionSelect = function (event) { + if (!selectingPermission) { + selectingPermission = true + $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow + if ($(this).hasClass('co')) { + $(this).append('') + } else if ($(this).hasClass('pu')) { + $(this).append('') + } else if ($(this).hasClass('pr')) { + $(this).append('') + } + $('#edit_synapse .permissionSelect li').click(permissionLiClick) + event.stopPropagation() + } + } + + var hidePermissionSelect = function () { + selectingPermission = false + $('#edit_synapse.yourEdge .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow + $('#edit_synapse .permissionSelect').remove() + } + + if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect) + $('#edit_synapse').click(hidePermissionSelect) + } + }, // add_perms_form + + add_direction_form: function (synapse) { + // directionality checkboxes + $('#editSynLowerBar').append('
    ') + $('#editSynLowerBar').append('
    ') + + var edge = synapse.get('edge') + + // determine which node is to the left and the right + // if directly in a line, top is left + if (edge.nodeFrom.pos.x < edge.nodeTo.pos.x || + edge.nodeFrom.pos.x == edge.nodeTo.pos.x && + edge.nodeFrom.pos.y < edge.nodeTo.pos.y) { + var left = edge.nodeTo.getData('topic') + var right = edge.nodeFrom.getData('topic') + } else { + var left = edge.nodeFrom.getData('topic') + var right = edge.nodeTo.getData('topic') + } + + /* + * One node is actually on the left onscreen. Call it left, & the other right. + * If category is from-to, and that node is first, check the 'right' checkbox. + * Else check the 'left' checkbox since the arrow is incoming. + */ + + var directionCat = synapse.get('category'); // both, none, from-to + if (directionCat == 'from-to') { + var from_to = [synapse.get('node1_id'), synapse.get('node2_id')] + if (from_to[0] == left.id) { + // check left checkbox + $('#edit_synapse_left').addClass('checked') + } else { + // check right checkbox + $('#edit_synapse_right').addClass('checked') + } + } else if (directionCat == 'both') { + // check both checkboxes + $('#edit_synapse_left').addClass('checked') + $('#edit_synapse_right').addClass('checked') + } + + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + $('#edit_synapse_left, #edit_synapse_right').click(function () { + $(this).toggleClass('checked') + + var leftChecked = $('#edit_synapse_left').is('.checked') + var rightChecked = $('#edit_synapse_right').is('.checked') + + var dir = synapse.getDirection() + var dirCat = 'none' + if (leftChecked && rightChecked) { + dirCat = 'both' + } else if (!leftChecked && rightChecked) { + dirCat = 'from-to' + dir = [right.id, left.id] + } else if (leftChecked && !rightChecked) { + dirCat = 'from-to' + dir = [left.id, right.id] + } + + synapse.save({ + category: dirCat, + node1_id: dir[0], + node2_id: dir[1] + }) + Metamaps.Visualize.mGraph.plot() + }) + } // if + } // add_direction_form +}; // end Metamaps.SynapseCard diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js new file mode 100644 index 00000000..da1bfc8c --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -0,0 +1,363 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Topic.js.erb + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Backbone + * - Metamaps.Backbone + * - Metamaps.Create + * - Metamaps.Creators + * - Metamaps.Famous + * - Metamaps.Filter + * - Metamaps.GlobalUI + * - Metamaps.JIT + * - Metamaps.Mappings + * - Metamaps.Selected + * - Metamaps.Settings + * - Metamaps.SynapseCard + * - Metamaps.Synapses + * - Metamaps.TopicCard + * - Metamaps.Topics + * - Metamaps.Util + * - Metamaps.Visualize + * - Metamaps.tempInit + * - Metamaps.tempNode + * - Metamaps.tempNode2 + */ + +Metamaps.Topic = { + // this function is to retrieve a topic JSON object from the database + // @param id = the id of the topic to retrieve + get: function (id, callback) { + // if the desired topic is not yet in the local topic repository, fetch it + if (Metamaps.Topics.get(id) == undefined) { + // console.log("Ajax call!") + if (!callback) { + var e = $.ajax({ + url: '/topics/' + id + '.json', + async: false + }) + Metamaps.Topics.add($.parseJSON(e.responseText)) + return Metamaps.Topics.get(id) + } else { + return $.ajax({ + url: '/topics/' + id + '.json', + success: function (data) { + Metamaps.Topics.add(data) + callback(Metamaps.Topics.get(id)) + } + }) + } + } else { + if (!callback) { + return Metamaps.Topics.get(id) + } else { + return callback(Metamaps.Topics.get(id)) + } + } + }, + launch: function (id) { + var bb = Metamaps.Backbone + var start = function (data) { + Metamaps.Active.Topic = new bb.Topic(data.topic) + Metamaps.Creators = new bb.MapperCollection(data.creators) + Metamaps.Topics = new bb.TopicCollection([data.topic].concat(data.relatives)) + Metamaps.Synapses = new bb.SynapseCollection(data.synapses) + Metamaps.Backbone.attachCollectionEvents() + + // set filter mapper H3 text + $('#filter_by_mapper h3').html('CREATORS') + + // build and render the visualization + Metamaps.Visualize.type = 'RGraph' + Metamaps.JIT.prepareVizData() + + // update filters + Metamaps.Filter.reset() + + // reset selected arrays + Metamaps.Selected.reset() + + // these three update the actual filter box with the right list items + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMappers() + } + + $.ajax({ + url: '/topics/' + id + '/network.json', + success: start + }) + }, + end: function () { + if (Metamaps.Active.Topic) { + $('.rightclickmenu').remove() + Metamaps.TopicCard.hideCard() + Metamaps.SynapseCard.hideCard() + Metamaps.Filter.close() + } + }, + centerOn: function (nodeid) { + if (!Metamaps.Visualize.mGraph.busy) { + Metamaps.Visualize.mGraph.onClick(nodeid, { + hideLabels: false, + duration: 1000, + onComplete: function () {} + }) + } + }, + fetchRelatives: function (node, metacode_id) { + var topics = Metamaps.Topics.map(function (t) { return t.id }) + var topics_string = topics.join() + + var creators = Metamaps.Creators.map(function (t) { return t.id }) + var creators_string = creators.join() + + var topic = node.getData('topic') + + var successCallback = function (data) { + if (data.creators.length > 0) Metamaps.Creators.add(data.creators) + if (data.topics.length > 0) Metamaps.Topics.add(data.topics) + if (data.synapses.length > 0) Metamaps.Synapses.add(data.synapses) + + var topicColl = new Metamaps.Backbone.TopicCollection(data.topics) + topicColl.add(topic) + var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses) + + var graph = Metamaps.JIT.convertModelsToJIT(topicColl, synapseColl)[0] + Metamaps.Visualize.mGraph.op.sum(graph, { + type: 'fade', + duration: 500, + hideLabels: false + }) + + var i, l, t, s + + Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + t = Metamaps.Topics.get(n.id) + t.set({ node: n }, { silent: true }) + t.updateNode() + + n.eachAdjacency(function (edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + s = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + s.set({ edge: edge }, { silent: true }) + s.updateEdge() + } + } + }) + }) + } + + var paramsString = metacode_id ? 'metacode=' + metacode_id + '&' : '' + paramsString += 'network=' + topics_string + '&creators=' + creators_string + + $.ajax({ + type: 'Get', + url: '/topics/' + topic.id + '/relatives.json?' + paramsString, + success: successCallback, + error: function () {} + }) + }, + /* + * + * + */ + renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter) { + var self = Metamaps.Topic + + var nodeOnViz, tempPos + + var newnode = topic.createNode() + + var midpoint = {}, pixelPos + + if (!$.isEmptyObject(Metamaps.Visualize.mGraph.graph.nodes)) { + Metamaps.Visualize.mGraph.graph.addNode(newnode) + nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + topic.set('node', nodeOnViz, {silent: true}) + topic.updateNode() // links the topic and the mapping to the node + + nodeOnViz.setData('dim', 1, 'start') + nodeOnViz.setData('dim', 25, 'end') + if (Metamaps.Visualize.type === 'RGraph') { + tempPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) + tempPos = tempPos.toPolar() + nodeOnViz.setPos(tempPos, 'current') + nodeOnViz.setPos(tempPos, 'start') + nodeOnViz.setPos(tempPos, 'end') + } else if (Metamaps.Visualize.type === 'ForceDirected') { + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') + } + if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { + Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id + + // position the form + midpoint.x = Metamaps.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 + midpoint.y = Metamaps.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + pixelPos = Metamaps.Util.coordsToPixels(midpoint) + $('#new_synapse').css('left', pixelPos.x + 'px') + $('#new_synapse').css('top', pixelPos.y + 'px') + // show the form + Metamaps.Create.newSynapse.open() + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['node-property:dim'], + duration: 500, + onComplete: function () { + Metamaps.tempNode = null + Metamaps.tempNode2 = null + Metamaps.tempInit = false + } + }) + } else { + Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['node-property:dim'], + duration: 500, + onComplete: function () {} + }) + } + } else { + Metamaps.Visualize.mGraph.loadJSON(newnode) + nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + topic.set('node', nodeOnViz, {silent: true}) + topic.updateNode() // links the topic and the mapping to the node + + nodeOnViz.setData('dim', 1, 'start') + nodeOnViz.setData('dim', 25, 'end') + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') + nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') + Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) + Metamaps.Visualize.mGraph.fx.animate({ + modes: ['node-property:dim'], + duration: 500, + onComplete: function () {} + }) + } + + var mappingSuccessCallback = function (mappingModel, response) { + var newTopicData = { + mappingid: mappingModel.id, + mappableid: mappingModel.get('mappable_id') + } + + $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + } + var topicSuccessCallback = function (topicModel, response) { + if (Metamaps.Active.Map) { + mapping.save({ mappable_id: topicModel.id }, { + success: mappingSuccessCallback, + error: function (model, response) { + console.log('error saving mapping to database') + } + }) + } + + if (Metamaps.Create.newTopic.addSynapse) { + Metamaps.Create.newSynapse.topic2id = topicModel.id + } + } + + if (!Metamaps.Settings.sandbox && createNewInDB) { + if (topic.isNew()) { + topic.save(null, { + success: topicSuccessCallback, + error: function (model, response) { + console.log('error saving topic to database') + } + }) + } else if (!topic.isNew() && Metamaps.Active.Map) { + mapping.save(null, { + success: mappingSuccessCallback + }) + } + } + }, + createTopicLocally: function () { + var self = Metamaps.Topic + + if (Metamaps.Create.newTopic.name === '') { + Metamaps.GlobalUI.notifyUser('Please enter a topic title...') + return + } + + // hide the 'double-click to add a topic' message + Metamaps.Famous.viz.hideInstructions() + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + var metacode = Metamaps.Metacodes.get(Metamaps.Create.newTopic.metacode) + + var topic = new Metamaps.Backbone.Topic({ + name: Metamaps.Create.newTopic.name, + metacode_id: metacode.id, + defer_to_map_id: Metamaps.Active.Map.id + }) + Metamaps.Topics.add(topic) + + var mapping = new Metamaps.Backbone.Mapping({ + xloc: Metamaps.Create.newTopic.x, + yloc: Metamaps.Create.newTopic.y, + mappable_id: topic.cid, + mappable_type: 'Topic', + }) + Metamaps.Mappings.add(mapping) + + // these can't happen until the value is retrieved, which happens in the line above + Metamaps.Create.newTopic.hide() + + self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database + }, + getTopicFromAutocomplete: function (id) { + var self = Metamaps.Topic + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + Metamaps.Create.newTopic.hide() + + var topic = self.get(id) + + var mapping = new Metamaps.Backbone.Mapping({ + xloc: Metamaps.Create.newTopic.x, + yloc: Metamaps.Create.newTopic.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) + + self.renderTopic(mapping, topic, true, true) + }, + getTopicFromSearch: function (event, id) { + var self = Metamaps.Topic + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + var topic = self.get(id) + + var nextCoords = Metamaps.Map.getNextCoord() + var mapping = new Metamaps.Backbone.Mapping({ + xloc: nextCoords.x, + yloc: nextCoords.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) + + self.renderTopic(mapping, topic, true, true) + + Metamaps.GlobalUI.notifyUser('Topic was added to your map!') + + event.stopPropagation() + event.preventDefault() + return false + } +}; // end Metamaps.Topic diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/app/assets/javascripts/src/Metamaps.TopicCard.js new file mode 100644 index 00000000..194433db --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.TopicCard.js @@ -0,0 +1,451 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.TopicCard.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.GlobalUI + * - Metamaps.Mapper + * - Metamaps.Metacodes + * - Metamaps.Router + * - Metamaps.Util + * - Metamaps.Visualize + */ +Metamaps.TopicCard = { + openTopicCard: null, // stores the topic that's currently open + authorizedToEdit: false, // stores boolean for edit permission for open topic card + init: function () { + var self = Metamaps.TopicCard + + // initialize best_in_place editing + $('.authenticated div.permission.canEdit .best_in_place').best_in_place() + + Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) + + // initialize topic card draggability and resizability + $('.showcard').draggable({ + handle: '.metacodeImage' + }) + + embedly('on', 'card.rendered', self.embedlyCardRendered) + }, + /** + * Will open the Topic Card for the node that it's passed + * @param {$jit.Graph.Node} node + */ + showCard: function (node) { + var self = Metamaps.TopicCard + + var topic = node.getData('topic') + + self.openTopicCard = topic + self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) + // populate the card that's about to show with the right topics data + self.populateShowCard(topic) + $('.showcard').fadeIn('fast') + }, + hideCard: function () { + var self = Metamaps.TopicCard + + $('.showcard').fadeOut('fast') + self.openTopicCard = null + self.authorizedToEdit = false + }, + embedlyCardRendered: function (iframe) { + var self = Metamaps.TopicCard + + $('#embedlyLinkLoader').hide() + + // means that the embedly call returned 404 not found + if ($('#embedlyLink')[0]) { + $('#embedlyLink').css('display', 'block').fadeIn('fast') + $('.embeds').addClass('nonEmbedlyLink') + } + + $('.CardOnGraph').addClass('hasAttachment') + if (self.authorizedToEdit) { + $('.embeds').append('
    ') + $('#linkremove').click(self.removeLink) + } + }, + removeLink: function () { + var self = Metamaps.TopicCard + self.openTopicCard.save({ + link: null + }) + $('.embeds').empty().removeClass('nonEmbedlyLink') + $('#addLinkInput input').val('') + $('.attachments').removeClass('hidden') + $('.CardOnGraph').removeClass('hasAttachment') + }, + bindShowCardListeners: function (topic) { + var self = Metamaps.TopicCard + var showCard = document.getElementById('showcard') + + var authorized = self.authorizedToEdit + + // get mapper image + var setMapperImage = function (mapper) { + $('.contributorIcon').attr('src', mapper.get('image')) + } + Metamaps.Mapper.get(topic.get('user_id'), setMapperImage) + + // starting embed.ly + var resetFunc = function () { + $('#addLinkInput input').val('') + $('#addLinkInput input').focus() + } + var inputEmbedFunc = function (event) { + var element = this + setTimeout(function () { + var text = $(element).val() + if (event.type == 'paste' || (event.type == 'keyup' && event.which == 13)) { + // TODO evaluate converting this to '//' no matter what (infer protocol) + if (text.slice(0, 7) !== 'http://' && + text.slice(0, 8) !== 'https://' && + text.slice(0, 2) !== '//') { + text = '//' + text + } + topic.save({ + link: text + }) + var embedlyEl = $('', { + id: 'embedlyLink', + 'data-card-description': '0', + href: text + }).html(text) + $('.attachments').addClass('hidden') + $('.embeds').append(embedlyEl) + $('.embeds').append('
    ') + var loader = new CanvasLoader('embedlyLinkLoader') + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + var e = embedly('card', document.getElementById('embedlyLink')) + if (!e) { + self.handleInvalidLink() + } + } + }, 100) + } + $('#addLinkReset').click(resetFunc) + $('#addLinkInput input').bind('paste keyup', inputEmbedFunc) + + // initialize the link card, if there is a link + if (topic.get('link') && topic.get('link') !== '') { + var loader = new CanvasLoader('embedlyLinkLoader') + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + var e = embedly('card', document.getElementById('embedlyLink')) + if (!e) { + self.handleInvalidLink() + } + } + + var selectingMetacode = false + // attach the listener that shows the metacode title when you hover over the image + $('.showcard .metacodeImage').mouseenter(function () { + $('.showcard .icon').css('z-index', '4') + $('.showcard .metacodeTitle').show() + }) + $('.showcard .linkItem.icon').mouseleave(function () { + if (!selectingMetacode) { + $('.showcard .metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + }) + + var metacodeLiClick = function () { + selectingMetacode = false + var metacodeId = parseInt($(this).attr('data-id')) + var metacode = Metamaps.Metacodes.get(metacodeId) + $('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name')) + .append('
    ') + .attr('class', 'metacodeTitle mbg' + metacode.id) + $('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')') + topic.save({ + metacode_id: metacode.id + }) + Metamaps.Visualize.mGraph.plot() + $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') + $('.metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + + var openMetacodeSelect = function (event) { + var windowWidth + var showcardLeft + var TOPICCARD_WIDTH = 300 + var METACODESELECT_WIDTH = 404 + var distanceFromEdge + + var MAX_METACODELIST_HEIGHT = 270 + var windowHeight + var showcardTop + var topicTitleHeight + var distanceFromBottom + + if (!selectingMetacode) { + selectingMetacode = true + + // this is to make sure the metacode + // select is accessible onscreen, when opened + // while topic card is close to the right + // edge of the screen + windowWidth = $(window).width() + showcardLeft = parseInt($('.showcard').css('left')) + distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) + if (distanceFromEdge < METACODESELECT_WIDTH) { + $('.metacodeSelect').addClass('onRightEdge') + } + + // this is to make sure the metacode + // select is accessible onscreen, when opened + // while topic card is close to the bottom + // edge of the screen + windowHeight = $(window).height() + showcardTop = parseInt($('.showcard').css('top')) + topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) + heightOfSetList = $('.showcard .metacodeSelect').height() + distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) + if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { + $('.metacodeSelect').addClass('onBottomEdge') + } + + $('.metacodeSelect').show() + event.stopPropagation() + } + } + + var hideMetacodeSelect = function () { + selectingMetacode = false + $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') + $('.metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + + if (authorized) { + $('.showcard .metacodeTitle').click(openMetacodeSelect) + $('.showcard').click(hideMetacodeSelect) + $('.metacodeSelect > ul > li').click(function (event) { + event.stopPropagation() + }) + $('.metacodeSelect li li').click(metacodeLiClick) + + var bipName = $(showCard).find('.best_in_place_name') + bipName.bind('best_in_place:activate', function () { + var $el = bipName.find('textarea') + var el = $el[0] + + $el.attr('maxlength', '140') + + $('.showcard .title').append('
    ') + + var callback = function (data) { + $('.nameCounter.forTopic').html(data.all + '/140') + } + Countable.live(el, callback) + }) + bipName.bind('best_in_place:deactivate', function () { + $('.nameCounter.forTopic').remove() + }) + + // bind best_in_place ajax callbacks + bipName.bind('ajax:success', function () { + var name = Metamaps.Util.decodeEntities($(this).html()) + topic.set('name', name) + topic.trigger('saved') + }) + + $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { + this.innerHTML = this.innerHTML.replace(/\r/g, '') + var desc = $(this).html() === $(this).data('nil') ? '' : $(this).html() + topic.set('desc', desc) + topic.trigger('saved') + }) + } + + var permissionLiClick = function (event) { + selectingPermission = false + var permission = $(this).attr('class') + topic.save({ + permission: permission, + defer_to_map_id: null + }) + $('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)) + $('.showcard .permissionSelect').remove() + event.stopPropagation() + } + + var openPermissionSelect = function (event) { + if (!selectingPermission) { + selectingPermission = true + $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow + if ($(this).hasClass('co')) { + $(this).append('
    ') + } else if ($(this).hasClass('pu')) { + $(this).append('
    ') + } else if ($(this).hasClass('pr')) { + $(this).append('
    ') + } + $('.showcard .permissionSelect li').click(permissionLiClick) + event.stopPropagation() + } + } + + var hidePermissionSelect = function () { + selectingPermission = false + $('.showcard .yourTopic .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow + $('.showcard .permissionSelect').remove() + } + // ability to change permission + var selectingPermission = false + if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + $('.showcard .yourTopic .mapPerm').click(openPermissionSelect) + $('.showcard').click(hidePermissionSelect) + } + + $('.links .mapCount').unbind().click(function (event) { + $('.mapCount .tip').toggle() + $('.showcard .hoverTip').toggleClass('hide') + event.stopPropagation() + }) + $('.mapCount .tip').unbind().click(function (event) { + event.stopPropagation() + }) + $('.showcard').unbind('.hideTip').bind('click.hideTip', function () { + $('.mapCount .tip').hide() + $('.showcard .hoverTip').removeClass('hide') + }) + + $('.mapCount .tip li a').click(Metamaps.Router.intercept) + + var originalText = $('.showMore').html() + $('.mapCount .tip .showMore').unbind().toggle( + function (event) { + $('.extraText').toggleClass('hideExtra') + $('.showMore').html('Show less...') + }, + function (event) { + $('.extraText').toggleClass('hideExtra') + $('.showMore').html(originalText) + }) + + $('.mapCount .tip showMore').unbind().click(function (event) { + event.stopPropagation() + }) + }, + handleInvalidLink: function () { + var self = Metamaps.TopicCard + + self.removeLink() + Metamaps.GlobalUI.notifyUser('Invalid link') + }, + populateShowCard: function (topic) { + var self = Metamaps.TopicCard + + var showCard = document.getElementById('showcard') + + $(showCard).find('.permission').remove() + + var topicForTemplate = self.buildObject(topic) + var html = self.generateShowcardHTML.render(topicForTemplate) + + if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + var perm = document.createElement('div') + + var string = 'permission canEdit' + if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic' + perm.className = string + perm.innerHTML = html + showCard.appendChild(perm) + } else { + var perm = document.createElement('div') + perm.className = 'permission cannotEdit' + perm.innerHTML = html + showCard.appendChild(perm) + } + + Metamaps.TopicCard.bindShowCardListeners(topic) + }, + generateShowcardHTML: null, // will be initialized into a Hogan template within init function + // generateShowcardHTML + buildObject: function (topic) { + var self = Metamaps.TopicCard + + var nodeValues = {} + + var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + } else { + } + + var desc_nil = 'Click to add description...' + + nodeValues.attachmentsHidden = '' + if (topic.get('link') && topic.get('link') !== '') { + nodeValues.embeds = '
    ' + nodeValues.embeds += topic.get('link') + nodeValues.embeds += '
    ' + nodeValues.attachmentsHidden = 'hidden' + nodeValues.hasAttachment = 'hasAttachment' + } else { + nodeValues.embeds = '' + nodeValues.hasAttachment = '' + } + + if (authorized) { + nodeValues.attachments = '' + } else { + nodeValues.attachmentsHidden = 'hidden' + nodeValues.attachments = '' + } + + var inmapsAr = topic.get('inmaps') + var inmapsLinks = topic.get('inmapsLinks') + nodeValues.inmaps = '' + if (inmapsAr.length < 6) { + for (i = 0; i < inmapsAr.length; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + } else { + for (i = 0; i < 5; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + extra = inmapsAr.length - 5 + nodeValues.inmaps += '
  • See ' + extra + ' more...
  • ' + for (i = 5; i < inmapsAr.length; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + } + nodeValues.permission = topic.get('calculated_permission') + nodeValues.mk_permission = topic.get('calculated_permission').substring(0, 2) + nodeValues.map_count = topic.get('map_count').toString() + nodeValues.synapse_count = topic.get('synapse_count').toString() + nodeValues.id = topic.isNew() ? topic.cid : topic.id + nodeValues.metacode = topic.getMetacode().get('name') + nodeValues.metacode_class = 'mbg' + topic.get('metacode_id') + nodeValues.imgsrc = topic.getMetacode().get('icon') + nodeValues.name = topic.get('name') + nodeValues.userid = topic.get('user_id') + nodeValues.username = topic.get('user_name') + nodeValues.date = topic.getDate() + // the code for this is stored in /views/main/_metacodeOptions.html.erb + nodeValues.metacode_select = $('#metacodeOptions').html() + nodeValues.desc_nil = desc_nil + nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') + return nodeValues + } +}; // end Metamaps.TopicCard diff --git a/app/assets/javascripts/src/Metamaps.Util.js b/app/assets/javascripts/src/Metamaps.Util.js new file mode 100644 index 00000000..e150d3bb --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Util.js @@ -0,0 +1,130 @@ +/* global Metamaps */ + +/* + * Metamaps.Util.js + * + * Dependencies: + * - Metamaps.Visualize + */ + +Metamaps.Util = { + // helper function to determine how many lines are needed + // Line Splitter Function + // copyright Stephen Chapman, 19th April 2006 + // you may copy this code but please keep the copyright notice as well + splitLine: function (st, n) { + var b = '' + var s = st ? st : '' + while (s.length > n) { + var c = s.substring(0, n) + var d = c.lastIndexOf(' ') + var e = c.lastIndexOf('\n') + if (e != -1) d = e + if (d == -1) d = n + b += c.substring(0, d) + '\n' + s = s.substring(d + 1) + } + return b + s + }, + nowDateFormatted: function () { + var date = new Date(Date.now()) + var month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1) + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var year = date.getFullYear() + + return month + '/' + day + '/' + year + }, + decodeEntities: function (desc) { + var str, temp = document.createElement('p') + temp.innerHTML = desc // browser handles the topics + str = temp.textContent || temp.innerText + temp = null // delete the element + return str + }, // decodeEntities + getDistance: function (p1, p2) { + return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)) + }, + coordsToPixels: function (coords) { + if (Metamaps.Visualize.mGraph) { + var canvas = Metamaps.Visualize.mGraph.canvas, + s = canvas.getSize(), + p = canvas.getPos(), + ox = canvas.translateOffsetX, + oy = canvas.translateOffsetY, + sx = canvas.scaleOffsetX, + sy = canvas.scaleOffsetY + var pixels = { + x: (coords.x / (1 / sx)) + p.x + s.width / 2 + ox, + y: (coords.y / (1 / sy)) + p.y + s.height / 2 + oy + } + return pixels + } else { + return { + x: 0, + y: 0 + } + } + }, + pixelsToCoords: function (pixels) { + var coords + if (Metamaps.Visualize.mGraph) { + var canvas = Metamaps.Visualize.mGraph.canvas, + s = canvas.getSize(), + p = canvas.getPos(), + ox = canvas.translateOffsetX, + oy = canvas.translateOffsetY, + sx = canvas.scaleOffsetX, + sy = canvas.scaleOffsetY + coords = { + x: (pixels.x - p.x - s.width / 2 - ox) * (1 / sx), + y: (pixels.y - p.y - s.height / 2 - oy) * (1 / sy), + } + } else { + coords = { + x: 0, + y: 0 + } + } + return coords + }, + getPastelColor: function () { + var r = (Math.round(Math.random() * 127) + 127).toString(16) + var g = (Math.round(Math.random() * 127) + 127).toString(16) + var b = (Math.round(Math.random() * 127) + 127).toString(16) + return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4) + }, + // darkens a hex value by 'lum' percentage + colorLuminance: function (hex, lum) { + // validate hex string + hex = String(hex).replace(/[^0-9a-f]/gi, '') + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + } + lum = lum || 0 + + // convert to decimal and change luminosity + var rgb = '#', c, i + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i * 2, 2), 16) + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) + rgb += ('00' + c).substr(c.length) + } + + return rgb + }, + generateOptionsList: function (data) { + var newlist = '' + for (var i = 0; i < data.length; i++) { + newlist = newlist + '' + } + return newlist + }, + checkURLisImage: function (url) { + // when the page reloads the following regular expression will be screwed up + // please replace it with this one before you save: /*backslashhere*.(jpeg|jpg|gif|png)$/ + return (url.match(/\.(jpeg|jpg|gif|png)$/) != null) + }, + checkURLisYoutubeVideo: function (url) { + return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) + } +}; // end Metamaps.Util diff --git a/app/assets/javascripts/src/Metamaps.Views.js.erb b/app/assets/javascripts/src/Metamaps.Views.js similarity index 100% rename from app/assets/javascripts/src/Metamaps.Views.js.erb rename to app/assets/javascripts/src/Metamaps.Views.js diff --git a/app/assets/javascripts/src/Metamaps.Visualize.js b/app/assets/javascripts/src/Metamaps.Visualize.js new file mode 100644 index 00000000..2353cac5 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Visualize.js @@ -0,0 +1,210 @@ +/* global Metamaps, $ */ +/* + * Metamaps.Visualize + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.JIT + * - Metamaps.Loading + * - Metamaps.Metacodes + * - Metamaps.Router + * - Metamaps.Synapses + * - Metamaps.TopicCard + * - Metamaps.Topics + * - Metamaps.Touch + * - Metamaps.Visualize + */ + +Metamaps.Visualize = { + mGraph: null, // a reference to the graph object. + cameraPosition: null, // stores the camera position when using a 3D visualization + type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" + loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created + init: function () { + var self = Metamaps.Visualize + // disable awkward dragging of the canvas element that would sometimes happen + $('#infovis-canvas').on('dragstart', function (event) { + event.preventDefault() + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchstart', function (event) { + event.preventDefault() + self.mGraph.events.touched = true + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchmove', function (event) { + // Metamaps.JIT.touchPanZoomHandler(event) + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchend touchcancel', function (event) { + lastDist = 0 + if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + self.mGraph.events.touched = self.mGraph.events.touchMoved = false + Metamaps.Touch.touchDragNode = false + }) + }, + computePositions: function () { + var self = Metamaps.Visualize, + mapping + + if (self.type == 'RGraph') { + var i, l, startPos, endPos, topic, synapse + + self.mGraph.graph.eachNode(function (n) { + topic = Metamaps.Topics.get(n.id) + topic.set({ node: n }, { silent: true }) + topic.updateNode() + + n.eachAdjacency(function (edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse.set({ edge: edge }, { silent: true }) + synapse.updateEdge() + } + } + }) + + var pos = n.getPos() + pos.setc(-200, -200) + }) + self.mGraph.compute('end') + } else if (self.type == 'ForceDirected') { + var i, l, startPos, endPos, topic, synapse + + self.mGraph.graph.eachNode(function (n) { + topic = Metamaps.Topics.get(n.id) + topic.set({ node: n }, { silent: true }) + topic.updateNode() + mapping = topic.getMapping() + + n.eachAdjacency(function (edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse.set({ edge: edge }, { silent: true }) + synapse.updateEdge() + } + } + }) + + startPos = new $jit.Complex(0, 0) + endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) + n.setPos(startPos, 'start') + n.setPos(endPos, 'end') + }) + } else if (self.type == 'ForceDirected3D') { + self.mGraph.compute() + } + }, + /** + * render does the heavy lifting of creating the engine that renders the graph with the properties we desire + * + */ + render: function () { + var self = Metamaps.Visualize, RGraphSettings, FDSettings + + if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { + RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + + $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) + $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + + RGraphSettings.width = $(document).width() + RGraphSettings.height = $(document).height() + RGraphSettings.background = Metamaps.JIT.RGraph.background + RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance + + self.mGraph = new $jit.RGraph(RGraphSettings) + } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { + FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + + $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) + $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + + FDSettings.width = $('body').width() + FDSettings.height = $('body').height() + + self.mGraph = new $jit.ForceDirected(FDSettings) + } else if (self.type == 'ForceDirected3D' && !self.mGraph) { + // init ForceDirected3D + self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings) + self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position + } else { + self.mGraph.graph.empty() + } + + + if (self.type == 'ForceDirected' && Metamaps.Active.Mapper) $.post('/maps/' + Metamaps.Active.Map.id + '/events/user_presence') + + function runAnimation () { + Metamaps.Loading.hide() + // load JSON data, if it's not empty + if (!self.loadLater) { + // load JSON data. + var rootIndex = 0 + if (Metamaps.Active.Topic) { + var node = _.find(Metamaps.JIT.vizData, function (node) { + return node.id === Metamaps.Active.Topic.id + }) + rootIndex = _.indexOf(Metamaps.JIT.vizData, node) + } + self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex) + // compute positions and plot. + self.computePositions() + self.mGraph.busy = true + if (self.type == 'RGraph') { + self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate) + } else if (self.type == 'ForceDirected') { + self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + } else if (self.type == 'ForceDirected3D') { + self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout) + } + } + } + // hold until all the needed metacode images are loaded + // hold for a maximum of 80 passes, or 4 seconds of waiting time + var tries = 0 + function hold () { + var unique = _.uniq(Metamaps.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), + requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), + loadedCount = 0 + + _.each(requiredMetacodes, function (metacode_id) { + var metacode = Metamaps.Metacodes.get(metacode_id), + img = metacode ? metacode.get('image') : false + + if (img && (img.complete || (typeof img.naturalWidth !== 'undefined' && img.naturalWidth !== 0))) { + loadedCount += 1 + } + }) + + if (loadedCount === requiredMetacodes.length || tries > 80) runAnimation() + else setTimeout(function () { tries++; hold() }, 50) + } + hold() + + // update the url now that the map is ready + clearTimeout(Metamaps.Router.timeoutId) + Metamaps.Router.timeoutId = setTimeout(function () { + var m = Metamaps.Active.Map + var t = Metamaps.Active.Topic + + if (m && window.location.pathname !== '/maps/' + m.id) { + Metamaps.Router.navigate('/maps/' + m.id) + } + else if (t && window.location.pathname !== '/topics/' + t.id) { + Metamaps.Router.navigate('/topics/' + t.id) + } + }, 800) + } +}; // end Metamaps.Visualize diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 70252ea8..a2fd8a21 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -1,5519 +1,73 @@ -// TODO document this user agent function +/* global Metamaps */ -var labelType, useGradients, nativeTextSupport, animate; - -(function () { - var ua = navigator.userAgent, - iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), - typeOfCanvas = typeof HTMLCanvasElement, - nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), - textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function'); - //I'm setting this based on the fact that ExCanvas provides text support for IE - //and that as of today iPhone/iPad current text support is lame - labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML'; - nativeTextSupport = labelType == 'Native'; - useGradients = nativeCanvasSupport; - animate = !(iStuff || !nativeCanvasSupport); -})(); +/* + * Metamaps.js.erb + */ // TODO eliminate these 5 top-level variables -Metamaps.panningInt = null; -Metamaps.tempNode = null; -Metamaps.tempInit = false; -Metamaps.tempNode2 = null; +Metamaps.panningInt = null +Metamaps.tempNode = null +Metamaps.tempInit = false +Metamaps.tempNode2 = null Metamaps.VERSION = '<%= METAMAPS_VERSION %>' +/* erb variables from rails */ +Metamaps.Erb = {} +Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' +Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' +Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' +Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' +Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' +Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' + Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' }, -}; + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -}; + touchPos: null, // this stores the x and y values of a current touch event + touchDragNode: null // this stores a reference to a JIT node that is being dragged +} Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -}; + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected; - - self.Nodes = []; - self.Edges = []; - }, - Nodes: [], - Edges: [] -}; - -/* - * - * BACKBONE - * - */ -Metamaps.Backbone.init = function () { - var self = Metamaps.Backbone; - - self.Metacode = Backbone.Model.extend({ - initialize: function () { - var image = new Image(); - image.crossOrigin = "Anonymous"; - image.src = this.get('icon'); - this.set('image',image); - }, - prepareLiForFilter: function () { - var li = ''; - li += '
  • ';       - li += '';       - li += '

    ' + this.get('name').toLowerCase() + '

  • '; - return li; - } - - }); - self.MetacodeCollection = Backbone.Collection.extend({ - model: this.Metacode, - url: '/metacodes', - comparator: function (a, b) { - a = a.get('name').toLowerCase(); - b = b.get('name').toLowerCase(); - return a > b ? 1 : a < b ? -1 : 0; - } - }); - - self.Topic = Backbone.Model.extend({ - urlRoot: '/topics', - blacklist: ['node', 'created_at', 'updated_at', 'user_name', 'user_image', 'map_count', 'synapse_count'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - save: function (key, val, options) { - - var attrs; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - var newOptions = options || {}; - var s = newOptions.success; - - var permBefore = this.get('permission'); - - newOptions.success = function (model, response, opt) { - if (s) s(model, response, opt); - model.trigger('saved'); - - if (permBefore === 'private' && model.get('permission') !== 'private') { - model.trigger('noLongerPrivate'); - } - else if (permBefore !== 'private' && model.get('permission') === 'private') { - model.trigger('nowPrivate'); - } - }; - return Backbone.Model.prototype.save.call(this, attrs, newOptions); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "desc": '', - "link": '', - "permission": Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' - }); - } - - this.on('changeByOther', this.updateCardView); - this.on('change', this.updateNodeView); - this.on('saved', this.savedEvent); - this.on('nowPrivate', function(){ - var removeTopicData = { - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]); - }); - this.on('noLongerPrivate', function(){ - var newTopicData = { - mappingid: this.getMapping().id, - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]); - }); - - this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this); - - }, - authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === "commons" || this.get('user_id') === mapper.get('id'))) return true; - else return false; - }, - authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true; - else return false; - }, - getDate: function () { - - }, - getMetacode: function () { - return Metamaps.Metacodes.get(this.get('metacode_id')); - }, - getMapping: function () { - - if (!Metamaps.Active.Map) return false; - - return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, - mappable_type: "Topic", - mappable_id: this.isNew() ? this.cid : this.id - }); - }, - createNode: function () { - var mapping; - var node = { - adjacencies: [], - id: this.isNew() ? this.cid : this.id, - name: this.get('name') - }; - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - node.data = { - $mapping: null, - $mappingID: mapping.id - }; - } - - return node; - }, - updateNode: function () { - var mapping; - var node = this.get('node'); - node.setData('topic', this); - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - node.setData('mapping', mapping); - } - - return node; - }, - savedEvent: function() { - Metamaps.Realtime.sendTopicChange(this); - }, - updateViews: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node); - } - - // update the node on the map - if (onPageWithTopicCard && node) { - node.name = this.get('name'); - Metamaps.Visualize.mGraph.plot(); - } - }, - updateCardView: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node); - } - }, - updateNodeView: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - - // update the node on the map - if (onPageWithTopicCard && node) { - node.name = this.get('name'); - Metamaps.Visualize.mGraph.plot(); - } - } - }); - - self.TopicCollection = Backbone.Collection.extend({ - model: self.Topic, - url: '/topics' - }); - - self.Synapse = Backbone.Model.extend({ - urlRoot: '/synapses', - blacklist: ['edge', 'created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - save: function (key, val, options) { - - var attrs; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - var newOptions = options || {}; - var s = newOptions.success; - - var permBefore = this.get('permission'); - - newOptions.success = function (model, response, opt) { - if (s) s(model, response, opt); - model.trigger('saved'); - - if (permBefore === 'private' && model.get('permission') !== 'private') { - model.trigger('noLongerPrivate'); - } - else if (permBefore !== 'private' && model.get('permission') === 'private') { - model.trigger('nowPrivate'); - } - }; - return Backbone.Model.prototype.save.call(this, attrs, newOptions); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "permission": Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', - "category": "from-to" - }); - } - - this.on('changeByOther', this.updateCardView); - this.on('change', this.updateEdgeView); - this.on('saved', this.savedEvent); - this.on('noLongerPrivate', function(){ - var newSynapseData = { - mappingid: this.getMapping().id, - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]); - }); - this.on('nowPrivate', function(){ - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ - mappableid: this.id - }]); - }); - - this.on('change:desc', Metamaps.Filter.checkSynapses, this); - }, - prepareLiForFilter: function () { - var li = ''; - li += '
  • ';       - li += '
  • '; - return li; - }, - authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === "commons" || this.get('user_id') === mapper.get('id'))) return true; - else return false; - }, - authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true; - else return false; - }, - getTopic1: function () { - return Metamaps.Topics.get(this.get('node1_id')); - }, - getTopic2: function () { - return Metamaps.Topics.get(this.get('node2_id')); - }, - getDirection: function () { - var t1 = this.getTopic1(), - t2 = this.getTopic2(); - - return t1 && t2 ? [ - t1.get('node').id, - t2.get('node').id - ] : false; - }, - getMapping: function () { - - if (!Metamaps.Active.Map) return false; - - return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, - mappable_type: "Synapse", - mappable_id: this.isNew() ? this.cid : this.id - }); - }, - createEdge: function (providedMapping) { - var mapping, mappingID; - var synapseID = this.isNew() ? this.cid : this.id; - - var edge = { - nodeFrom: this.get('node1_id'), - nodeTo: this.get('node2_id'), - data: { - $synapses: [], - $synapseIDs: [synapseID], - } - }; - - if (Metamaps.Active.Map) { - mapping = providedMapping || this.getMapping(); - mappingID = mapping.isNew() ? mapping.cid : mapping.id; - edge.data.$mappings = []; - edge.data.$mappingIDs = [mappingID]; - } - - return edge; - }, - updateEdge: function () { - var mapping; - var edge = this.get('edge'); - edge.getData('synapses').push(this); - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - edge.getData('mappings').push(mapping); - } - - return edge; - }, - savedEvent: function() { - Metamaps.Realtime.sendSynapseChange(this); - }, - updateViews: function() { - this.updateCardView(); - this.updateEdgeView(); - }, - updateCardView: function() { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var edge = this.get('edge'); - - // update synapse card, if this synapse is the one open there - if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { - Metamaps.SynapseCard.showCard(edge); - } - }, - updateEdgeView: function() { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var edge = this.get('edge'); - - // update the edge on the map - if (onPageWithSynapseCard && edge) { - Metamaps.Visualize.mGraph.plot(); - } - } - }); - - self.SynapseCollection = Backbone.Collection.extend({ - model: self.Synapse, - url: '/synapses' - }); - - self.Mapping = Backbone.Model.extend({ - urlRoot: '/mappings', - blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "map_id": Metamaps.Active.Map ? Metamaps.Active.Map.id : null - }); - } - }, - getMap: function () { - return Metamaps.Map.get(this.get('map_id')); - }, - getTopic: function () { - if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')); - else return false; - }, - getSynapse: function () { - if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')); - else return false; - } - }); - - self.MappingCollection = Backbone.Collection.extend({ - model: self.Mapping, - url: '/mappings' - }); - - Metamaps.Metacodes = Metamaps.Metacodes ? new self.MetacodeCollection(Metamaps.Metacodes) : new self.MetacodeCollection(); - - Metamaps.Topics = Metamaps.Topics ? new self.TopicCollection(Metamaps.Topics) : new self.TopicCollection(); - - Metamaps.Synapses = Metamaps.Synapses ? new self.SynapseCollection(Metamaps.Synapses) : new self.SynapseCollection(); - - Metamaps.Mappers = Metamaps.Mappers ? new self.MapperCollection(Metamaps.Mappers) : new self.MapperCollection(); - - // this is for topic view - Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection(); - - if (Metamaps.Active.Map) { - Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection(); - - Metamaps.Active.Map = new self.Map(Metamaps.Active.Map); - } - - if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic); - - //attach collection event listeners - self.attachCollectionEvents = function () { - - Metamaps.Topics.on("add remove", function(topic){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkMappers(); - }); - - Metamaps.Synapses.on("add remove", function(synapse){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMappers(); - }); - - if (Metamaps.Active.Map) { - Metamaps.Mappings.on("add remove", function(mapping){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkMappers(); - }); - } - } - self.attachCollectionEvents(); -}; // end Metamaps.Backbone.init - - -/* - * - * CREATE - * - */ -Metamaps.Create = { - isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open - selectedMetacodeSet: null, - selectedMetacodeSetIndex: null, - selectedMetacodeNames: [], - newSelectedMetacodeNames: [], - selectedMetacodes: [], - newSelectedMetacodes: [], - init: function () { - var self = Metamaps.Create; - self.newTopic.init(); - self.newSynapse.init(); - - ////// - ////// - //// SWITCHING METACODE SETS - - $('#metacodeSwitchTabs').tabs({ - selected: self.selectedMetacodeSetIndex - }).addClass("ui-tabs-vertical ui-helper-clearfix"); - $("#metacodeSwitchTabs .ui-tabs-nav li").removeClass("ui-corner-top").addClass("ui-corner-left"); - $('.customMetacodeList li').click(self.toggleMetacodeSelected); // within the custom metacode set tab - }, - toggleMetacodeSelected: function () { - var self = Metamaps.Create; - - if ($(this).attr('class') != 'toggledOff') { - $(this).addClass('toggledOff'); - var value_to_remove = $(this).attr('id'); - var name_to_remove = $(this).attr('data-name'); - self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(value_to_remove), 1); - self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(name_to_remove), 1); - } else if ($(this).attr('class') == 'toggledOff') { - $(this).removeClass('toggledOff'); - self.newSelectedMetacodes.push($(this).attr('id')); - self.newSelectedMetacodeNames.push($(this).attr('data-name')); - } - }, - updateMetacodeSet: function (set, index, custom) { - - if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { - alert('Please select at least one metacode to use!'); - return false; - } - - var codesToSwitchToIds; - var metacodeModels = new Metamaps.Backbone.MetacodeCollection(); - Metamaps.Create.selectedMetacodeSetIndex = index; - Metamaps.Create.selectedMetacodeSet = "metacodeset-" + set; - - if (!custom) { - codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(','); - $('.customMetacodeList li').addClass('toggledOff'); - Metamaps.Create.selectedMetacodes = []; - Metamaps.Create.selectedMetacodeNames = []; - Metamaps.Create.newSelectedMetacodes = []; - Metamaps.Create.newSelectedMetacodeNames = []; - } - else if (custom) { - // uses .slice to avoid setting the two arrays to the same actual array - Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0); - Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0); - codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0); - } - - // sort by name - for (var i = 0; i < codesToSwitchToIds.length; i++) { - metacodeModels.add( Metamaps.Metacodes.get(codesToSwitchToIds[i]) ); - }; - metacodeModels.sort(); - - $('#metacodeImg, #metacodeImgTitle').empty(); - $('#metacodeImg').removeData('cloudcarousel'); - var newMetacodes = ""; - metacodeModels.each(function(metacode){ - newMetacodes += '' + metacode.get('name') + ''; - }); - - $('#metacodeImg').empty().append(newMetacodes).CloudCarousel({ - titleBox: $('#metacodeImgTitle'), - yRadius: 40, - xRadius: 190, - xPos: 170, - yPos: 40, - speed: 0.3, - mouseWheel: true, - bringToFront: true - }); - - Metamaps.GlobalUI.closeLightbox(); - $('#topic_name').focus(); - - var mdata = { - "metacodes": { - "value": custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet - } - }; - $.ajax({ - type: "POST", - dataType: 'json', - url: "/user/updatemetacodes", - data: mdata, - success: function (data) { - console.log('selected metacodes saved'); - }, - error: function () { - console.log('failed to save selected metacodes'); - } - }); - }, - - cancelMetacodeSetSwitch: function () { - var self = Metamaps.Create; - self.isSwitchingSet = false; - - if (self.selectedMetacodeSet != "metacodeset-custom") { - $('.customMetacodeList li').addClass('toggledOff'); - self.selectedMetacodes = []; - self.selectedMetacodeNames = []; - self.newSelectedMetacodes = []; - self.newSelectedMetacodeNames = []; - } else { // custom set is selected - // reset it to the current actual selection - $('.customMetacodeList li').addClass('toggledOff'); - for (var i = 0; i < self.selectedMetacodes.length; i++) { - $('#' + self.selectedMetacodes[i]).removeClass('toggledOff'); - }; - // uses .slice to avoid setting the two arrays to the same actual array - self.newSelectedMetacodeNames = self.selectedMetacodeNames.slice(0); - self.newSelectedMetacodes = self.selectedMetacodes.slice(0); - } - $('#metacodeSwitchTabs').tabs("option", "active", self.selectedMetacodeSetIndex); - $('#topic_name').focus(); - }, - newTopic: { - init: function () { - - $('#topic_name').keyup(function () { - Metamaps.Create.newTopic.name = $(this).val(); - }); - - var topicBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/topics/autocomplete_topic?term=%QUERY', - wildcard: '%QUERY', - }, - }); - - // initialize the autocomplete results for the metacode spinner - $('#topic_name').typeahead( - { - highlight: true, - minLength: 2, - }, - [{ - name: 'topic_autocomplete', - limit: 8, - display: function (s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s); - }, - }, - source: topicBloodhound, - }] - ); - - // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete - $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Metamaps.Topic.getTopicFromAutocomplete(datum.id); - }); - - // initialize metacode spinner and then hide it - $("#metacodeImg").CloudCarousel({ - titleBox: $('#metacodeImgTitle'), - yRadius: 40, - xRadius: 190, - xPos: 170, - yPos: 40, - speed: 0.3, - mouseWheel: true, - bringToFront: true - }); - $('.new_topic').hide(); - }, - name: null, - newId: 1, - beingCreated: false, - metacode: null, - x: null, - y: null, - addSynapse: false, - open: function () { - $('#new_topic').fadeIn('fast', function () { - $('#topic_name').focus(); - }); - Metamaps.Create.newTopic.beingCreated = true; - Metamaps.Create.newTopic.name = ""; - }, - hide: function () { - $('#new_topic').fadeOut('fast'); - $("#topic_name").typeahead('val', ''); - Metamaps.Create.newTopic.beingCreated = false; - } - }, - newSynapse: { - init: function () { - var self = Metamaps.Create.newSynapse; - - var synapseBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/synapses?term=%QUERY', - wildcard: '%QUERY', - }, - }); - var existingSynapseBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', - prepare: function(query, settings) { - var self = Metamaps.Create.newSynapse; - if (Metamaps.Selected.Nodes.length < 2) { - settings.url = settings.url.replace("%TOPIC1", self.topic1id).replace("%TOPIC2", self.topic2id); - return settings; - } else { - return null; - } - }, - }, - }); - - // initialize the autocomplete results for synapse creation - $('#synapse_desc').typeahead( - { - highlight: true, - minLength: 2, - }, - [{ - name: 'synapse_autocomplete', - display: function(s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile("
    {{label}}
    ").render(s); - }, - }, - source: synapseBloodhound, - }, - { - name: 'existing_synapses', - limit: 50, - display: function(s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s); - }, - header: "

    Existing synapses

    " - }, - source: existingSynapseBloodhound, - }] - ); - - $('#synapse_desc').keyup(function (e) { - var ESC = 27, BACKSPACE = 8, DELETE = 46; - if (e.keyCode === BACKSPACE && $(this).val() === "" || - e.keyCode === DELETE && $(this).val() === "" || - e.keyCode === ESC) { - Metamaps.Create.newSynapse.hide(); - }//if - Metamaps.Create.newSynapse.description = $(this).val(); - }); - - $('#synapse_desc').focusout(function() { - if (Metamaps.Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally(); - } - }); - - $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { - if (datum.id) { // if they clicked on an existing synapse get it - Metamaps.Synapse.getSynapseFromAutocomplete(datum.id); - } - else { - Metamaps.Create.newSynapse.description = datum.value; - Metamaps.Synapse.createSynapseLocally(); - } - }); - }, - beingCreated: false, - description: null, - topic1id: null, - topic2id: null, - newSynapseId: null, - open: function () { - $('#new_synapse').fadeIn(100, function () { - $('#synapse_desc').focus(); - }); - Metamaps.Create.newSynapse.beingCreated = true; - }, - hide: function () { - $('#new_synapse').fadeOut('fast'); - $("#synapse_desc").typeahead('val', ''); - Metamaps.Create.newSynapse.beingCreated = false; - Metamaps.Create.newTopic.addSynapse = false; - Metamaps.Create.newSynapse.topic1id = 0; - Metamaps.Create.newSynapse.topic2id = 0; - Metamaps.Mouse.synapseStartCoordinates = []; - Metamaps.Visualize.mGraph.plot(); - }, - } -}; // end Metamaps.Create - - -////////////////// TOPIC AND SYNAPSE CARDS ////////////////////////// - - -/* - * - * TOPICCARD - * - */ -Metamaps.TopicCard = { - openTopicCard: null, //stores the topic that's currently open - authorizedToEdit: false, // stores boolean for edit permission for open topic card - init: function () { - var self = Metamaps.TopicCard; - - // initialize best_in_place editing - $('.authenticated div.permission.canEdit .best_in_place').best_in_place(); - - Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()); - - // initialize topic card draggability and resizability - $('.showcard').draggable({ - handle: ".metacodeImage" - }); - - embedly('on', 'card.rendered', self.embedlyCardRendered); - }, - /** - * Will open the Topic Card for the node that it's passed - * @param {$jit.Graph.Node} node - */ - showCard: function (node) { - var self = Metamaps.TopicCard; - - var topic = node.getData('topic'); - - self.openTopicCard = topic; - self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper); - //populate the card that's about to show with the right topics data - self.populateShowCard(topic); - $('.showcard').fadeIn('fast'); - }, - hideCard: function () { - var self = Metamaps.TopicCard; - - $('.showcard').fadeOut('fast'); - self.openTopicCard = null; - self.authorizedToEdit = false; - }, - embedlyCardRendered: function (iframe) { - var self = Metamaps.TopicCard; - - $('#embedlyLinkLoader').hide(); - - // means that the embedly call returned 404 not found - if ($('#embedlyLink')[0]) { - $('#embedlyLink').css('display', 'block').fadeIn('fast'); - $('.embeds').addClass('nonEmbedlyLink'); - } - - $('.CardOnGraph').addClass('hasAttachment'); - if (self.authorizedToEdit) { - $('.embeds').append('
    '); - $('#linkremove').click(self.removeLink); - } - }, - removeLink: function () { - var self = Metamaps.TopicCard; - self.openTopicCard.save({ - link: null - }); - $('.embeds').empty().removeClass('nonEmbedlyLink'); - $('#addLinkInput input').val(""); - $('.attachments').removeClass('hidden'); - $('.CardOnGraph').removeClass('hasAttachment'); - }, - bindShowCardListeners: function (topic) { - var self = Metamaps.TopicCard; - var showCard = document.getElementById('showcard'); - - var authorized = self.authorizedToEdit; - - // get mapper image - var setMapperImage = function (mapper) { - $('.contributorIcon').attr('src', mapper.get('image')); - }; - Metamaps.Mapper.get(topic.get('user_id'), setMapperImage); - - // starting embed.ly - var resetFunc = function () { - $('#addLinkInput input').val(""); - $('#addLinkInput input').focus(); - }; - var inputEmbedFunc = function (event) { - - var element = this; - setTimeout(function () { - var text = $(element).val(); - if (event.type=="paste" || (event.type=="keyup" && event.which==13)){ - // TODO evaluate converting this to '//' no matter what (infer protocol) - if (text.slice(0, 7) !== 'http://' && - text.slice(0, 8) !== 'https://' && - text.slice(0, 2) !== '//') { - text='//'+text; - } - topic.save({ - link: text - }); - var embedlyEl = $('', { - id: 'embedlyLink', - 'data-card-description': '0', - href: text - }).html(text); - $('.attachments').addClass('hidden'); - $('.embeds').append(embedlyEl); - $('.embeds').append('
    '); - var loader = new CanvasLoader('embedlyLinkLoader'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')); - if (!e) { - self.handleInvalidLink(); - } - } - }, 100); - }; - $('#addLinkReset').click(resetFunc); - $('#addLinkInput input').bind("paste keyup",inputEmbedFunc); - - // initialize the link card, if there is a link - if (topic.get('link') && topic.get('link') !== '') { - var loader = new CanvasLoader('embedlyLinkLoader'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')); - if (!e) { - self.handleInvalidLink(); - } - } - - - var selectingMetacode = false; - // attach the listener that shows the metacode title when you hover over the image - $('.showcard .metacodeImage').mouseenter(function () { - $('.showcard .icon').css('z-index', '4'); - $('.showcard .metacodeTitle').show(); - }); - $('.showcard .linkItem.icon').mouseleave(function () { - if (!selectingMetacode) { - $('.showcard .metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - } - }); - - var metacodeLiClick = function () { - selectingMetacode = false; - var metacodeId = parseInt($(this).attr('data-id')); - var metacode = Metamaps.Metacodes.get(metacodeId); - $('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name')) - .append('
    ') - .attr('class', 'metacodeTitle mbg' + metacode.id); - $('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')'); - topic.save({ - metacode_id: metacode.id - }); - Metamaps.Visualize.mGraph.plot(); - $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge'); - $('.metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - }; - - var openMetacodeSelect = function (event) { - var windowWidth; - var showcardLeft; - var TOPICCARD_WIDTH = 300; - var METACODESELECT_WIDTH = 404; - var distanceFromEdge; - - var MAX_METACODELIST_HEIGHT = 270; - var windowHeight; - var showcardTop; - var topicTitleHeight; - var distanceFromBottom; - - if (!selectingMetacode) { - selectingMetacode = true; - - // this is to make sure the metacode - // select is accessible onscreen, when opened - // while topic card is close to the right - // edge of the screen - windowWidth = $(window).width(); - showcardLeft = parseInt($('.showcard').css('left')); - distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH); - if (distanceFromEdge < METACODESELECT_WIDTH) { - $('.metacodeSelect').addClass('onRightEdge'); - } - - // this is to make sure the metacode - // select is accessible onscreen, when opened - // while topic card is close to the bottom - // edge of the screen - windowHeight = $(window).height(); - showcardTop = parseInt($('.showcard').css('top')); - topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')); - heightOfSetList = $('.showcard .metacodeSelect').height(); - distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight); - if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { - $('.metacodeSelect').addClass('onBottomEdge'); - } - - $('.metacodeSelect').show(); - event.stopPropagation(); - } - }; - - var hideMetacodeSelect = function () { - selectingMetacode = false; - $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge'); - $('.metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - }; - - if (authorized) { - $('.showcard .metacodeTitle').click(openMetacodeSelect); - $('.showcard').click(hideMetacodeSelect); - $('.metacodeSelect > ul > li').click(function (event){ - event.stopPropagation(); - }); - $('.metacodeSelect li li').click(metacodeLiClick); - - var bipName = $(showCard).find('.best_in_place_name'); - bipName.bind("best_in_place:activate", function () { - var $el = bipName.find('textarea'); - var el = $el[0]; - - $el.attr('maxlength', '140'); - - $('.showcard .title').append('
    '); - - var callback = function (data) { - $('.nameCounter.forTopic').html(data.all + '/140'); - }; - Countable.live(el, callback); - }); - bipName.bind("best_in_place:deactivate", function () { - $('.nameCounter.forTopic').remove(); - }); - - //bind best_in_place ajax callbacks - bipName.bind("ajax:success", function () { - var name = Metamaps.Util.decodeEntities($(this).html()); - topic.set("name", name); - topic.trigger('saved'); - }); - - $(showCard).find('.best_in_place_desc').bind("ajax:success", function () { - this.innerHTML = this.innerHTML.replace(/\r/g, ''); - var desc = $(this).html() === $(this).data('nil') ? "" : $(this).html(); - topic.set("desc", desc); - topic.trigger('saved'); - }); - } - - - var permissionLiClick = function (event) { - selectingPermission = false; - var permission = $(this).attr('class'); - topic.save({ - permission: permission - }); - $('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)); - $('.showcard .permissionSelect').remove(); - event.stopPropagation(); - }; - - var openPermissionSelect = function (event) { - if (!selectingPermission) { - selectingPermission = true; - $(this).addClass('minimize'); // this line flips the drop down arrow to a pull up arrow - if ($(this).hasClass('co')) { - $(this).append('
    '); - } else if ($(this).hasClass('pu')) { - $(this).append('
    '); - } else if ($(this).hasClass('pr')) { - $(this).append('
    '); - } - $('.showcard .permissionSelect li').click(permissionLiClick); - event.stopPropagation(); - } - }; - - var hidePermissionSelect = function () { - selectingPermission = false; - $('.showcard .yourTopic .mapPerm').removeClass('minimize'); // this line flips the pull up arrow to a drop down arrow - $('.showcard .permissionSelect').remove(); - }; - // ability to change permission - var selectingPermission = false; - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { - $('.showcard .yourTopic .mapPerm').click(openPermissionSelect); - $('.showcard').click(hidePermissionSelect); - } - - $('.links .mapCount').unbind().click(function(event){ - $('.mapCount .tip').toggle(); - $('.showcard .hoverTip').toggleClass('hide'); - event.stopPropagation(); - }); - $('.mapCount .tip').unbind().click(function(event){ - event.stopPropagation(); - }); - $('.showcard').unbind('.hideTip').bind('click.hideTip', function(){ - $('.mapCount .tip').hide(); - $('.showcard .hoverTip').removeClass('hide'); - }); - - $('.mapCount .tip li a').click(Metamaps.Router.intercept); - - var originalText = $('.showMore').html(); - $('.mapCount .tip .showMore').unbind().toggle( - function(event){ - $('.extraText').toggleClass("hideExtra"); - $('.showMore').html('Show less...'); - }, - function(event){ - $('.extraText').toggleClass("hideExtra"); - $('.showMore').html(originalText); - }); - - $('.mapCount .tip showMore').unbind().click(function(event){ - event.stopPropagation(); - }); - }, - handleInvalidLink: function() { - var self = Metamaps.TopicCard; - - self.removeLink(); - Metamaps.GlobalUI.notifyUser("Invalid link"); - }, - populateShowCard: function (topic) { - var self = Metamaps.TopicCard; - - var showCard = document.getElementById('showcard'); - - $(showCard).find('.permission').remove(); - - var topicForTemplate = self.buildObject(topic); - var html = self.generateShowcardHTML.render(topicForTemplate); - - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { - var perm = document.createElement('div'); - - var string = 'permission canEdit'; - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic'; - perm.className = string; - perm.innerHTML = html; - showCard.appendChild(perm); - } else { - var perm = document.createElement('div'); - perm.className = 'permission cannotEdit'; - perm.innerHTML = html; - showCard.appendChild(perm); - } - - Metamaps.TopicCard.bindShowCardListeners(topic); - }, - generateShowcardHTML: null, // will be initialized into a Hogan template within init function - //generateShowcardHTML - buildObject: function (topic) { - var self=Metamaps.TopicCard; - - var nodeValues = {}; - - var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - - } else { - - } - - var desc_nil = "Click to add description..."; - - nodeValues.attachmentsHidden = ''; - if (topic.get('link') && topic.get('link')!== '') { - nodeValues.embeds = '
    '; - nodeValues.embeds += topic.get('link'); - nodeValues.embeds += '
    '; - nodeValues.attachmentsHidden = 'hidden'; - nodeValues.hasAttachment = "hasAttachment"; - } - else { - nodeValues.embeds = ''; - nodeValues.hasAttachment = ''; - } - - if (authorized) { - nodeValues.attachments = ''; - } else { - nodeValues.attachmentsHidden = 'hidden'; - nodeValues.attachments = ''; - } - - var inmapsAr = topic.get("inmaps"); - var inmapsLinks = topic.get("inmapsLinks"); - nodeValues.inmaps =''; - if (inmapsAr.length < 6) { - for (i = 0; i < inmapsAr.length; i++) { - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i]+ '
  • '; - } - } - else { - for (i = 0; i < 5; i++){ - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • '; - } - extra = inmapsAr.length - 5; - nodeValues.inmaps += '
  • See ' + extra + ' more...
  • ' - for (i = 5; i < inmapsAr.length; i++){ - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i]+ '
  • '; - } - } - nodeValues.permission = topic.get("permission"); - nodeValues.mk_permission = topic.get("permission").substring(0, 2); - nodeValues.map_count = topic.get("map_count").toString(); - nodeValues.synapse_count = topic.get("synapse_count").toString(); - nodeValues.id = topic.isNew() ? topic.cid : topic.id; - nodeValues.metacode = topic.getMetacode().get("name"); - nodeValues.metacode_class = 'mbg' + topic.get('metacode_id'); - nodeValues.imgsrc = topic.getMetacode().get("icon"); - nodeValues.name = topic.get("name"); - nodeValues.userid = topic.get("user_id"); - nodeValues.username = topic.get("user_name"); - nodeValues.date = topic.getDate(); - // the code for this is stored in /views/main/_metacodeOptions.html.erb - nodeValues.metacode_select = $('#metacodeOptions').html(); - nodeValues.desc_nil = desc_nil; - nodeValues.desc = (topic.get("desc") == "" && authorized) ? desc_nil : topic.get("desc"); - return nodeValues; - } -}; // end Metamaps.TopicCard - - -/* - * - * SYNAPSECARD - * - */ -Metamaps.SynapseCard = { - openSynapseCard: null, - showCard: function (edge, e) { - var self = Metamaps.SynapseCard; - - //reset so we don't interfere with other edges, but first, save its x and y - var myX = $('#edit_synapse').css('left'); - var myY = $('#edit_synapse').css('top'); - $('#edit_synapse').remove(); - - //so label is missing while editing - Metamaps.Control.deselectEdge(edge); - - var index = edge.getData("displayIndex") ? edge.getData("displayIndex") : 0; - var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse - - //create the wrapper around the form elements, including permissions - //classes to make best_in_place happy - var edit_div = document.createElement('div'); - edit_div.innerHTML = '
    '; - edit_div.setAttribute('id', 'edit_synapse'); - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - edit_div.className = 'permission canEdit'; - edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : ''; - } else { - edit_div.className = 'permission cannotEdit'; - } - $('#wrapper').append(edit_div); - - self.populateShowCard(edge, synapse); - - //drop it in the right spot, activate it - $('#edit_synapse').css('position', 'absolute'); - if (e) { - $('#edit_synapse').css('left', e.clientX); - $('#edit_synapse').css('top', e.clientY); - } else { - $('#edit_synapse').css('left', myX); - $('#edit_synapse').css('top', myY); - } - //$('#edit_synapse_name').click(); //required in case name is empty - //$('#edit_synapse_name input').focus(); - $('#edit_synapse').show(); - - self.openSynapseCard = edge; - }, - - hideCard: function () { - $('#edit_synapse').remove(); - Metamaps.SynapseCard.openSynapseCard = null; - }, - - populateShowCard: function (edge, synapse) { - var self = Metamaps.SynapseCard; - - self.add_synapse_count(edge); - self.add_desc_form(synapse); - self.add_drop_down(edge, synapse); - self.add_user_info(synapse); - self.add_perms_form(synapse); - self.add_direction_form(synapse); - }, - add_synapse_count: function (edge) { - var count = edge.getData("synapses").length; - - $('#editSynUpperBar').append('
    ' + count + '
    ') - }, - add_desc_form: function (synapse) { - var data_nil = 'Click to add description.'; - - // TODO make it so that this would work even in sandbox mode, - // currently with Best_in_place it won't - - //desc editing form - $('#editSynUpperBar').append('
    '); - $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc'); - $('#edit_synapse_desc').attr('data-object', 'synapse'); - $('#edit_synapse_desc').attr('data-attribute', 'desc'); - $('#edit_synapse_desc').attr('data-type', 'textarea'); - $('#edit_synapse_desc').attr('data-nil', data_nil); - $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id); - $('#edit_synapse_desc').html(synapse.get("desc")); - - //if edge data is blank or just whitespace, populate it with data_nil - if ($('#edit_synapse_desc').html().trim() == '') { - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - $('#edit_synapse_desc').html(data_nil); - } - else { - $('#edit_synapse_desc').html("(no description)"); - } - } - - $('#edit_synapse_desc').bind("ajax:success", function () { - var desc = $(this).html(); - if (desc == data_nil) { - synapse.set("desc", ''); - } else { - synapse.set("desc", desc); - } - synapse.trigger('saved'); - Metamaps.Control.selectEdge(synapse.get('edge')); - Metamaps.Visualize.mGraph.plot(); - }); - }, - add_drop_down: function (edge, synapse) { - var list, i, synapses, l, desc; - - synapses = edge.getData("synapses"); - l = synapses.length; - - if (l > 1) { - // append the element that you click to show dropdown select - $('#editSynUpperBar').append(''); - $('#dropdownSynapses').click(function(e){ - e.preventDefault(); - e.stopPropagation(); // stop it from immediately closing it again - $('#switchSynapseList').toggle(); - }); - // hide the dropdown again if you click anywhere else on the synapse card - $('#edit_synapse').click(function(){ - $('#switchSynapseList').hide(); - }); - - // generate the list of other synapses - list = '' - // add the list of the other synapses - $('#editSynLowerBar').append(list); - - // attach click listeners to list items that - // will cause it to switch the displayed synapse - // when you click it - $('#switchSynapseList li').click(function(e){ - e.stopPropagation(); - var index = parseInt($(this).attr('data-synapse-index')); - edge.setData('displayIndex', index); - Metamaps.Visualize.mGraph.plot(); - Metamaps.SynapseCard.showCard(edge, false); - }); - } - }, - add_user_info: function (synapse) { - var u = '
    '; - u += ' ' - u += '
    ' + synapse.get("user_name") + '
    '; - $('#editSynLowerBar').append(u); - - // get mapper image - var setMapperImage = function (mapper) { - $('#edgeUser img').attr('src', mapper.get('image')); - }; - Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage); - }, - - add_perms_form: function (synapse) { - //permissions - if owner, also allow permission editing - $('#editSynLowerBar').append('
    '); - - // ability to change permission - var selectingPermission = false; - var permissionLiClick = function (event) { - selectingPermission = false; - var permission = $(this).attr('class'); - synapse.save({ - permission: permission - }); - $('#edit_synapse .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)); - $('#edit_synapse .permissionSelect').remove(); - event.stopPropagation(); - }; - - var openPermissionSelect = function (event) { - if (!selectingPermission) { - selectingPermission = true; - $(this).addClass('minimize'); // this line flips the drop down arrow to a pull up arrow - if ($(this).hasClass('co')) { - $(this).append(''); - } else if ($(this).hasClass('pu')) { - $(this).append(''); - } else if ($(this).hasClass('pr')) { - $(this).append(''); - } - $('#edit_synapse .permissionSelect li').click(permissionLiClick); - event.stopPropagation(); - } - }; - - var hidePermissionSelect = function () { - selectingPermission = false; - $('#edit_synapse.yourEdge .mapPerm').removeClass('minimize'); // this line flips the pull up arrow to a drop down arrow - $('#edit_synapse .permissionSelect').remove(); - }; - - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { - $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect); - $('#edit_synapse').click(hidePermissionSelect); - } - }, //add_perms_form - - add_direction_form: function (synapse) { - //directionality checkboxes - $('#editSynLowerBar').append('
    '); - $('#editSynLowerBar').append('
    '); - - var edge = synapse.get('edge'); - - //determine which node is to the left and the right - //if directly in a line, top is left - if (edge.nodeFrom.pos.x < edge.nodeTo.pos.x || - edge.nodeFrom.pos.x == edge.nodeTo.pos.x && - edge.nodeFrom.pos.y < edge.nodeTo.pos.y) { - var left = edge.nodeTo.getData("topic"); - var right = edge.nodeFrom.getData("topic"); - } else { - var left = edge.nodeFrom.getData("topic"); - var right = edge.nodeTo.getData("topic"); - } - - /* - * One node is actually on the left onscreen. Call it left, & the other right. - * If category is from-to, and that node is first, check the 'right' checkbox. - * Else check the 'left' checkbox since the arrow is incoming. - */ - - var directionCat = synapse.get('category'); //both, none, from-to - if (directionCat == 'from-to') { - var from_to = [synapse.get("node1_id"), synapse.get("node2_id")]; - if (from_to[0] == left.id) { - //check left checkbox - $('#edit_synapse_left').addClass('checked'); - } else { - //check right checkbox - $('#edit_synapse_right').addClass('checked'); - } - } else if (directionCat == 'both') { - //check both checkboxes - $('#edit_synapse_left').addClass('checked'); - $('#edit_synapse_right').addClass('checked'); - } - - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - $('#edit_synapse_left, #edit_synapse_right').click(function () { - - $(this).toggleClass('checked'); - - var leftChecked = $('#edit_synapse_left').is('.checked'); - var rightChecked = $('#edit_synapse_right').is('.checked'); - - var dir = synapse.getDirection(); - var dirCat = 'none'; - if (leftChecked && rightChecked) { - dirCat = 'both'; - } else if (!leftChecked && rightChecked) { - dirCat = 'from-to'; - dir = [right.id, left.id]; - } else if (leftChecked && !rightChecked) { - dirCat = 'from-to'; - dir = [left.id, right.id]; - } - - synapse.save({ - category: dirCat, - node1_id: dir[0], - node2_id: dir[1] - }); - Metamaps.Visualize.mGraph.plot(); - }); - } // if - } //add_direction_form -}; // end Metamaps.SynapseCard - - -////////////////////// END TOPIC AND SYNAPSE CARDS ////////////////////////////////// - - -/* - * - * VISUALIZE - * - */ -Metamaps.Visualize = { - mGraph: null, // a reference to the graph object. - cameraPosition: null, // stores the camera position when using a 3D visualization - type: "ForceDirected", // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" - loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created - init: function () { - var self = Metamaps.Visualize; - // disable awkward dragging of the canvas element that would sometimes happen - $('#infovis-canvas').on('dragstart', function (event) { - event.preventDefault(); - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchstart', function (event) { - event.preventDefault(); - self.mGraph.events.touched = true; - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchmove', function (event) { - //Metamaps.JIT.touchPanZoomHandler(event); - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchend touchcancel', function (event) { - lastDist = 0; - if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard(); - self.mGraph.events.touched = self.mGraph.events.touchMoved = false; - Metamaps.Touch.touchDragNode = false; - }); - }, - computePositions: function () { - var self = Metamaps.Visualize, - mapping; - - if (self.type == "RGraph") { - var i, l, startPos, endPos, topic, synapse; - - self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id); - topic.set({ node: n }, { silent: true }); - topic.updateNode(); - - n.eachAdjacency(function (edge) { - if(!edge.getData('init')) { - edge.setData('init', true); - - l = edge.getData('synapseIDs').length; - for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]); - synapse.set({ edge: edge }, { silent: true }); - synapse.updateEdge(); - } - } - }); - - var pos = n.getPos(); - pos.setc(-200, -200); - }); - self.mGraph.compute('end'); - } else if (self.type == "ForceDirected") { - var i, l, startPos, endPos, topic, synapse; - - self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id); - topic.set({ node: n }, { silent: true }); - topic.updateNode(); - mapping = topic.getMapping(); - - n.eachAdjacency(function (edge) { - if(!edge.getData('init')) { - edge.setData('init', true); - - l = edge.getData('synapseIDs').length; - for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]); - synapse.set({ edge: edge }, { silent: true }); - synapse.updateEdge(); - } - } - }); - - startPos = new $jit.Complex(0, 0); - endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')); - n.setPos(startPos, 'start'); - n.setPos(endPos, 'end'); - }); - } else if (self.type == "ForceDirected3D") { - self.mGraph.compute(); - } - }, - /** - * render does the heavy lifting of creating the engine that renders the graph with the properties we desire - * - */ - render: function () { - var self = Metamaps.Visualize, RGraphSettings, FDSettings; - - if (self.type == "RGraph" && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { - - RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings); - - $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings); - $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings); - - RGraphSettings.width = $(document).width(); - RGraphSettings.height = $(document).height(); - RGraphSettings.background = Metamaps.JIT.RGraph.background; - RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance; - - self.mGraph = new $jit.RGraph(RGraphSettings); - - } else if (self.type == "ForceDirected" && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { - - FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings); - - $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings); - $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings); - - FDSettings.width = $('body').width(); - FDSettings.height = $('body').height(); - - self.mGraph = new $jit.ForceDirected(FDSettings); - - } else if (self.type == "ForceDirected3D" && !self.mGraph) { - // init ForceDirected3D - self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings); - self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position; - } - else { - self.mGraph.graph.empty(); - } - - function runAnimation() { - Metamaps.Loading.hide(); - // load JSON data, if it's not empty - if (!self.loadLater) { - //load JSON data. - var rootIndex = 0; - if (Metamaps.Active.Topic) { - var node = _.find(Metamaps.JIT.vizData, function(node){ - return node.id === Metamaps.Active.Topic.id; - }); - rootIndex = _.indexOf(Metamaps.JIT.vizData, node); - } - self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex); - //compute positions and plot. - self.computePositions(); - self.mGraph.busy = true; - if (self.type == "RGraph") { - self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate); - } else if (self.type == "ForceDirected") { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout); - } else if (self.type == "ForceDirected3D") { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout); - } - } - } - // hold until all the needed metacode images are loaded - // hold for a maximum of 80 passes, or 4 seconds of waiting time - var tries = 0; - function hold() { - var unique = _.uniq(Metamaps.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), - requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), - loadedCount = 0; - - _.each(requiredMetacodes, function (metacode_id) { - var metacode = Metamaps.Metacodes.get(metacode_id), - img = metacode ? metacode.get('image') : false; - - if (img && (img.complete || (typeof img.naturalWidth !== "undefined" && img.naturalWidth !== 0))) { - loadedCount += 1; - } - }); - - if (loadedCount === requiredMetacodes.length || tries > 80) runAnimation(); - else setTimeout(function(){ tries++; hold() }, 50); - } - hold(); - - // update the url now that the map is ready - clearTimeout(Metamaps.Router.timeoutId); - Metamaps.Router.timeoutId = setTimeout(function(){ - var m = Metamaps.Active.Map; - var t = Metamaps.Active.Topic; - - if (m && window.location.pathname !== "/maps/" + m.id) { - Metamaps.Router.navigate("/maps/" + m.id); - } - else if (t && window.location.pathname !== "/topics/" + t.id) { - Metamaps.Router.navigate("/topics/" + t.id); - } - }, 800); - - } -}; // end Metamaps.Visualize - - -/* - * - * UTIL - * - */ -Metamaps.Util = { - // helper function to determine how many lines are needed - // Line Splitter Function - // copyright Stephen Chapman, 19th April 2006 - // you may copy this code but please keep the copyright notice as well - splitLine: function (st, n) { - var b = ''; - var s = st ? st : ''; - while (s.length > n) { - var c = s.substring(0, n); - var d = c.lastIndexOf(' '); - var e = c.lastIndexOf('\n'); - if (e != -1) d = e; - if (d == -1) d = n; - b += c.substring(0, d) + '\n'; - s = s.substring(d + 1); - } - return b + s; - }, - nowDateFormatted: function () { - var date = new Date(Date.now()); - var month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1); - var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate(); - var year = date.getFullYear(); - - return month + '/' + day + '/' + year; - }, - decodeEntities: function (desc) { - var str, temp = document.createElement('p'); - temp.innerHTML = desc; //browser handles the topics - str = temp.textContent || temp.innerText; - temp = null; //delete the element; - return str; - }, //decodeEntities - getDistance: function (p1, p2) { - return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)); - }, - coordsToPixels: function (coords) { - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - var pixels = { - x: (coords.x / (1/sx)) + p.x + s.width/2 + ox, - y: (coords.y / (1/sy)) + p.y + s.height/2 + oy - }; - return pixels; - } - else { - return { - x: 0, - y: 0 - }; - } - }, - pixelsToCoords: function (pixels) { - var coords; - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - coords = { - x: (pixels.x - p.x - s.width/2 - ox) * (1/sx), - y: (pixels.y - p.y - s.height/2 - oy) * (1/sy), - }; - } - else { - coords = { - x: 0, - y: 0 - }; - } - return coords; - }, - getPastelColor: function () { - var r = (Math.round(Math.random()* 127) + 127).toString(16); - var g = (Math.round(Math.random()* 127) + 127).toString(16); - var b = (Math.round(Math.random()* 127) + 127).toString(16); - return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4); - }, - // darkens a hex value by 'lum' percentage - colorLuminance: function (hex, lum) { - - // validate hex string - hex = String(hex).replace(/[^0-9a-f]/gi, ''); - if (hex.length < 6) { - hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; - } - lum = lum || 0; - - // convert to decimal and change luminosity - var rgb = "#", c, i; - for (i = 0; i < 3; i++) { - c = parseInt(hex.substr(i*2,2), 16); - c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); - rgb += ("00"+c).substr(c.length); - } - - return rgb; - }, - generateOptionsList: function (data) { - var newlist = ""; - for (var i = 0; i < data.length; i++) { - newlist = newlist + ''; - } - return newlist; - }, - checkURLisImage: function (url) { - // when the page reloads the following regular expression will be screwed up - // please replace it with this one before you save: /*backslashhere*.(jpeg|jpg|gif|png)$/ - return (url.match(/\.(jpeg|jpg|gif|png)$/) != null); - }, - checkURLisYoutubeVideo: function (url) { - return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null); - } -}; // end Metamaps.Util - -/* - * - * REALTIME - * - */ -Metamaps.Realtime = { - videoId: 'video-wrapper', - socket: null, - webrtc: null, - readyToCall: false, - mappersOnMap: {}, - disconnected: false, - chatOpen: false, - status: true, // stores whether realtime is True/On or False/Off, - broadcastingStatus: false, - inConversation: false, - localVideo: null, - init: function () { - var self = Metamaps.Realtime; - - self.addJuntoListeners(); - - self.socket = new SocketIoConnection({ url: '<%= ENV['REALTIME_SERVER'] %>' }); - self.socket.on('connect', function () { - console.log('connected'); - if (!self.disconnected) { - self.startActiveMap(); - } else self.disconnected = false; - }); - self.socket.on('disconnect', function () { - self.disconnected = true; - }); - - if (Metamaps.Active.Mapper) { - - self.webrtc = new SimpleWebRTC({ - connection: self.socket, - localVideoEl: self.videoId, - remoteVideosEl: '', - detectSpeakingEvents: true, - autoAdjustMic: false, //true, - autoRequestMedia: false, - localVideo: { - autoplay: true, - mirror: true, - muted: true - }, - media: { - video: true, - audio: true - }, - nick: Metamaps.Active.Mapper.id - }); - - var - $video = $('').attr('id', self.videoId); - self.localVideo = { - $video: $video, - view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { - DOUBLE_CLICK_TOLERANCE: 200, - avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' - }) - }; - - self.room = new Metamaps.Views.room({ - webrtc: self.webrtc, - socket: self.socket, - username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', - image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', - room: 'global', - $video: self.localVideo.$video, - myVideoView: self.localVideo.view, - config: { DOUBLE_CLICK_TOLERANCE: 200 } - }); - self.room.videoAdded(self.handleVideoAdded); - - self.room.chat.$container.hide(); - $('body').prepend(self.room.chat.$container); - } // if Metamaps.Active.Mapper - }, - addJuntoListeners: function () { - var self = Metamaps.Realtime; - - $(document).on(Metamaps.Views.chatView.events.openTray, function () { - $('.main').addClass('compressed'); - self.chatOpen = true; - self.positionPeerIcons(); - }); - $(document).on(Metamaps.Views.chatView.events.closeTray, function () { - $('.main').removeClass('compressed'); - self.chatOpen = false; - self.positionPeerIcons(); - }); - $(document).on(Metamaps.Views.chatView.events.videosOn, function () { - $('#wrapper').removeClass('hideVideos'); - }); - $(document).on(Metamaps.Views.chatView.events.videosOff, function () { - $('#wrapper').addClass('hideVideos'); - }); - $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { - $('#wrapper').removeClass('hideCursors'); - }); - $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { - $('#wrapper').addClass('hideCursors'); - }); - }, - handleVideoAdded: function (v, id) { - var self = Metamaps.Realtime; - self.positionVideos(); - v.setParent($('#wrapper')); - v.$container.find('.video-cutoff').css({ - border: '4px solid ' + self.mappersOnMap[id].color - }); - $('#wrapper').append(v.$container); - }, - positionVideos: function () { - var self = Metamaps.Realtime; - var videoIds = Object.keys(self.room.videos); - var numOfVideos = videoIds.length; - var numOfVideosToPosition = _.filter(videoIds, function (id) { - return !self.room.videos[id].manuallyPositioned; - }).length; - - var screenHeight = $(document).height(); - var screenWidth = $(document).width(); - var topExtraPadding = 20; - var topPadding = 30; - var leftPadding = 30; - var videoHeight = 150; - var videoWidth = 180; - var column = 0; - var row = 0; - var yFormula = function () { - var y = topExtraPadding + (topPadding + videoHeight)*row + topPadding; - if (y + videoHeight > screenHeight) { - row = 0; - column += 1; - y = yFormula(); - } - row++; - return y; - }; - var xFormula = function () { - var x = (leftPadding + videoWidth)*column + leftPadding; - return x; - }; - - // do self first - var myVideo = Metamaps.Realtime.localVideo.view; - if (!myVideo.manuallyPositioned) { - myVideo.$container.css({ - top: yFormula() + 'px', - left: xFormula() + 'px' - }); - } - videoIds.forEach(function (id) { - var video = self.room.videos[id]; - if (!video.manuallyPositioned) { - video.$container.css({ - top: yFormula() + 'px', - left: xFormula() + 'px' - }); - } - }); - }, - startActiveMap: function () { - var self = Metamaps.Realtime; - - if (Metamaps.Active.Map && Metamaps.Active.Mapper) { - var commonsMap = Metamaps.Active.Map.get('permission') === 'commons'; - var publicMap = Metamaps.Active.Map.get('permission') === 'public'; - - if (commonsMap) { - self.turnOn(); - self.setupSocket(); - } - else if (publicMap) { - self.attachMapListener(); - } - self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true); - } - }, - endActiveMap: function () { - var self = Metamaps.Realtime; - - $(document).off('mousemove'); - self.socket.removeAllListeners(); - if (self.inConversation) self.leaveCall(); - self.socket.emit('endMapperNotify'); - $(".collabCompass").remove(); - self.status = false; - self.room.leave(); - self.room.chat.$container.hide(); - self.room.chat.close(); - }, - reenableRealtime: function() { - var confirmString = "The layout of your map has fallen out of sync with the saved copy. "; - confirmString += "To save your changes without overwriting the map, hit 'Cancel' and "; - confirmString += "then use 'Save to new map'. "; - confirmString += "Do you want to discard your changes and enable realtime?"; - var c = confirm(confirmString); - if (c) { - Metamaps.Router.maps(Metamaps.Active.Map.id); - } - }, - turnOn: function (notify) { - var self = Metamaps.Realtime; - - if (notify) self.sendRealtimeOn(); - //$(".rtMapperSelf").removeClass('littleRtOff').addClass('littleRtOn'); - //$('.rtOn').addClass('active'); - //$('.rtOff').removeClass('active'); - self.status = true; - //$(".sidebarCollaborateIcon").addClass("blue"); - $(".collabCompass").show(); - self.room.chat.$container.show(); - self.room.room = 'map-' + Metamaps.Active.Map.id; - self.checkForACallToJoin(); - - self.activeMapper = { - id: Metamaps.Active.Mapper.id, - name: Metamaps.Active.Mapper.get('name'), - username: Metamaps.Active.Mapper.get('name'), - image: Metamaps.Active.Mapper.get('image'), - color: Metamaps.Util.getPastelColor(), - self: true - }; - self.localVideo.view.$container.find('.video-cutoff').css({ - border: '4px solid ' + self.activeMapper.color - }); - self.room.chat.addParticipant(self.activeMapper); - }, - checkForACallToJoin: function () { - var self = Metamaps.Realtime; - self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }); - }, - promptToJoin: function () { - var self = Metamaps.Realtime; - - var notifyText = 'There\'s a conversation happening, want to join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - self.room.conversationInProgress(); - }, - conversationHasBegun: function () { - var self = Metamaps.Realtime; - - if (self.inConversation) return; - var notifyText = 'There\'s a conversation starting, want to join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - self.room.conversationInProgress(); - }, - countOthersInConversation: function () { - var self = Metamaps.Realtime; - var count = 0; - - for (var key in self.mappersOnMap) { - if (self.mappersOnMap[key].inConversation) count++; - } - return count; - }, - mapperJoinedCall: function (id) { - var self = Metamaps.Realtime; - var mapper = self.mappersOnMap[id]; - - if (mapper) { - if (self.inConversation) { - var username = mapper.name; - var notifyText = username + ' joined the call'; - Metamaps.GlobalUI.notifyUser(notifyText); - } - - mapper.inConversation = true; - self.room.chat.mapperJoinedCall(id); - } - }, - mapperLeftCall: function (id) { - var self = Metamaps.Realtime; - var mapper = self.mappersOnMap[id]; - - if (mapper) { - if (self.inConversation) { - var username = mapper.name; - var notifyText = username + ' left the call'; - Metamaps.GlobalUI.notifyUser(notifyText); - } - - mapper.inConversation = false; - self.room.chat.mapperLeftCall(id); - - if ((self.inConversation && self.countOthersInConversation() === 0) || - (!self.inConversation && self.countOthersInConversation() === 1)) { - self.callEnded(); - } - } - }, - callEnded: function () { - var self = Metamaps.Realtime; - - self.room.conversationEnding(); - self.room.leaveVideoOnly(); - self.inConversation = false; - self.localVideo.view.$container.hide().css({ - top: '72px', - left: '30px' - }); - self.localVideo.view.audioOn(); - self.localVideo.view.videoOn(); - self.webrtc.webrtc.localStreams.forEach(function (stream) { - stream.getTracks().forEach(function (track) { - track.stop(); - }); - }); - self.webrtc.webrtc.localStreams = []; - }, - invitedToCall: function (inviter) { - var self = Metamaps.Realtime; - - self.room.chat.sound.stop('sessioninvite'); - self.room.chat.sound.play('sessioninvite'); - - var username = self.mappersOnMap[inviter].name; - var notifyText = "' style='display: inline-block; margin-top: -12px; vertical-align: top;' />"; - notifyText += username + ' is inviting you to a conversation. Join live?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - }, - invitedToJoin: function (inviter) { - var self = Metamaps.Realtime; - - self.room.chat.sound.stop('sessioninvite'); - self.room.chat.sound.play('sessioninvite'); - - var username = self.mappersOnMap[inviter].name; - var notifyText = username + ' is inviting you to the conversation. Join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - }, - acceptCall: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('callAccepted', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation'); - self.joinCall(); - Metamaps.GlobalUI.clearNotify(); - }, - denyCall: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('callDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - Metamaps.GlobalUI.clearNotify(); - }, - denyInvite: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('inviteDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - Metamaps.GlobalUI.clearNotify(); - }, - inviteACall: function (userid) { - var self = Metamaps.Realtime; - self.socket.emit('inviteACall', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, - invited: userid - }); - self.room.chat.invitationPending(userid); - Metamaps.GlobalUI.clearNotify(); - }, - inviteToJoin: function (userid) { - var self = Metamaps.Realtime; - self.socket.emit('inviteToJoin', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, - invited: userid - }); - self.room.chat.invitationPending(userid); - }, - callAccepted: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser('Conversation starting...'); - self.joinCall(); - self.room.chat.invitationAnswered(userid); - }, - callDenied: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invitation'); - self.room.chat.invitationAnswered(userid); - }, - inviteDenied: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invitation'); - self.room.chat.invitationAnswered(userid); - }, - joinCall: function () { - var self = Metamaps.Realtime; - - self.webrtc.off('readyToCall'); - self.webrtc.once('readyToCall', function () { - self.videoInitialized = true; - self.readyToCall = true; - self.localVideo.view.manuallyPositioned = false; - self.positionVideos(); - self.localVideo.view.$container.show(); - if (self.localVideo && self.status) { - $('#wrapper').append(self.localVideo.view.$container); - } - self.room.join(); - }); - self.inConversation = true; - self.socket.emit('mapperJoinedCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id - }); - self.webrtc.startLocalVideo(); - Metamaps.GlobalUI.clearNotify(); - self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id); - }, - leaveCall: function () { - var self = Metamaps.Realtime; - - self.socket.emit('mapperLeftCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id - }); - - self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id); - self.room.leaveVideoOnly(); - self.inConversation = false; - self.localVideo.view.$container.hide(); - - // if there's only two people in the room, and we're leaving - // we should shut down the call locally - if (self.countOthersInConversation() === 1) { - self.callEnded(); - } - }, - turnOff: function (silent) { - var self = Metamaps.Realtime; - - if (self.status) { - if (!silent) self.sendRealtimeOff(); - //$(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff'); - //$('.rtOn').removeClass('active'); - //$('.rtOff').addClass('active'); - self.status = false; - //$(".sidebarCollaborateIcon").removeClass("blue"); - $(".collabCompass").hide(); - $('#' + self.videoId).remove(); - } - }, - setupSocket: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - var myId = Metamaps.Active.Mapper.id; - - socket.emit('newMapperNotify', { - userid: myId, - username: Metamaps.Active.Mapper.get("name"), - userimage: Metamaps.Active.Mapper.get("image"), - mapid: Metamaps.Active.Map.id - }); - - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall); // new call - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin); // call already in progress - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted); - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied); - socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied); - - // receive word that there's a conversation in progress - socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin); - socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun); - - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall); - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall); - - // if you're the 'new guy' update your list with who's already online - socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList); - - // receive word that there's a new mapper on the map - socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap); - - // receive word that a mapper left the map - socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap); - - // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator); - - // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse); - - // update mapper compass position - socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords); - - // deletions - socket.on('deleteTopicFromServer', self.removeTopic); - socket.on('deleteSynapseFromServer', self.removeSynapse); - - socket.on('topicChangeFromServer', self.topicChange); - socket.on('synapseChangeFromServer', self.synapseChange); - self.attachMapListener(); - - // local event listeners that trigger events - var sendCoords = function (event) { - var pixels = { - x: event.pageX, - y: event.pageY - }; - var coords = Metamaps.Util.pixelsToCoords(pixels); - self.sendCoords(coords); - }; - $(document).mousemove(sendCoords); - - var zoom = function (event, e) { - if (e) { - var pixels = { - x: e.pageX, - y: e.pageY - }; - var coords = Metamaps.Util.pixelsToCoords(pixels); - self.sendCoords(coords); - } - self.positionPeerIcons(); - }; - $(document).on(Metamaps.JIT.events.zoom, zoom); - - $(document).on(Metamaps.JIT.events.pan, self.positionPeerIcons); - - var sendTopicDrag = function (event, positions) { - self.sendTopicDrag(positions); - }; - $(document).on(Metamaps.JIT.events.topicDrag, sendTopicDrag); - - var sendNewTopic = function (event, data) { - self.sendNewTopic(data); - }; - $(document).on(Metamaps.JIT.events.newTopic, sendNewTopic); - - var sendDeleteTopic = function (event, data) { - self.sendDeleteTopic(data); - }; - $(document).on(Metamaps.JIT.events.deleteTopic, sendDeleteTopic); - - var sendRemoveTopic = function (event, data) { - self.sendRemoveTopic(data); - }; - $(document).on(Metamaps.JIT.events.removeTopic, sendRemoveTopic); - - var sendNewSynapse = function (event, data) { - self.sendNewSynapse(data); - }; - $(document).on(Metamaps.JIT.events.newSynapse, sendNewSynapse); - - var sendDeleteSynapse = function (event, data) { - self.sendDeleteSynapse(data); - }; - $(document).on(Metamaps.JIT.events.deleteSynapse, sendDeleteSynapse); - - var sendRemoveSynapse = function (event, data) { - self.sendRemoveSynapse(data); - }; - $(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse); - - var sendNewMessage = function (event, data) { - self.sendNewMessage(data); - }; - $(document).on(Metamaps.Views.room.events.newMessage, sendNewMessage); - - }, - attachMapListener: function(){ - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - socket.on('mapChangeFromServer', self.mapChange); - }, - sendRealtimeOn: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // send this new mapper back your details, and the awareness that you're online - var update = { - username: Metamaps.Active.Mapper.get("name"), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('notifyStartRealtime', update); - }, - sendRealtimeOff: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // send this new mapper back your details, and the awareness that you're online - var update = { - username: Metamaps.Active.Mapper.get("name"), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('notifyStopRealtime', update); - }, - updateMapperList: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - // data.userimage - // data.userrealtime - - self.mappersOnMap[data.userid] = { - id: data.userid, - name: data.username, - username: data.username, - image: data.userimage, - color: Metamaps.Util.getPastelColor(), - realtime: data.userrealtime, - inConversation: data.userinconversation, - coords: { - x: 0, - y: 0 - } - }; - - if (data.userid !== Metamaps.Active.Mapper.id) { - self.room.chat.addParticipant(self.mappersOnMap[data.userid]); - if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid); - - // create a div for the collaborators compass - self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); - } - }, - newPeerOnMap: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - // data.userimage - // data.coords - var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0; - - self.mappersOnMap[data.userid] = { - id: data.userid, - name: data.username, - username: data.username, - image: data.userimage, - color: Metamaps.Util.getPastelColor(), - realtime: true, - coords: { - x: 0, - y: 0 - }, - }; - - // create an item for them in the realtime box - if (data.userid !== Metamaps.Active.Mapper.id && self.status) { - self.room.chat.sound.play('joinmap'); - self.room.chat.addParticipant(self.mappersOnMap[data.userid]); - - // create a div for the collaborators compass - self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); - - var notifyMessage = data.username + ' just joined the map'; - if (firstOtherPerson) { - notifyMessage += ' '; - } - Metamaps.GlobalUI.notifyUser(notifyMessage); - - // send this new mapper back your details, and the awareness that you've loaded the map - var update = { - userToNotify: data.userid, - username: Metamaps.Active.Mapper.get("name"), - userimage: Metamaps.Active.Mapper.get("image"), - userid: Metamaps.Active.Mapper.id, - userrealtime: self.status, - userinconversation: self.inConversation, - mapid: Metamaps.Active.Map.id - }; - socket.emit('updateNewMapperList', update); - } - }, - createCompass: function(name, id, image, color, hide) { - var str = '

    '+name+'

    '; - str += '
    '; - $('#compass' + id).remove(); - $('
    ', { - id: 'compass' + id, - class: 'collabCompass' - }).html(str).appendTo('#wrapper'); - if (hide) { - $('#compass' + id).hide(); - } - $('#compass' + id + ' img').css({ - 'border': '2px solid ' + color - }); - $('#compass' + id + ' p').css({ - 'background-color': color - }); - }, - lostPeerOnMap: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - delete self.mappersOnMap[data.userid]; - self.room.chat.sound.play('leavemap'); - //$('#mapper' + data.userid).remove(); - $('#compass' + data.userid).remove(); - self.room.chat.removeParticipant(data.username); - - Metamaps.GlobalUI.notifyUser(data.username + ' just left the map'); - - if ((self.inConversation && self.countOthersInConversation() === 0) || - (!self.inConversation && self.countOthersInConversation() === 1)) { - self.callEnded(); - } - }, - newCollaborator: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - self.mappersOnMap[data.userid].realtime = true; - - //$('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn'); - $('#compass' + data.userid).show(); - - Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime'); - }, - lostCollaborator: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - self.mappersOnMap[data.userid].realtime = false; - - //$('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff'); - $('#compass' + data.userid).hide(); - - Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime'); - }, - updatePeerCoords: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - self.mappersOnMap[data.userid].coords={x: data.usercoords.x,y:data.usercoords.y}; - self.positionPeerIcon(data.userid); - }, - positionPeerIcons: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - if (self.status) { // if i have realtime turned on - for (var key in self.mappersOnMap) { - var mapper = self.mappersOnMap[key]; - if (mapper.realtime) { - self.positionPeerIcon(key); - } - } - } - }, - positionPeerIcon: function (id) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var boundary = self.chatOpen ? '#wrapper' : document; - var mapper = self.mappersOnMap[id]; - var xMax=$(boundary).width(); - var yMax=$(boundary).height(); - var compassDiameter=56; - var compassArrowSize=24; - - var origPixels = Metamaps.Util.coordsToPixels(mapper.coords); - var pixels = self.limitPixelsToScreen(origPixels); - $('#compass' + id).css({ - left: pixels.x + 'px', - top: pixels.y + 'px' - }); - /* showing the arrow if the collaborator is off of the viewport screen */ - if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { - - var dy = origPixels.y - pixels.y; //opposite - var dx = origPixels.x - pixels.x; // adjacent - var ratio = dy / dx; - var angle = Math.atan2(dy, dx); - - $('#compassArrow' + id).show().css({ - transform: 'rotate(' + angle + 'rad)', - "-webkit-transform": 'rotate(' + angle + 'rad)', - }); - - if (dx > 0) { - $('#compass' + id).addClass('labelLeft'); - } - } else { - $('#compassArrow' + id).hide(); - $('#compass' + id).removeClass('labelLeft'); - } - }, - limitPixelsToScreen: function (pixels) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var boundary = self.chatOpen ? '#wrapper' : document; - var xLimit, yLimit; - var xMax=$(boundary).width(); - var yMax=$(boundary).height(); - var compassDiameter=56; - var compassArrowSize=24; - - xLimit = Math.max(0 + compassArrowSize, pixels.x); - xLimit = Math.min(xLimit, xMax - compassDiameter); - yLimit = Math.max(0 + compassArrowSize, pixels.y); - yLimit = Math.min(yLimit, yMax - compassDiameter); - - return {x:xLimit,y:yLimit}; - }, - sendCoords: function (coords) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var map = Metamaps.Active.Map; - var mapper = Metamaps.Active.Mapper; - - if (self.status && map.authorizeToEdit(mapper) && socket) { - var update = { - usercoords: coords, - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('updateMapperCoords', update); - } - }, - sendTopicDrag: function (positions) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map && self.status) { - positions.mapid = Metamaps.Active.Map.id; - socket.emit('topicDrag', positions); - } - }, - topicDrag: function (positions) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var topic; - var node; - - if (Metamaps.Active.Map && self.status) { - for (var key in positions) { - topic = Metamaps.Topics.get(key); - if (topic) node = topic.get('node'); - if (node) node.pos.setc(positions[key].x, positions[key].y); - } //for - Metamaps.Visualize.mGraph.plot(); - } - }, - sendTopicChange: function (topic) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - topicId: topic.id - } - - socket.emit('topicChangeFromClient', data); - }, - topicChange: function (data) { - var topic = Metamaps.Topics.get(data.topicId); - if (topic) { - var node = topic.get('node'); - topic.fetch({ - success: function (model) { - model.set({ node: node }); - model.trigger('changeByOther'); - } - }); - } - }, - sendSynapseChange: function (synapse) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - synapseId: synapse.id - } - - socket.emit('synapseChangeFromClient', data); - }, - synapseChange: function (data) { - var synapse = Metamaps.Synapses.get(data.synapseId); - if (synapse) { - // edge reset necessary because fetch causes model reset - var edge = synapse.get('edge'); - synapse.fetch({ - success: function (model) { - model.set({ edge: edge }); - model.trigger('changeByOther'); - } - }); - } - }, - sendMapChange: function (map) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - mapId: map.id - } - - socket.emit('mapChangeFromClient', data); - }, - mapChange: function (data) { - var map = Metamaps.Active.Map; - var isActiveMap = map && data.mapId === map.id; - if (isActiveMap) { - var permBefore = map.get('permission'); - var idBefore = map.id; - map.fetch({ - success: function (model, response) { - - var idNow = model.id; - var permNow = model.get('permission'); - if (idNow !== idBefore) { - Metamaps.Map.leavePrivateMap(); // this means the map has been changed to private - } - else if (permNow === 'public' && permBefore === 'commons') { - Metamaps.Map.commonsToPublic(); - } - else if (permNow === 'commons' && permBefore === 'public') { - Metamaps.Map.publicToCommons(); - } - else { - model.fetchContained(); - model.trigger('changeByOther'); - } - } - }); - } - }, - // newMessage - sendNewMessage: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var message = data.attributes; - message.mapid = Metamaps.Active.Map.id; - socket.emit('newMessage', message); - }, - newMessage: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)); - }, - // newTopic - sendNewTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map && self.status) { - data.mapperid = Metamaps.Active.Mapper.id; - data.mapid = Metamaps.Active.Map.id; - socket.emit('newTopic', data); - } - }, - newTopic: function (data) { - var topic, mapping, mapper, mapperCallback, cancel; - - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - function waitThenRenderTopic() { - if (topic && mapping && mapper) { - Metamaps.Topic.renderTopic(mapping, topic, false, false); - } - else if (!cancel) { - setTimeout(waitThenRenderTopic, 10); - } - } - - mapper = Metamaps.Mappers.get(data.mapperid); - if (mapper === undefined) { - mapperCallback = function (m) { - Metamaps.Mappers.add(m); - mapper = m; - }; - Metamaps.Mapper.get(data.mapperid, mapperCallback); - } - $.ajax({ - url: "/topics/" + data.mappableid + ".json", - success: function (response) { - Metamaps.Topics.add(response); - topic = Metamaps.Topics.get(response.id); - }, - error: function () { - cancel = true; - } - }); - $.ajax({ - url: "/mappings/" + data.mappingid + ".json", - success: function (response) { - Metamaps.Mappings.add(response); - mapping = Metamaps.Mappings.get(response.id); - }, - error: function () { - cancel = true; - } - }); - - waitThenRenderTopic(); - }, - // removeTopic - sendDeleteTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - socket.emit('deleteTopicFromClient', data); - } - }, - // removeTopic - sendRemoveTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('removeTopic', data); - } - }, - removeTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - var topic = Metamaps.Topics.get(data.mappableid); - if (topic) { - var node = topic.get('node'); - var mapping = topic.getMapping(); - Metamaps.Control.hideNode(node.id); - Metamaps.Topics.remove(topic); - Metamaps.Mappings.remove(mapping); - } - }, - // newSynapse - sendNewSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapperid = Metamaps.Active.Mapper.id; - data.mapid = Metamaps.Active.Map.id; - socket.emit('newSynapse', data); - } - }, - newSynapse: function (data) { - var topic1, topic2, node1, node2, synapse, mapping, cancel; - - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - function waitThenRenderSynapse() { - if (synapse && mapping && mapper) { - topic1 = synapse.getTopic1(); - node1 = topic1.get('node'); - topic2 = synapse.getTopic2(); - node2 = topic2.get('node'); - - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false); - } - else if (!cancel) { - setTimeout(waitThenRenderSynapse, 10); - } - } - - mapper = Metamaps.Mappers.get(data.mapperid); - if (mapper === undefined) { - mapperCallback = function (m) { - Metamaps.Mappers.add(m); - mapper = m; - }; - Metamaps.Mapper.get(data.mapperid, mapperCallback); - } - $.ajax({ - url: "/synapses/" + data.mappableid + ".json", - success: function (response) { - Metamaps.Synapses.add(response); - synapse = Metamaps.Synapses.get(response.id); - }, - error: function () { - cancel = true; - } - }); - $.ajax({ - url: "/mappings/" + data.mappingid + ".json", - success: function (response) { - Metamaps.Mappings.add(response); - mapping = Metamaps.Mappings.get(response.id); - }, - error: function () { - cancel = true; - } - }); - waitThenRenderSynapse(); - }, - // deleteSynapse - sendDeleteSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('deleteSynapseFromClient', data); - } - }, - // removeSynapse - sendRemoveSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('removeSynapse', data); - } - }, - removeSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - var synapse = Metamaps.Synapses.get(data.mappableid); - if (synapse) { - var edge = synapse.get('edge'); - var mapping = synapse.getMapping(); - if (edge.getData("mappings").length - 1 === 0) { - Metamaps.Control.hideEdge(edge); - } - - var index = _.indexOf(edge.getData("synapses"), synapse); - edge.getData("mappings").splice(index, 1); - edge.getData("synapses").splice(index, 1); - if (edge.getData("displayIndex")) { - delete edge.data.$displayIndex; - } - Metamaps.Synapses.remove(synapse); - Metamaps.Mappings.remove(mapping); - } - }, -}; // end Metamaps.Realtime - - -/* - * - * CONTROL - * - */ -Metamaps.Control = { - init: function () { - - }, - selectNode: function (node,e) { - var filtered = node.getData('alpha') === 0; - - if (filtered || Metamaps.Selected.Nodes.indexOf(node) != -1) return; - node.selected = true; - node.setData('dim', 30, 'current'); - Metamaps.Selected.Nodes.push(node); - }, - deselectAllNodes: function () { - var l = Metamaps.Selected.Nodes.length; - for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i]; - Metamaps.Control.deselectNode(node); - } - Metamaps.Visualize.mGraph.plot(); - }, - deselectNode: function (node) { - delete node.selected; - node.setData('dim', 25, 'current'); - - //remove the node - Metamaps.Selected.Nodes.splice( - Metamaps.Selected.Nodes.indexOf(node), 1); - }, - deleteSelected: function () { - - if (!Metamaps.Active.Map) return; - - var n = Metamaps.Selected.Nodes.length; - var e = Metamaps.Selected.Edges.length; - var ntext = n == 1 ? "1 topic" : n + " topics"; - var etext = e == 1 ? "1 synapse" : e + " synapses"; - var text = "You have " + ntext + " and " + etext + " selected. "; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - var r = confirm(text + "Are you sure you want to permanently delete them all? This will remove them from all maps they appear on."); - if (r == true) { - Metamaps.Control.deleteSelectedEdges(); - Metamaps.Control.deleteSelectedNodes(); - } - }, - deleteSelectedNodes: function () { // refers to deleting topics permanently - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - var l = Metamaps.Selected.Nodes.length; - for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i]; - Metamaps.Control.deleteNode(node.id); - } - }, - deleteNode: function (nodeid) { // refers to deleting topics permanently - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid); - var topic = node.getData('topic'); - - var permToDelete = Metamaps.Active.Mapper.id === topic.get('user_id') || Metamaps.Active.Mapper.get('admin'); - if (permToDelete) { - var mappableid = topic.id; - var mapping = node.getData('mapping'); - topic.destroy(); - Metamaps.Mappings.remove(mapping); - $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ - mappableid: mappableid - }]); - Metamaps.Control.hideNode(nodeid); - } else { - Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted'); - } - }, - removeSelectedNodes: function () { // refers to removing topics permanently from a map - - if (!Metamaps.Active.Map) return; - - var l = Metamaps.Selected.Nodes.length, - i, - node, - authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i]; - Metamaps.Control.removeNode(node.id); - } - }, - removeNode: function (nodeid) { // refers to removing topics permanently from a map - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - var topic = node.getData('topic'); - var mappableid = topic.id; - var mapping = node.getData('mapping'); - mapping.destroy(); - Metamaps.Topics.remove(topic); - $(document).trigger(Metamaps.JIT.events.removeTopic, [{ - mappableid: mappableid - }]); - Metamaps.Control.hideNode(nodeid); - }, - hideSelectedNodes: function () { - var l = Metamaps.Selected.Nodes.length, - i, - node; - - for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i]; - Metamaps.Control.hideNode(node.id); - } - }, - hideNode: function (nodeid) { - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid); - var graph = Metamaps.Visualize.mGraph; - - Metamaps.Control.deselectNode(node); - - node.setData('alpha', 0, 'end'); - node.eachAdjacency(function (adj) { - adj.setData('alpha', 0, 'end'); - }); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['node-property:alpha', - 'edge-property:alpha' - ], - duration: 500 - }); - setTimeout(function () { - if (nodeid == Metamaps.Visualize.mGraph.root) { // && Metamaps.Visualize.type === "RGraph" - var newroot = _.find(graph.graph.nodes, function(n){ return n.id !== nodeid; }); - graph.root = newroot ? newroot.id : null; - } - Metamaps.Visualize.mGraph.graph.removeNode(nodeid); - }, 500); - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkMappers(); - }, - selectEdge: function (edge) { - var filtered = edge.getData('alpha') === 0; // don't select if the edge is filtered - - if (filtered || Metamaps.Selected.Edges.indexOf(edge) != -1) return; - - var width = Metamaps.Mouse.edgeHoveringOver === edge ? 4 : 2; - edge.setDataset('current', { - showDesc: true, - lineWidth: width, - color: Metamaps.Settings.colors.synapses.selected - }); - Metamaps.Visualize.mGraph.plot(); - - Metamaps.Selected.Edges.push(edge); - }, - deselectAllEdges: function () { - var l = Metamaps.Selected.Edges.length; - for (var i = l - 1; i >= 0; i -= 1) { - var edge = Metamaps.Selected.Edges[i]; - Metamaps.Control.deselectEdge(edge); - } - Metamaps.Visualize.mGraph.plot(); - }, - deselectEdge: function (edge) { - edge.setData('showDesc', false, 'current'); - - edge.setDataset('current', { - lineWidth: 2, - color: Metamaps.Settings.colors.synapses.normal - }); - - if (Metamaps.Mouse.edgeHoveringOver == edge) { - edge.setDataset('current', { - showDesc: true, - lineWidth: 4 - }); - } - - Metamaps.Visualize.mGraph.plot(); - - //remove the edge - Metamaps.Selected.Edges.splice( - Metamaps.Selected.Edges.indexOf(edge), 1); - }, - deleteSelectedEdges: function () { // refers to deleting topics permanently - var edge, - l = Metamaps.Selected.Edges.length; - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i]; - Metamaps.Control.deleteEdge(edge); - } - }, - deleteEdge: function (edge) { - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - var index = edge.getData("displayIndex") ? edge.getData("displayIndex") : 0; - - var synapse = edge.getData("synapses")[index]; - var mapping = edge.getData("mappings")[index]; - - var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin'); - if (permToDelete) { - if (edge.getData("synapses").length - 1 === 0) { - Metamaps.Control.hideEdge(edge); - } - var mappableid = synapse.id; - synapse.destroy(); - - // the server will destroy the mapping, we just need to remove it here - Metamaps.Mappings.remove(mapping); - edge.getData("mappings").splice(index, 1); - edge.getData("synapses").splice(index, 1); - if (edge.getData("displayIndex")) { - delete edge.data.$displayIndex; - } - $(document).trigger(Metamaps.JIT.events.deleteSynapse, [{ - mappableid: mappableid - }]); - } else { - Metamaps.GlobalUI.notifyUser('Only synapses you created can be deleted'); - } - }, - removeSelectedEdges: function () { - var l = Metamaps.Selected.Edges.length, - i, - edge; - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i]; - Metamaps.Control.removeEdge(edge); - } - Metamaps.Selected.Edges = new Array(); - }, - removeEdge: function (edge) { - - if (!Metamaps.Active.Map) return; - - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); - return; - } - - if (edge.getData("mappings").length - 1 === 0) { - Metamaps.Control.hideEdge(edge); - } - - var index = edge.getData("displayIndex") ? edge.getData("displayIndex") : 0; - - var synapse = edge.getData("synapses")[index]; - var mapping = edge.getData("mappings")[index]; - var mappableid = synapse.id; - mapping.destroy(); - - Metamaps.Synapses.remove(synapse); - - edge.getData("mappings").splice(index, 1); - edge.getData("synapses").splice(index, 1); - if (edge.getData("displayIndex")) { - delete edge.data.$displayIndex; - } - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ - mappableid: mappableid - }]); - }, - hideSelectedEdges: function () { - var edge, - l = Metamaps.Selected.Edges.length, - i; - for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i]; - Metamaps.Control.hideEdge(edge); - } - Metamaps.Selected.Edges = new Array(); - }, - hideEdge: function (edge) { - var from = edge.nodeFrom.id; - var to = edge.nodeTo.id; - edge.setData('alpha', 0, 'end'); - Metamaps.Control.deselectEdge(edge); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['edge-property:alpha'], - duration: 500 - }); - setTimeout(function () { - Metamaps.Visualize.mGraph.graph.removeAdjacence(from, to); - }, 500); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMappers(); - }, - updateSelectedPermissions: function (permission) { - - var edge, synapse, node, topic; - - Metamaps.GlobalUI.notifyUser('Working...'); - - // variables to keep track of how many nodes and synapses you had the ability to change the permission of - var nCount = 0, - sCount = 0; - - // change the permission of the selected synapses, if logged in user is the original creator - var l = Metamaps.Selected.Edges.length; - for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i]; - synapse = edge.getData('synapses')[0]; - - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { - synapse.save({ - permission: permission - }); - sCount++; - } - } - - // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length; - for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i]; - topic = node.getData('topic'); - - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { - topic.save({ - permission: permission - }); - nCount++; - } - } - - var nString = nCount == 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and '); - var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses'); - - var message = nString + sString + ' you created updated to ' + permission; - Metamaps.GlobalUI.notifyUser(message); - }, - updateSelectedMetacodes: function (metacode_id) { - - var node, topic; - - Metamaps.GlobalUI.notifyUser('Working...'); - - var metacode = Metamaps.Metacodes.get(metacode_id); - - // variables to keep track of how many nodes and synapses you had the ability to change the permission of - var nCount = 0; - - // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length; - for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i]; - topic = node.getData('topic'); - - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { - topic.save({ - 'metacode_id': metacode_id - }); - nCount++; - } - } - - var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics'); - - var message = nString + ' you can edit updated to ' + metacode.get('name'); - Metamaps.GlobalUI.notifyUser(message); - Metamaps.Visualize.mGraph.plot(); - }, -}; // end Metamaps.Control - - -/* - * - * FILTER - * - */ -Metamaps.Filter = { - filters: { - name: "", - metacodes: [], - mappers: [], - synapses: [] - }, - visible: { - metacodes: [], - mappers: [], - synapses: [] - }, - isOpen: false, - changing: false, - init: function () { - var self = Metamaps.Filter; - - $('.sidebarFilterIcon').click(self.toggleBox); - - $('.sidebarFilterBox .showAllMetacodes').click(self.filterNoMetacodes); - $('.sidebarFilterBox .showAllSynapses').click(self.filterNoSynapses); - $('.sidebarFilterBox .showAllMappers').click(self.filterNoMappers); - $('.sidebarFilterBox .hideAllMetacodes').click(self.filterAllMetacodes); - $('.sidebarFilterBox .hideAllSynapses').click(self.filterAllSynapses); - $('.sidebarFilterBox .hideAllMappers').click(self.filterAllMappers); - - self.bindLiClicks(); - self.getFilterData(); - }, - toggleBox: function (event) { - var self = Metamaps.Filter; - - if (self.isOpen) self.close(); - else self.open(); - - event.stopPropagation(); - }, - open: function () { - var self = Metamaps.Filter; - - Metamaps.GlobalUI.Account.close(); - $('.sidebarFilterIcon div').addClass('hide'); - - - if (!self.isOpen && !self.changing) { - self.changing = true; - - var height = $(document).height() - 108; - $('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - }); - } - }, - close: function () { - var self = Metamaps.Filter; - $('.sidebarFilterIcon div').removeClass('hide'); - - - if (!self.changing) { - self.changing = true; - - $('.sidebarFilterBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); - } - }, - reset: function () { - var self = Metamaps.Filter; - - self.filters.metacodes = []; - self.filters.mappers = []; - self.filters.synapses = []; - self.visible.metacodes = []; - self.visible.mappers = []; - self.visible.synapses = []; - - $('#filter_by_metacode ul').empty(); - $('#filter_by_mapper ul').empty(); - $('#filter_by_synapse ul').empty(); - - $('.filterBox .showAll').addClass('active'); - }, - /* - Most of this data essentially depends on the ruby function which are happening for filter inside view filterBox - But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses - */ - getFilterData: function () { - var self = Metamaps.Filter; - - var metacode, mapper, synapse; - - $('#filter_by_metacode li').each(function() { - metacode = $( this ).attr('data-id'); - self.filters.metacodes.push(metacode); - self.visible.metacodes.push(metacode); - }); - - $('#filter_by_mapper li').each(function() { - mapper = ($( this ).attr('data-id')); - self.filters.mappers.push(mapper); - self.visible.mappers.push(mapper); - }); - - $('#filter_by_synapse li').each(function() { - synapse = ($( this ).attr('data-id')); - self.filters.synapses.push(synapse); - self.visible.synapses.push(synapse); - }); - }, - bindLiClicks: function () { - var self = Metamaps.Filter; - $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode); - $('#filter_by_mapper ul li').unbind().click(self.toggleMapper); - $('#filter_by_synapse ul li').unbind().click(self.toggleSynapse); - }, - // an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce - // code redundancy - /* - @param - */ - updateFilters: function (collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { - var self = Metamaps.Filter; - - var newList = []; - var removed = []; - var added = []; - - // the first option enables us to accept - // ['Topics', 'Synapses'] as 'collection' - if (typeof collection === "object") { - Metamaps[collection[0]].each(function(model) { - var prop = model.get(propertyToCheck); - if (prop !== null) { - prop = prop.toString(); - if (newList.indexOf(prop) === -1) { - newList.push(prop); - } - } - }); - Metamaps[collection[1]].each(function(model) { - var prop = model.get(propertyToCheck); - if (prop !== null) { - prop = prop.toString(); - if (newList.indexOf(prop) === -1) { - newList.push(prop); - } - } - }); - } else if (typeof collection === "string") { - Metamaps[collection].each(function(model) { - var prop = model.get(propertyToCheck); - if (prop !== null) { - prop = prop.toString(); - if (newList.indexOf(prop) === -1) { - newList.push(prop); - } - } - }); - } - - removed = _.difference(self.filters[filtersToUse], newList); - added = _.difference(newList, self.filters[filtersToUse]); - - // remove the list items for things no longer present on the map - _.each(removed, function(identifier) { - $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast',function(){ - $(this).remove(); - }); - index = self.visible[filtersToUse].indexOf(identifier); - self.visible[filtersToUse].splice(index, 1); - }); - - var model, li, jQueryLi; - function sortAlpha(a,b){ - return a.childNodes[1].innerHTML.toLowerCase() > b.childNodes[1].innerHTML.toLowerCase() ? 1 : -1; - } - // for each new filter to be added, create a list item for it and fade it in - _.each(added, function (identifier) { - model = Metamaps[correlatedModel].get(identifier) || - Metamaps[correlatedModel].find(function (model) { - return model.get(propertyToCheck) === identifier; - }); - li = model.prepareLiForFilter(); - jQueryLi = $(li).hide(); - $('li', '#filter_by_' + listToModify + ' ul').add(jQueryLi.fadeIn("fast")) - .sort(sortAlpha).appendTo('#filter_by_' + listToModify + ' ul'); - self.visible[filtersToUse].push(identifier); - }); - - // update the list of filters with the new list we just generated - self.filters[filtersToUse] = newList; - - // make sure clicks on list items still trigger the right events - self.bindLiClicks(); - }, - checkMetacodes: function () { - var self = Metamaps.Filter; - self.updateFilters('Topics', 'metacode_id', 'Metacodes', 'metacodes', 'metacode'); - }, - checkMappers: function () { - var self = Metamaps.Filter; - var onMap = Metamaps.Active.Map ? true : false; - if (onMap) { - self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper'); - } - else { - // on topic view - self.updateFilters(['Topics', 'Synapses'], 'user_id', 'Creators', 'mappers', 'mapper'); - } - }, - checkSynapses: function () { - var self = Metamaps.Filter; - self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse'); - }, - filterAllMetacodes: function (e) { - var self = Metamaps.Filter; - $('#filter_by_metacode ul li').addClass('toggledOff'); - $('.showAllMetacodes').removeClass('active'); - $('.hideAllMetacodes').addClass('active'); - self.visible.metacodes = []; - self.passFilters(); - }, - filterNoMetacodes: function (e) { - var self = Metamaps.Filter; - $('#filter_by_metacode ul li').removeClass('toggledOff'); - $('.showAllMetacodes').addClass('active'); - $('.hideAllMetacodes').removeClass('active'); - self.visible.metacodes = self.filters.metacodes.slice(); - self.passFilters(); - }, - filterAllMappers: function (e) { - var self = Metamaps.Filter; - $('#filter_by_mapper ul li').addClass('toggledOff'); - $('.showAllMappers').removeClass('active'); - $('.hideAllMappers').addClass('active'); - self.visible.mappers = []; - self.passFilters(); - }, - filterNoMappers: function (e) { - var self = Metamaps.Filter; - $('#filter_by_mapper ul li').removeClass('toggledOff'); - $('.showAllMappers').addClass('active'); - $('.hideAllMappers').removeClass('active'); - self.visible.mappers = self.filters.mappers.slice(); - self.passFilters(); - }, - filterAllSynapses: function (e) { - var self = Metamaps.Filter; - $('#filter_by_synapse ul li').addClass('toggledOff'); - $('.showAllSynapses').removeClass('active'); - $('.hideAllSynapses').addClass('active'); - self.visible.synapses = []; - self.passFilters(); - }, - filterNoSynapses: function (e) { - var self = Metamaps.Filter; - $('#filter_by_synapse ul li').removeClass('toggledOff'); - $('.showAllSynapses').addClass('active'); - $('.hideAllSynapses').removeClass('active'); - self.visible.synapses = self.filters.synapses.slice(); - self.passFilters(); - }, - // an abstraction function for toggleMetacode, toggleMapper, toggleSynapse - // to reduce code redundancy - // gets called in the context of a list item in a filter box - toggleLi: function (whichToFilter) { - var self = Metamaps.Filter, index; - var id = $(this).attr("data-id"); - if (self.visible[whichToFilter].indexOf(id) == -1) { - self.visible[whichToFilter].push(id); - $(this).removeClass('toggledOff'); - } - else { - index = self.visible[whichToFilter].indexOf(id); - self.visible[whichToFilter].splice(index, 1); - $(this).addClass('toggledOff'); - } - self.passFilters(); - }, - toggleMetacode: function () { - var self = Metamaps.Filter; - self.toggleLi.call(this, 'metacodes'); - - if (self.visible.metacodes.length === self.filters.metacodes.length) { - $('.showAllMetacodes').addClass('active'); - $('.hideAllMetacodes').removeClass('active'); - } - else if (self.visible.metacodes.length === 0) { - $('.showAllMetacodes').removeClass('active'); - $('.hideAllMetacodes').addClass('active'); - } - else { - $('.showAllMetacodes').removeClass('active'); - $('.hideAllMetacodes').removeClass('active'); - } - }, - toggleMapper: function () { - var self = Metamaps.Filter; - self.toggleLi.call(this, 'mappers'); - - if (self.visible.mappers.length === self.filters.mappers.length) { - $('.showAllMappers').addClass('active'); - $('.hideAllMappers').removeClass('active'); - } - else if (self.visible.mappers.length === 0) { - $('.showAllMappers').removeClass('active'); - $('.hideAllMappers').addClass('active'); - } - else { - $('.showAllMappers').removeClass('active'); - $('.hideAllMappers').removeClass('active'); - } - }, - toggleSynapse: function () { - var self = Metamaps.Filter; - self.toggleLi.call(this, 'synapses'); - - if (self.visible.synapses.length === self.filters.synapses.length) { - $('.showAllSynapses').addClass('active'); - $('.hideAllSynapses').removeClass('active'); - } - else if (self.visible.synapses.length === 0) { - $('.showAllSynapses').removeClass('active'); - $('.hideAllSynapses').addClass('active'); - } - else { - $('.showAllSynapses').removeClass('active'); - $('.hideAllSynapses').removeClass('active'); - } - }, - passFilters: function () { - var self = Metamaps.Filter; - var visible = self.visible; - - var passesMetacode, passesMapper, passesSynapse; - var onMap; - - if (Metamaps.Active.Map) { - onMap = true; - } - else if (Metamaps.Active.Topic) { - onMap = false; - } - - var opacityForFilter = onMap ? 0 : 0.4; - - Metamaps.Topics.each(function(topic) { - var n = topic.get('node'); - var metacode_id = topic.get("metacode_id").toString(); - - if (visible.metacodes.indexOf(metacode_id) == -1) passesMetacode = false; - else passesMetacode = true; - - if (onMap) { - // when on a map, - // we filter by mapper according to the person who added the - // topic or synapse to the map - var user_id = topic.getMapping().get("user_id").toString(); - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false; - else passesMapper = true; - } - else { - // when on a topic view, - // we filter by mapper according to the person who created the - // topic or synapse - var user_id = topic.get("user_id").toString(); - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false; - else passesMapper = true; - } - - if (passesMetacode && passesMapper) { - if (n) { - n.setData('alpha', 1, 'end'); - } - else console.log(topic); - } - else { - if (n) { - Metamaps.Control.deselectNode(n, true); - n.setData('alpha', opacityForFilter, 'end'); - n.eachAdjacency(function(e){ - Metamaps.Control.deselectEdge(e, true); - }); - } - else console.log(topic); - } - }); - - // flag all the edges back to 'untouched' - Metamaps.Synapses.each(function(synapse) { - var e = synapse.get('edge'); - e.setData('touched', false); - }); - Metamaps.Synapses.each(function(synapse) { - var e = synapse.get('edge'); - var desc; - var user_id = synapse.get("user_id").toString(); - - if (e && !e.getData('touched')) { - - var synapses = e.getData('synapses'); - - // if any of the synapses represent by the edge are still unfiltered - // leave the edge visible - passesSynapse = false; - for (var i = 0; i < synapses.length; i++) { - desc = synapses[i].get("desc"); - if (visible.synapses.indexOf(desc) > -1) passesSynapse = true; - } - - // if the synapse description being displayed is now being - // filtered, set the displayIndex to the first unfiltered synapse if there is one - var displayIndex = e.getData("displayIndex") ? e.getData("displayIndex") : 0; - var displayedSynapse = synapses[displayIndex]; - desc = displayedSynapse.get("desc"); - if (passesSynapse && visible.synapses.indexOf(desc) == -1) { - // iterate and find an unfiltered one - for (var i = 0; i < synapses.length; i++) { - desc = synapses[i].get("desc"); - if (visible.synapses.indexOf(desc) > -1) { - e.setData('displayIndex', i); - break; - } - } - } - - if (onMap) { - // when on a map, - // we filter by mapper according to the person who added the - // topic or synapse to the map - user_id = synapse.getMapping().get("user_id").toString(); - } - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false; - else passesMapper = true; - - var color = Metamaps.Settings.colors.synapses.normal; - if (passesSynapse && passesMapper) { - e.setData('alpha', 1, 'end'); - e.setData('color', color, 'end'); - } - else { - Metamaps.Control.deselectEdge(e, true); - e.setData('alpha', opacityForFilter, 'end'); - } - - e.setData('touched', true); - } - else if (!e) console.log(synapse); - }); - - // run the animation - Metamaps.Visualize.mGraph.fx.animate({ - modes: ['node-property:alpha', - 'edge-property:alpha'], - duration: 200 - }); - } -}; // end Metamaps.Filter - - -/* - * - * LISTENERS - * - */ -Metamaps.Listeners = { - - init: function () { - - $(document).on('keydown', function (e) { - if (!(Metamaps.Active.Map || Metamaps.Active.Topic)) return; - - switch (e.which) { - case 13: // if enter key is pressed - Metamaps.JIT.enterKeyHandler(); - e.preventDefault(); - break; - case 27: // if esc key is pressed - Metamaps.JIT.escKeyHandler(); - break; - case 65: //if a or A is pressed - if (e.ctrlKey){ - Metamaps.Control.deselectAllNodes(); - Metamaps.Control.deselectAllEdges(); - - e.preventDefault(); - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - Metamaps.Control.selectNode(n,e); - }); - - Metamaps.Visualize.mGraph.plot(); - } - - break; - case 69: //if e or E is pressed - if (e.ctrlKey){ - e.preventDefault(); - if (Metamaps.Active.Map) { - Metamaps.JIT.zoomExtents(null, Metamaps.Visualize.mGraph.canvas); - } - } - break; - case 77: //if m or M is pressed - if (e.ctrlKey){ - e.preventDefault(); - Metamaps.Control.removeSelectedNodes(); - Metamaps.Control.removeSelectedEdges(); - } - break; - case 68: //if d or D is pressed - if (e.ctrlKey){ - e.preventDefault(); - Metamaps.Control.deleteSelected(); - } - break; - case 72: //if h or H is pressed - if (e.ctrlKey){ - e.preventDefault(); - Metamaps.Control.hideSelectedNodes(); - Metamaps.Control.hideSelectedEdges(); - } - break; - default: - break; //alert(e.which); - } - }); - - $(window).resize(function () { - if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()); - if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition(); - if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos(); - }); - } -}; // end Metamaps.Listeners - - -/* - * - * ORGANIZE - * - */ -Metamaps.Organize = { - init: function () { - - }, - arrange: function (layout, centerNode) { - - - // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin - if (layout == 'grid') { - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization - var numColumns = Math.floor(Math.sqrt(numNodes)); // the number of columns to make an even grid - var GRIDSPACE = 400; - var row = 0; - var column = 0; - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - if (column == numColumns) { - column = 0; - row += 1; - } - var newPos = new $jit.Complex(); - newPos.x = column * GRIDSPACE; - newPos.y = row * GRIDSPACE; - n.setPos(newPos, 'end'); - column += 1; - }); - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout); - } else if (layout == 'grid_full') { - - // this will always be an integer, the # of nodes on your graph visualization - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes); - //var numColumns = Math.floor(Math.sqrt(numNodes)); // the number of columns to make an even grid - //var GRIDSPACE = 400; - var height = Metamaps.Visualize.mGraph.canvas.getSize(0).height; - var width = Metamaps.Visualize.mGraph.canvas.getSize(0).width; - var totalArea = height * width; - var cellArea = totalArea / numNodes; - var ratio = height / width; - var cellWidth = sqrt(cellArea / ratio); - var cellHeight = cellArea / cellWidth; - var row = floor(height / cellHeight); - var column = floor(width / cellWidth); - var totalCells = row * column; - - if (totalCells) - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - if (column == numColumns) { - column = 0; - row += 1; - } - var newPos = new $jit.Complex(); - newPos.x = column * GRIDSPACE; - newPos.y = row * GRIDSPACE; - n.setPos(newPos, 'end'); - column += 1; - }); - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout); - } else if (layout == 'radial') { - - var centerX = centerNode.getPos().x; - var centerY = centerNode.getPos().y; - centerNode.setPos(centerNode.getPos(), 'end'); - - console.log(centerNode.adjacencies); - var lineLength = 200; - var usedNodes = {}; - usedNodes[centerNode.id] = centerNode; - var radial = function (node, level, degree) { - if (level == 1) { - var numLinksTemp = _.size(node.adjacencies); - var angleTemp = 2 * Math.PI / numLinksTemp; - } else { - angleTemp = 2 * Math.PI / 20 - }; - node.eachAdjacency(function (a) { - var isSecondLevelNode = (centerNode.adjacencies[a.nodeTo.id] != undefined && level > 1); - if (usedNodes[a.nodeTo.id] == undefined && !isSecondLevelNode) { - var newPos = new $jit.Complex(); - newPos.x = level * lineLength * Math.sin(degree) + centerX; - newPos.y = level * lineLength * Math.cos(degree) + centerY; - a.nodeTo.setPos(newPos, 'end'); - usedNodes[a.nodeTo.id] = a.nodeTo; - - radial(a.nodeTo, level + 1, degree); - degree += angleTemp; - }; - }); - }; - radial(centerNode, 1, 0); - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout); - - } else if (layout == 'center_viewport') { - - var lowX = 0, - lowY = 0, - highX = 0, - highY = 0; - var oldOriginX = Metamaps.Visualize.mGraph.canvas.translateOffsetX; - var oldOriginY = Metamaps.Visualize.mGraph.canvas.translateOffsetY; - - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - if (n.id === 1) { - lowX = n.getPos().x; - lowY = n.getPos().y; - highX = n.getPos().x; - highY = n.getPos().y; - }; - if (n.getPos().x < lowX) lowX = n.getPos().x; - if (n.getPos().y < lowY) lowY = n.getPos().y; - if (n.getPos().x > highX) highX = n.getPos().x; - if (n.getPos().y > highY) highY = n.getPos().y; - }); - console.log(lowX, lowY, highX, highY); - var newOriginX = (lowX + highX) / 2; - var newOriginY = (lowY + highY) / 2; - - } else alert('please call function with a valid layout dammit!'); - } -}; // end Metamaps.Organize - - -/* - * - * TOPIC - * - */ -Metamaps.Topic = { - // this function is to retrieve a topic JSON object from the database - // @param id = the id of the topic to retrieve - get: function (id, callback) { - // if the desired topic is not yet in the local topic repository, fetch it - if (Metamaps.Topics.get(id) == undefined) { - //console.log("Ajax call!"); - if (!callback) { - var e = $.ajax({ - url: "/topics/" + id + ".json", - async: false - }); - Metamaps.Topics.add($.parseJSON(e.responseText)); - return Metamaps.Topics.get(id); - } else { - return $.ajax({ - url: "/topics/" + id + ".json", - success: function (data) { - Metamaps.Topics.add(data); - callback(Metamaps.Topics.get(id)); - } - }); - } - } else { - if (!callback) { - return Metamaps.Topics.get(id); - } else { - return callback(Metamaps.Topics.get(id)); - } - } - }, - launch: function (id) { - var bb = Metamaps.Backbone; - var start = function (data) { - Metamaps.Active.Topic = new bb.Topic(data.topic); - Metamaps.Creators = new bb.MapperCollection(data.creators); - Metamaps.Topics = new bb.TopicCollection([data.topic].concat(data.relatives)); - Metamaps.Synapses = new bb.SynapseCollection(data.synapses); - Metamaps.Backbone.attachCollectionEvents(); - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('CREATORS'); - - // build and render the visualization - Metamaps.Visualize.type = "RGraph"; - Metamaps.JIT.prepareVizData(); - - // update filters - Metamaps.Filter.reset(); - - // reset selected arrays - Metamaps.Selected.reset(); - - // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMappers(); - } - - $.ajax({ - url: "/topics/" + id + "/network.json", - success: start - }); - }, - end: function () { - if (Metamaps.Active.Topic) { - $('.rightclickmenu').remove(); - Metamaps.TopicCard.hideCard(); - Metamaps.SynapseCard.hideCard(); - Metamaps.Filter.close(); - } - }, - centerOn: function (nodeid) { - if (!Metamaps.Visualize.mGraph.busy) { - Metamaps.Visualize.mGraph.onClick(nodeid, { - hideLabels: false, - duration: 1000, - onComplete: function () { - - } - }); - } - }, - fetchRelatives: function(node, metacode_id) { - - var topics = Metamaps.Topics.map(function(t){ return t.id }); - var topics_string = topics.join(); - - var creators = Metamaps.Creators.map(function(t){ return t.id }); - var creators_string = creators.join(); - - var topic = node.getData('topic'); - - var successCallback = function(data) { - if (data.creators.length > 0) Metamaps.Creators.add(data.creators); - if (data.topics.length > 0) Metamaps.Topics.add(data.topics); - if (data.synapses.length > 0) Metamaps.Synapses.add(data.synapses); - - var topicColl = new Metamaps.Backbone.TopicCollection(data.topics); - topicColl.add(topic); - var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses); - - var graph = Metamaps.JIT.convertModelsToJIT(topicColl, synapseColl)[0]; - Metamaps.Visualize.mGraph.op.sum(graph, { - type: 'fade', - duration: 500, - hideLabels: false - }); - - var i, l, t, s; - - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - t = Metamaps.Topics.get(n.id); - t.set({ node: n }, { silent: true }); - t.updateNode(); - - n.eachAdjacency(function (edge) { - if(!edge.getData('init')) { - edge.setData('init', true); - - l = edge.getData('synapseIDs').length; - for (i = 0; i < l; i++) { - s = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]); - s.set({ edge: edge }, { silent: true }); - s.updateEdge(); - } - } - }); - }); - }; - - var paramsString = metacode_id ? "metacode=" + metacode_id + "&" : ""; - paramsString += "network=" + topics_string + "&creators=" + creators_string; - - $.ajax({ - type: "Get", - url: "/topics/" + topic.id + "/relatives.json?" + paramsString, - success: successCallback, - error: function () { - - } - }); - }, - /* - * - * - */ - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter) { - var self = Metamaps.Topic; - - var nodeOnViz, tempPos; - - var newnode = topic.createNode(); - - var midpoint = {}, pixelPos; - - if (!$.isEmptyObject(Metamaps.Visualize.mGraph.graph.nodes)) { - Metamaps.Visualize.mGraph.graph.addNode(newnode); - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id); - topic.set('node', nodeOnViz, {silent: true}); - topic.updateNode(); // links the topic and the mapping to the node - - nodeOnViz.setData("dim", 1, "start"); - nodeOnViz.setData("dim", 25, "end"); - if (Metamaps.Visualize.type === "RGraph") { - tempPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')); - tempPos = tempPos.toPolar(); - nodeOnViz.setPos(tempPos, "current"); - nodeOnViz.setPos(tempPos, "start"); - nodeOnViz.setPos(tempPos, "end"); - } else if (Metamaps.Visualize.type === "ForceDirected") { - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "current"); - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "start"); - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "end"); - } - if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id; - - // position the form - midpoint.x = Metamaps.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2; - midpoint.y = Metamaps.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2; - pixelPos = Metamaps.Util.coordsToPixels(midpoint); - $('#new_synapse').css('left', pixelPos.x + "px"); - $('#new_synapse').css('top', pixelPos.y + "px"); - // show the form - Metamaps.Create.newSynapse.open(); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ["node-property:dim"], - duration: 500, - onComplete: function () { - Metamaps.tempNode = null; - Metamaps.tempNode2 = null; - Metamaps.tempInit = false; - } - }); - } else { - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ["node-property:dim"], - duration: 500, - onComplete: function () { - - } - }); - } - } else { - Metamaps.Visualize.mGraph.loadJSON(newnode); - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id); - topic.set('node', nodeOnViz, {silent: true}); - topic.updateNode(); // links the topic and the mapping to the node - - nodeOnViz.setData("dim", 1, "start"); - nodeOnViz.setData("dim", 25, "end"); - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "current"); - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "start"); - nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "end"); - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas); - Metamaps.Visualize.mGraph.fx.animate({ - modes: ["node-property:dim"], - duration: 500, - onComplete: function () { - - } - }); - } - - var mappingSuccessCallback = function (mappingModel, response) { - var newTopicData = { - mappingid: mappingModel.id, - mappableid: mappingModel.get('mappable_id') - }; - - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]); - }; - var topicSuccessCallback = function (topicModel, response) { - if (Metamaps.Active.Map) { - mapping.save({ mappable_id: topicModel.id }, { - success: mappingSuccessCallback, - error: function (model, response) { - console.log('error saving mapping to database'); - } - }); - } - - if (Metamaps.Create.newTopic.addSynapse) { - Metamaps.Create.newSynapse.topic2id = topicModel.id; - } - }; - - if (!Metamaps.Settings.sandbox && createNewInDB) { - if (topic.isNew()) { - topic.save(null, { - success: topicSuccessCallback, - error: function (model, response) { - console.log('error saving topic to database'); - } - }); - } else if (!topic.isNew() && Metamaps.Active.Map) { - mapping.save(null, { - success: mappingSuccessCallback - }); - } - } - }, - createTopicLocally: function () { - var self = Metamaps.Topic; - - if (Metamaps.Create.newTopic.name === "") { - Metamaps.GlobalUI.notifyUser("Please enter a topic title..."); - return; - } - - // hide the 'double-click to add a topic' message - Metamaps.Famous.viz.hideInstructions(); - - $(document).trigger(Metamaps.Map.events.editedByActiveMapper); - - var metacode = Metamaps.Metacodes.get(Metamaps.Create.newTopic.metacode); - - var topic = new Metamaps.Backbone.Topic({ - name: Metamaps.Create.newTopic.name, - metacode_id: metacode.id - }); - Metamaps.Topics.add(topic); - - var mapping = new Metamaps.Backbone.Mapping({ - xloc: Metamaps.Create.newTopic.x, - yloc: Metamaps.Create.newTopic.y, - mappable_id: topic.cid, - mappable_type: "Topic", - }); - Metamaps.Mappings.add(mapping); - - //these can't happen until the value is retrieved, which happens in the line above - Metamaps.Create.newTopic.hide(); - - self.renderTopic(mapping, topic, true, true); // this function also includes the creation of the topic in the database - }, - getTopicFromAutocomplete: function (id) { - var self = Metamaps.Topic; - - $(document).trigger(Metamaps.Map.events.editedByActiveMapper); - - Metamaps.Create.newTopic.hide(); - - var topic = self.get(id); - - var mapping = new Metamaps.Backbone.Mapping({ - xloc: Metamaps.Create.newTopic.x, - yloc: Metamaps.Create.newTopic.y, - mappable_type: "Topic", - mappable_id: topic.id, - }); - Metamaps.Mappings.add(mapping); - - self.renderTopic(mapping, topic, true, true); - }, - getTopicFromSearch: function (event, id) { - var self = Metamaps.Topic; - - $(document).trigger(Metamaps.Map.events.editedByActiveMapper); - - var topic = self.get(id); - - var nextCoords = Metamaps.Map.getNextCoord(); - var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords.x, - yloc: nextCoords.y, - mappable_type: "Topic", - mappable_id: topic.id, - }); - Metamaps.Mappings.add(mapping); - - self.renderTopic(mapping, topic, true, true); - - Metamaps.GlobalUI.notifyUser('Topic was added to your map!'); - - event.stopPropagation(); - event.preventDefault(); - return false; - } -}; // end Metamaps.Topic - - -/* - * - * SYNAPSE - * - */ -Metamaps.Synapse = { - // this function is to retrieve a synapse JSON object from the database - // @param id = the id of the synapse to retrieve - get: function (id, callback) { - // if the desired topic is not yet in the local topic repository, fetch it - if (Metamaps.Synapses.get(id) == undefined) { - if (!callback) { - var e = $.ajax({ - url: "/synapses/" + id + ".json", - async: false - }); - Metamaps.Synapses.add($.parseJSON(e.responseText)); - return Metamaps.Synapses.get(id); - } else { - return $.ajax({ - url: "/synapses/" + id + ".json", - success: function (data) { - Metamaps.Synapses.add(data); - callback(Metamaps.Synapses.get(id)); - } - }); - } - } else { - if (!callback) { - return Metamaps.Synapses.get(id); - } else { - return callback(Metamaps.Synapses.get(id)); - } - } - }, - /* - * - * - */ - renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { - var self = Metamaps.Synapse; - - var edgeOnViz; - - var newedge = synapse.createEdge(mapping); - - Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data); - edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id); - synapse.set('edge', edgeOnViz); - synapse.updateEdge(); // links the synapse and the mapping to the edge - - Metamaps.Control.selectEdge(edgeOnViz); - - var mappingSuccessCallback = function (mappingModel, response) { - var newSynapseData = { - mappingid: mappingModel.id, - mappableid: mappingModel.get('mappable_id') - }; - - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]); - }; - var synapseSuccessCallback = function (synapseModel, response) { - if (Metamaps.Active.Map) { - mapping.save({ mappable_id: synapseModel.id }, { - success: mappingSuccessCallback - }); - } - }; - - if (!Metamaps.Settings.sandbox && createNewInDB) { - if (synapse.isNew()) { - synapse.save(null, { - success: synapseSuccessCallback, - error: function (model, response) { - console.log('error saving synapse to database'); - } - }); - } else if (!synapse.isNew() && Metamaps.Active.Map) { - mapping.save(null, { - success: mappingSuccessCallback - }); - } - } - }, - createSynapseLocally: function () { - var self = Metamaps.Synapse, - topic1, - topic2, - node1, - node2, - synapse, - mapping; - - $(document).trigger(Metamaps.Map.events.editedByActiveMapper); - - //for each node in this array we will create a synapse going to the position2 node. - var synapsesToCreate = []; - - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id); - node2 = topic2.get('node'); - - var len = Metamaps.Selected.Nodes.length; - if (len == 0) { - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id); - synapsesToCreate[0] = topic1.get('node'); - } else if (len > 0) { - synapsesToCreate = Metamaps.Selected.Nodes; - } - - for (var i = 0; i < synapsesToCreate.length; i++) { - node1 = synapsesToCreate[i]; - topic1 = node1.getData('topic'); - synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description, - node1_id: topic1.isNew() ? topic1.cid : topic1.id, - node2_id: topic2.isNew() ? topic2.cid : topic2.id, - }); - Metamaps.Synapses.add(synapse); - - mapping = new Metamaps.Backbone.Mapping({ - mappable_type: "Synapse", - mappable_id: synapse.cid, - }); - Metamaps.Mappings.add(mapping); - - // this function also includes the creation of the synapse in the database - self.renderSynapse(mapping, synapse, node1, node2, true); - } // for each in synapsesToCreate - - Metamaps.Create.newSynapse.hide(); - }, - getSynapseFromAutocomplete: function (id) { - var self = Metamaps.Synapse, - topic1, - topic2, - node1, - node2; - - var synapse = self.get(id); - - var mapping = new Metamaps.Backbone.Mapping({ - mappable_type: "Synapse", - mappable_id: synapse.id, - }); - Metamaps.Mappings.add(mapping); - - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id); - node1 = topic1.get('node'); - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id); - node2 = topic2.get('node'); - Metamaps.Create.newSynapse.hide(); - - self.renderSynapse(mapping, synapse, node1, node2, true); - } -}; // end Metamaps.Synapse - - -/* - * - * MAP - * - */ -Metamaps.Map = { - events: { - editedByActiveMapper: "Metamaps:Map:events:editedByActiveMapper" - }, - nextX: 0, - nextY: 0, - sideLength: 1, - turnCount: 0, - nextXshift: 1, - nextYshift: 0, - timeToTurn: 0, - init: function () { - var self = Metamaps.Map; - - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#center-container').bind('contextmenu', function (e) { - return false; - }); - - $('.sidebarFork').click(function () { - self.fork(); - }); - - Metamaps.GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html(); - - self.InfoBox.init(); - self.CheatSheet.init(); - - $(document).on(Metamaps.Map.events.editedByActiveMapper, self.editedByActiveMapper); - }, - launch: function (id) { - var bb = Metamaps.Backbone; - var start = function (data) { - Metamaps.Active.Map = new bb.Map(data.map); - Metamaps.Mappers = new bb.MapperCollection(data.mappers); - Metamaps.Topics = new bb.TopicCollection(data.topics); - Metamaps.Synapses = new bb.SynapseCollection(data.synapses); - Metamaps.Mappings = new bb.MappingCollection(data.mappings); - Metamaps.Messages = data.messages; - Metamaps.Backbone.attachCollectionEvents(); - - var map = Metamaps.Active.Map; - var mapper = Metamaps.Active.Mapper; - - // add class to .wrapper for specifying whether you can edit the map - if (map.authorizeToEdit(mapper)) { - $('.wrapper').addClass('canEditMap'); - } - - // add class to .wrapper for specifying if the map can - // be collaborated on - if (map.get('permission') === 'commons') { - $('.wrapper').addClass('commonsMap'); - } - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('MAPPERS'); - - // build and render the visualization - Metamaps.Visualize.type = "ForceDirected"; - Metamaps.JIT.prepareVizData(); - - // update filters - Metamaps.Filter.reset(); - - // reset selected arrays - Metamaps.Selected.reset(); - - // set the proper mapinfobox content - Metamaps.Map.InfoBox.load(); - - // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMappers(); - - Metamaps.Realtime.startActiveMap(); - Metamaps.Loading.hide(); - } - - $.ajax({ - url: "/maps/" + id + "/contains.json", - success: start - }); - }, - end: function () { - if (Metamaps.Active.Map) { - - $('.wrapper').removeClass('canEditMap commonsMap'); - Metamaps.Map.resetSpiral(); - - $('.rightclickmenu').remove(); - Metamaps.TopicCard.hideCard(); - Metamaps.SynapseCard.hideCard(); - Metamaps.Create.newTopic.hide(); - Metamaps.Create.newSynapse.hide(); - Metamaps.Filter.close(); - Metamaps.Map.InfoBox.close(); - Metamaps.Realtime.endActiveMap(); - } - }, - fork: function () { - Metamaps.GlobalUI.openLightbox('forkmap'); - - var nodes_data = "", - synapses_data = ""; - var nodes_array = []; - var synapses_array = []; - // collect the unfiltered topics - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - // if the opacity is less than 1 then it's filtered - if (n.getData('alpha') === 1) { - var id = n.getData('topic').id; - nodes_array.push(id); - var x, y; - if (n.pos.x && n.pos.y) { - x = n.pos.x; - y = n.pos.y; - } else { - var x = Math.cos(n.pos.theta) * n.pos.rho; - var y = Math.sin(n.pos.theta) * n.pos.rho; - } - nodes_data += id + '/' + x + '/' + y + ','; - } - }); - // collect the unfiltered synapses - Metamaps.Synapses.each(function(synapse){ - var desc = synapse.get("desc"); - - var descNotFiltered = Metamaps.Filter.visible.synapses.indexOf(desc) > -1; - // make sure that both topics are being added, otherwise, it - // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1; - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1; - if (descNotFiltered && topicsNotFiltered) { - synapses_array.push(synapse.id); - } - }); - - synapses_data = synapses_array.join(); - nodes_data = nodes_data.slice(0, -1); - - Metamaps.GlobalUI.CreateMap.topicsToMap = nodes_data; - Metamaps.GlobalUI.CreateMap.synapsesToMap = synapses_data; - - }, - leavePrivateMap: function(){ - var map = Metamaps.Active.Map; - Metamaps.Maps.Active.remove(map); - Metamaps.Maps.Featured.remove(map); - Metamaps.Router.home(); - Metamaps.GlobalUI.notifyUser('Sorry! That map has been changed to Private.'); - }, - commonsToPublic: function(){ - Metamaps.Realtime.turnOff(true); // true is for 'silence' - Metamaps.GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.'); - Metamaps.Active.Map.trigger('changeByOther'); - }, - publicToCommons: function(){ - var confirmString = "This map permission has been changed to Commons! "; - confirmString += "Do you want to reload and enable realtime collaboration?"; - var c = confirm(confirmString); - if (c) { - Metamaps.Router.maps(Metamaps.Active.Map.id); - } - }, - editedByActiveMapper: function () { - if (Metamaps.Active.Mapper) { - Metamaps.Mappers.add(Metamaps.Active.Mapper); - } - }, - getNextCoord: function() { - var self = Metamaps.Map; - var nextX = self.nextX; - var nextY = self.nextY; - - var DISTANCE_BETWEEN = 120; - - self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift; - self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift; - - self.timeToTurn += 1; - // if true, it's time to turn - if (self.timeToTurn === self.sideLength) { - - self.turnCount += 1; - // if true, it's time to increase side length - if (self.turnCount % 2 === 0) { - self.sideLength += 1; - } - self.timeToTurn = 0; - - // going right? turn down - if (self.nextXshift == 1 && self.nextYshift == 0) { - self.nextXshift = 0; - self.nextYshift = 1; - } - // going down? turn left - else if (self.nextXshift == 0 && self.nextYshift == 1) { - self.nextXshift = -1; - self.nextYshift = 0; - } - // going left? turn up - else if (self.nextXshift == -1 && self.nextYshift == 0) { - self.nextXshift = 0; - self.nextYshift = -1; - } - // going up? turn right - else if (self.nextXshift == 0 && self.nextYshift == -1) { - self.nextXshift = 1; - self.nextYshift = 0; - } - } - - return { - x: nextX, - y: nextY - } - }, - resetSpiral: function() { - Metamaps.Map.nextX = 0; - Metamaps.Map.nextY = 0; - Metamaps.Map.nextXshift = 1; - Metamaps.Map.nextYshift = 0; - Metamaps.Map.sideLength = 1; - Metamaps.Map.timeToTurn = 0; - Metamaps.Map.turnCount = 0; - }, - exportImage: function() { - - var canvas = {}; - - canvas.canvas = document.createElement("canvas"); - canvas.canvas.width = 1880; // 960; - canvas.canvas.height = 1260; // 630 - - canvas.scaleOffsetX = 1; - canvas.scaleOffsetY = 1; - canvas.translateOffsetY = 0; - canvas.translateOffsetX = 0; - canvas.denySelected = true; - - canvas.getSize = function() { - if(this.size) return this.size; - var canvas = this.canvas; - return this.size = { - width: canvas.width, - height: canvas.height - }; - }; - canvas.scale = function(x, y) { - var px = this.scaleOffsetX * x, - py = this.scaleOffsetY * y; - var dx = this.translateOffsetX * (x -1) / px, - dy = this.translateOffsetY * (y -1) / py; - this.scaleOffsetX = px; - this.scaleOffsetY = py; - this.getCtx().scale(x, y); - this.translate(dx, dy); - }; - canvas.translate = function(x, y) { - var sx = this.scaleOffsetX, - sy = this.scaleOffsetY; - this.translateOffsetX += x*sx; - this.translateOffsetY += y*sy; - this.getCtx().translate(x, y); - }; - canvas.getCtx = function() { - return this.canvas.getContext("2d"); - }; - // center it - canvas.getCtx().translate(1880/2, 1260/2); - - var mGraph = Metamaps.Visualize.mGraph; - - var id = mGraph.root; - var root = mGraph.graph.getNode(id); - var T = !!root.visited; - - // pass true to avoid basing it on a selection - Metamaps.JIT.zoomExtents(null, canvas, true); - - var c = canvas.canvas, - ctx = canvas.getCtx(), - scale = canvas.scaleOffsetX; - - // draw a grey background - ctx.fillStyle = '#d8d9da'; - var xPoint = (-(c.width/scale)/2) - (canvas.translateOffsetX/scale), - yPoint = (-(c.height/scale)/2) - (canvas.translateOffsetY/scale); - ctx.fillRect(xPoint,yPoint,c.width/scale,c.height/scale); - - // draw the graph - mGraph.graph.eachNode(function(node) { - var nodeAlpha = node.getData('alpha'); - node.eachAdjacency(function(adj) { - var nodeTo = adj.nodeTo; - if(!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { - mGraph.fx.plotLine(adj, canvas); - } - }); - if(node.drawn) { - mGraph.fx.plotNode(node, canvas); - } - if(!mGraph.labelsHidden) { - if(node.drawn && nodeAlpha >= 0.95) { - mGraph.labels.plotLabel(canvas, node); - } else { - mGraph.labels.hideLabel(node, false); - } - } - node.visited = !T; - }); - - var imageData = { - encoded_image: canvas.canvas.toDataURL() - }; - - var map = Metamaps.Active.Map; - - var today = new Date(); - var dd = today.getDate(); - var mm = today.getMonth()+1; //January is 0! - var yyyy = today.getFullYear(); - if(dd<10) { - dd='0'+dd - } - if(mm<10) { - mm='0'+mm - } - today = mm+'/'+dd+'/'+yyyy; - - var mapName = map.get("name").split(" ").join([separator = '-']); - var downloadMessage = ""; - downloadMessage += "Captured map screenshot! "; - downloadMessage += "DOWNLOAD"; - Metamaps.GlobalUI.notifyUser(downloadMessage); - - $.ajax({ - type: "POST", - dataType: 'json', - url: "/maps/" + Metamaps.Active.Map.id + "/upload_screenshot", - data: imageData, - success: function (data) { - console.log('successfully uploaded map screenshot'); - }, - error: function () { - console.log('failed to save map screenshot'); - } - }); - } -}; - - -/* - * - * CHEATSHEET - * - */ -Metamaps.Map.CheatSheet = { - init: function () { - // tab the cheatsheet - $('#cheatSheet').tabs(); - $('#quickReference').tabs().addClass("ui-tabs-vertical ui-helper-clearfix"); - $("#quickReference .ui-tabs-nav li").removeClass("ui-corner-top").addClass("ui-corner-left"); - - // id = the id of a vimeo video - var switchVideo = function (element, id) { - $('.tutorialItem').removeClass("active"); - $(element).addClass("active"); - $('#tutorialVideo').attr('src','//player.vimeo.com/video/'+id); - }; - - $('#gettingStarted').click(function() { - //switchVideo(this,'88334167'); - }); - $('#upYourSkillz').click(function() { - //switchVideo(this,'100118167'); - }); - $('#advancedMapping').click(function() { - //switchVideo(this,'88334167'); - }); - } -}; // end Metamaps.Map.CheatSheet - - -/* - * - * INFOBOX - * - */ -Metamaps.Map.InfoBox = { - isOpen: false, - changing: false, - selectingPermission: false, - changePermissionText: "
    As the creator, you can change the permission of this map, but the permissions of the topics and synapses on it must be changed independently.
    ", - nameHTML: '{{name}}', - descHTML: '{{desc}}', - init: function () { - var self = Metamaps.Map.InfoBox; - - $('.mapInfoIcon').click(self.toggleBox); - $('.mapInfoBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); - - self.attachEventListeners(); - - self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()); - }, - toggleBox: function (event) { - var self = Metamaps.Map.InfoBox; - - if (self.isOpen) self.close(); - else self.open(); - - event.stopPropagation(); - }, - open: function () { - var self = Metamaps.Map.InfoBox; - $('.mapInfoIcon div').addClass('hide'); - if (!self.isOpen && !self.changing) { - self.changing = true; - $('.mapInfoBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - }); - } - }, - close: function () { - var self = Metamaps.Map.InfoBox; - - $('.mapInfoIcon div').removeClass('hide'); - if (!self.changing) { - self.changing = true; - $('.mapInfoBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - self.hidePermissionSelect(); - $('.mapContributors .tip').hide(); - }); - } - }, - load: function () { - var self = Metamaps.Map.InfoBox; - - var map = Metamaps.Active.Map; - - var obj = map.pick("permission","contributor_count","topic_count","synapse_count"); - - var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper); - var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper); - var shareable = map.get('permission') !== 'private'; - - obj["name"] = canEdit ? Hogan.compile(self.nameHTML).render({id: map.id, name: map.get("name")}) : map.get("name"); - obj["desc"] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get("desc")}) : map.get("desc"); - obj["map_creator_tip"] = isCreator ? self.changePermissionText : ""; - obj["contributors_class"] = Metamaps.Mappers.length > 1 ? "multiple" : ""; - obj["contributors_class"] += Metamaps.Mappers.length === 2 ? " mTwo" : ""; - obj["contributor_image"] = Metamaps.Mappers.length > 0 ? Metamaps.Mappers.models[0].get("image") : "<%= asset_path('user.png') %>"; - obj["contributor_list"] = self.createContributorList(); - obj["user_name"] = isCreator ? "You" : map.get("user_name"); - obj["created_at"] = map.get("created_at_clean"); - obj["updated_at"] = map.get("updated_at_clean"); - - var classes = isCreator ? "yourMap" : ""; - classes += canEdit ? " canEdit" : ""; - classes += shareable ? " shareable" : ""; - $(".mapInfoBox").removeClass("shareable yourMap canEdit") - .addClass(classes) - .html(self.generateBoxHTML.render(obj)); - - self.attachEventListeners(); - }, - attachEventListeners: function () { - var self = Metamaps.Map.InfoBox; - - $('.mapInfoBox.canEdit .best_in_place').best_in_place(); - - // because anyone who can edit the map can change the map title - var bipName = $('.mapInfoBox .best_in_place_name'); - bipName.unbind("best_in_place:activate").bind("best_in_place:activate", function () { - var $el = bipName.find('textarea'); - var el = $el[0]; - - $el.attr('maxlength', '140'); - - $('.mapInfoName').append('
    '); - - var callback = function (data) { - $('.nameCounter.forMap').html(data.all + '/140'); - }; - Countable.live(el, callback); - }); - bipName.unbind("best_in_place:deactivate").bind("best_in_place:deactivate", function () { - $('.nameCounter.forMap').remove(); - }); - - $('.mapInfoName .best_in_place_name').unbind("ajax:success").bind("ajax:success", function () { - var name = $(this).html(); - Metamaps.Active.Map.set('name', name); - Metamaps.Active.Map.trigger('saved'); - }); - - $('.mapInfoDesc .best_in_place_desc').unbind("ajax:success").bind("ajax:success", function () { - var desc = $(this).html(); - Metamaps.Active.Map.set('desc', desc); - Metamaps.Active.Map.trigger('saved'); - }); - - $('.yourMap .mapPermission').unbind().click(self.onPermissionClick); - // .yourMap in the unbind/bind is just a namespace for the events - // not a reference to the class .yourMap on the .mapInfoBox - $('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect); - - $('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap); - - $('.mapContributors span, #mapContribs').unbind().click(function(event){ - $('.mapContributors .tip').toggle(); - event.stopPropagation(); - }); - $('.mapContributors .tip').unbind().click(function(event){ - event.stopPropagation(); - }); - $('.mapContributors .tip li a').click(Metamaps.Router.intercept); - - $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function(){ - $('.mapContributors .tip').hide(); - }); - }, - updateNameDescPerm: function(name, desc, perm) { - $('.mapInfoName .best_in_place_name').html(name); - $('.mapInfoDesc .best_in_place_desc').html(desc); - $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm); - }, - createContributorList: function () { - var self = Metamaps.Map.InfoBox; - - var string = ""; - string += ""; - return string; - }, - updateNumbers: function () { - var self = Metamaps.Map.InfoBox; - var mapper = Metamaps.Active.Mapper; - - var contributors_class = ""; - if (Metamaps.Mappers.length === 2) contributors_class = "multiple mTwo"; - else if (Metamaps.Mappers.length > 2) contributors_class = "multiple"; - - var contributors_image = "<%= asset_path('user.png') %>"; - if (Metamaps.Mappers.length > 0) { - // get the first contributor and use their image - contributors_image = Metamaps.Mappers.models[0].get("image"); - } - $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class); - $('.mapContributors span').text(Metamaps.Mappers.length) - $('.mapContributors .tip').html(self.createContributorList()); - $('.mapTopics').text(Metamaps.Topics.length); - $('.mapSynapses').text(Metamaps.Synapses.length); - - $('.mapEditedAt').html('Last edited: ' + Metamaps.Util.nowDateFormatted()); - }, - onPermissionClick: function (event) { - var self = Metamaps.Map.InfoBox; - - if (!self.selectingPermission) { - self.selectingPermission = true; - $(this).addClass('minimize'); // this line flips the drop down arrow to a pull up arrow - if ($(this).hasClass('commons')) { - $(this).append('
    '); - } else if ($(this).hasClass('public')) { - $(this).append('
    '); - } else if ($(this).hasClass('private')) { - $(this).append('
    '); - } - $('.mapPermission .permissionSelect li').click(self.selectPermission); - event.stopPropagation(); - } - }, - hidePermissionSelect: function () { - var self = Metamaps.Map.InfoBox; - - self.selectingPermission = false; - $('.mapPermission').removeClass('minimize'); // this line flips the pull up arrow to a drop down arrow - $('.mapPermission .permissionSelect').remove(); - }, - selectPermission: function (event) { - var self = Metamaps.Map.InfoBox; - - self.selectingPermission = false; - var permission = $(this).attr('class'); - var permBefore = Metamaps.Active.Map.get('permission'); - Metamaps.Active.Map.save({ - permission: permission - }); - Metamaps.Active.Map.updateMapWrapper(); - if (permBefore !== 'commons' && permission === 'commons') { - Metamaps.Realtime.setupSocket(); - Metamaps.Realtime.turnOn(); - } - else if (permBefore === 'commons' && permission === 'public') { - Metamaps.Realtime.turnOff(true); // true is to 'silence' - // the notification that would otherwise be sent - } - shareable = permission === 'private' ? '' : 'shareable'; - $('.mapPermission').removeClass('commons public private minimize').addClass(permission); - $('.mapPermission .permissionSelect').remove(); - $('.mapInfoBox').removeClass('shareable').addClass(shareable); - event.stopPropagation(); - }, - deleteActiveMap: function () { - var confirmString = 'Are you sure you want to delete this map? '; - confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.'; - - var doIt = confirm(confirmString); - var map = Metamaps.Active.Map; - var mapper = Metamaps.Active.Mapper; - var authorized = map.authorizePermissionChange(mapper); - - if (doIt && authorized) { - Metamaps.Map.InfoBox.close(); - Metamaps.Maps.Active.remove(map); - Metamaps.Maps.Featured.remove(map); - Metamaps.Maps.Mine.remove(map); - map.destroy(); - Metamaps.Router.home(); - Metamaps.GlobalUI.notifyUser('Map eliminated!'); - } - else if (!authorized) { - alert('Hey now. We can\'t just go around willy nilly deleting other people\'s maps now can we? Run off and find something constructive to do, eh?'); - } - } -}; // end Metamaps.Map.InfoBox - -/* -* -* Account Settings -* -*/ -Metamaps.Account = { - listenersInitialized: false, - init: function () { - var self = Metamaps.Account; - - - }, - initListeners: function(){ - var self = Metamaps.Account; - - $('#user_image').change(self.showImagePreview); - self.listenersInitialized = true; - }, - toggleChangePicture: function(){ - var self = Metamaps.Account; - - $('.userImageMenu').toggle(); - if (!self.listenersInitialized) self.initListeners(); - }, - openChangePicture: function(){ - var self = Metamaps.Account; - - $('.userImageMenu').show(); - if (!self.listenersInitialized) self.initListeners(); - }, - closeChangePicture: function(){ - var self = Metamaps.Account; - - $('.userImageMenu').hide(); - }, - showLoading: function(){ - var self = Metamaps.Account; - - var loader = new CanvasLoader('accountPageLoading'); - loader.setColor('#4FC059'); // default is '#000000' - loader.setDiameter(28); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - $('#accountPageLoading').show(); - }, - showImagePreview: function(){ - var self = Metamaps.Account; - - var file = $('#user_image')[0].files[0]; - - var reader = new FileReader(); - - reader.onload = function(e) { - var $canvas = $('').attr({ - width: 84, - height: 84 - }); - var context = $canvas[0].getContext('2d'); - var imageObj = new Image(); - - imageObj.onload = function() { - $('.userImageDiv canvas').remove(); - $('.userImageDiv img').hide(); - - var imgWidth = imageObj.width; - var imgHeight = imageObj.height; - - var dimensionToMatch = imgWidth > imgHeight ? imgHeight : imgWidth; - // draw cropped image - var nonZero = Math.abs(imgHeight - imgWidth) / 2; - var sourceX = dimensionToMatch === imgWidth ? 0 : nonZero; - var sourceY = dimensionToMatch === imgHeight ? 0 : nonZero; - var sourceWidth = dimensionToMatch; - var sourceHeight = dimensionToMatch; - var destX = 0; - var destY = 0; - var destWidth = 84; - var destHeight = 84; - - context.drawImage(imageObj, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight); - $('.userImageDiv').prepend($canvas); - }; - imageObj.src = reader.result; - }; - - if (file) { - reader.readAsDataURL(file); - $('.userImageMenu').hide(); - $('#remove_image').val('0'); - } - }, - removePicture: function(){ - var self = Metamaps.Account; - - $('.userImageDiv canvas').remove(); - $('.userImageDiv img').attr('src', '<%= asset_path('user.png') %>').show(); - $('.userImageMenu').hide(); - - var input = $('#user_image'); - input.replaceWith(input.val('').clone(true)); - $('#remove_image').val('1'); - }, - changeName: function(){ - $('.accountName').hide(); - $('.changeName').show(); - }, - showPass: function(){ - $(".toHide").show(); - $(".changePass").hide(); - }, - hidePass: function(){ - $(".toHide").hide(); - $(".changePass").show(); - - $('#current_password').val(''); - $('#user_password').val(''); - $('#user_password_confirmation').val(''); - } -}; - -/* - * - * MAPPER - * - */ -Metamaps.Mapper = { - // this function is to retrieve a mapper JSON object from the database - // @param id = the id of the mapper to retrieve - get: function (id, callback) { - return $.ajax({ - url: "/users/" + id + ".json", - success: function (data) { - callback(new Metamaps.Backbone.Mapper(data)); - } - }); - } -}; // end Metamaps.Mapper + reset: function () { + var self = Metamaps.Selected + + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} diff --git a/app/assets/javascripts/src/check-canvas-support.js b/app/assets/javascripts/src/check-canvas-support.js new file mode 100644 index 00000000..90afdde1 --- /dev/null +++ b/app/assets/javascripts/src/check-canvas-support.js @@ -0,0 +1,15 @@ +// TODO document this user agent function +var labelType, useGradients, nativeTextSupport, animate +;(function () { + var ua = navigator.userAgent, + iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), + typeOfCanvas = typeof HTMLCanvasElement, + nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), + textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function') + // I'm setting this based on the fact that ExCanvas provides text support for IE + // and that as of today iPhone/iPad current text support is lame + labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML' + nativeTextSupport = labelType == 'Native' + useGradients = nativeCanvasSupport + animate = !(iStuff || !nativeCanvasSupport) +})() diff --git a/app/assets/javascripts/test/Metamaps.Import.spec.js b/app/assets/javascripts/test/Metamaps.Import.spec.js new file mode 100644 index 00000000..d8993e42 --- /dev/null +++ b/app/assets/javascripts/test/Metamaps.Import.spec.js @@ -0,0 +1,13 @@ +var chai = require('chai') +var expect = chai.expect + +Metamaps = {} +Metamaps.Import = require('../src/Metamaps.Import') + +describe('Metamaps.Import.js', function() { + it('has a topic whitelist', function() { + expect(Metamaps.Import.topicWhitelist).to.deep.equal( + ['id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission'] + ) + }) +}) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index 5970c18b..6c76dd9e 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -1569,6 +1569,11 @@ h3.filterBox { background-repeat: no-repeat; text-align: left; } + +.commonsMap .mapContributors { + visibility: hidden; +} + .mapContributors { position: relative; height: 30px; @@ -1576,6 +1581,7 @@ h3.filterBox { padding: 0; width: 64px; } + #mapContribs { float: left; border: 2px solid #424242; @@ -1591,7 +1597,7 @@ h3.filterBox { #mapContribs.multiple { box-shadow: 1px 1px 0 0 #B5B5B5,3px 2px 0 0 #424242,4px 3px 0 0 #B5B5B5,5px 4px 0 0 #424242; } -.mapContributors span { +.mapContributors span.count { height: 20px; padding-top: 5px; padding-left: 8px; @@ -1626,10 +1632,11 @@ h3.filterBox { .mapContributors .tip { top: 45px; left: -10px; + min-width: 200px; } .mapContributors .tip ul { - max-height: 188px; + max-height: 144px; overflow-y: auto; } @@ -1652,7 +1659,7 @@ h3.filterBox { .mapContributors .tip li a { color: white; } -.mapContributors div:after { +.mapContributors div.tip:after { content: ''; position: absolute; top: -4px; @@ -1672,6 +1679,90 @@ h3.filterBox { border-radius: 14px; } +.mapContributors span.twitter-typeahead { + padding: 0; +} + +.collabSearchField { + text-align: left; +} + +.collabNameWrapper { + float: left; +} + +.collabIconWrapper img.icon { + position: relative; + top: 0; + left: 0; +} + +.collabIconWrapper { + position: relative; + float: left; + padding: 0 4px; +} + +.mapContributors .collabName { + font-weight: normal; + font-size: 14px; + line-height: 28px; + color: #424242; + padding: 0 4px; +} + +span.removeCollaborator { + position: absolute; + top: 11px; + right: 8px; + height: 16px; + width: 16px; + background-image: url(<%= asset_data_uri('removecollab_sprite.png') %>); + cursor: pointer; +} + +span.removeCollaborator:hover { + background-position: -16px 0; +} + +span.addCollab { + width: 16px; + height: 16px; + background-image: url(<%= asset_data_uri('addcollab_sprite.png') %>); + display: inline-block; + vertical-align: middle; + margin: 0 12px 0 10px; +} + +input.collaboratorSearchField { + background: #FFFFFF; + height: 14px; + margin: 0; + padding: 10px 6px; + border: none; + border-radius: 2px; + outline: none; + font-size: 14px; + line-height: 14px; + color: #424242; + font-family: 'din-medium', helvetica, sans-serif; +} + +.tt-dataset.tt-dataset-collaborators { + padding: 2px; + background: #E0E0E0; + min-width: 156px; + border-radius: 2px; +} + +.tt-dataset.tt-dataset .collabResult { + padding: 4px; +} + +.collabResult.tt-suggestion.tt-cursor, .collabResult.tt-suggestion:hover { + background-color: #CCCCCC; +} + .mapInfoBox .mapPermission .tooltips { top: -20px; right: 36px; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 5665716a..47e18a94 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -681,6 +681,10 @@ background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: 0 0; } +.exploreMapsCenter .sharedMaps .exploreMapsIcon { + background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-position: -96px 0; +} .exploreMapsCenter .activeMaps .exploreMapsIcon { background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: -32px 0; @@ -698,6 +702,9 @@ .featuredMaps:hover .exploreMapsIcon, .featuredMaps.active .exploreMapsIcon { background-position: -64px -32px; } +.sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { + background-position: -96px -32px; +} .mapsWrapper { /*overflow-y: auto; */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c1e9e26d..5db7d62a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,7 +32,11 @@ class ApplicationController < ActionController::Base end def handle_unauthorized - head :forbidden # TODO make this better + if authenticated? + head :forbidden # TODO make this better + else + redirect_to new_user_session_path, notice: "Try signing in to do that." + end end private diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index d8ce06b6..87264c4c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,7 +4,7 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper - after_action :verify_policy_scoped, except: :requestinvite + after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers] respond_to :html, :json @@ -133,8 +133,9 @@ class MainController < ApplicationController #remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == "mapper:" search = term.downcase + '%' - builder = policy_scope(User) # TODO do I need to policy scope? I guess yes to verify_policy_scoped - builder = builder.where('LOWER("name") like ?', search) + + skip_policy_scope # TODO builder = policy_scope(User) + builder = User.where('LOWER("name") like ?', search) @mappers = builder.order(:name) else @mappers = [] diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 936ffcc2..9a5f1f1f 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -45,6 +45,13 @@ class MappingsController < ApplicationController @mapping = Mapping.find(params[:id]) authorize @mapping + mappable = @mapping.mappable + if mappable.defer_to_map + mappable.permission = mappable.defer_to_map.permission + mappable.defer_to_map_id = nil + mappable.save + end + @mapping.destroy head :no_content diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 91eb97e1..e52b1833 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,8 +1,7 @@ class MapsController < ApplicationController - - before_action :require_user, only: [:create, :update, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps, :events] - after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] + before_action :require_user, only: [:create, :update, :access, :screenshot, :events, :destroy] + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :usermaps, :events] + after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :usermaps] respond_to :html, :json, :csv @@ -53,6 +52,21 @@ class MapsController < ApplicationController end end + # GET /explore/shared + def sharedmaps + return redirect_to activemaps_url if !authenticated? + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where("maps.id IN (?)", current_user.shared_maps.map(&:id)) + ).order("updated_at DESC").page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + # GET /explore/mapper/:id def usermaps page = params[:page].present? ? params[:page] : 1 @@ -74,15 +88,13 @@ class MapsController < ApplicationController respond_to do |format| format.html { @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| - object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) - } + @allcollaborators = @map.editors + @alltopics = @map.topics.to_a.delete_if {|t| not policy(t).show? } + @allsynapses = @map.synapses.to_a.delete_if {|s| not policy(s).show? } + @allmappings = @map.mappings.to_a.delete_if {|m| not policy(m).show? } @allmessages = @map.messages.sort_by(&:created_at) - respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @map) } format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -111,6 +123,9 @@ class MapsController < ApplicationController if params[:event] == 'conversation' Events::ConversationStartedOnMap.publish!(map, current_user) valid_event = true + elsif params[:event] == 'user_presence' + Events::UserPresentOnMap.publish!(map, current_user) + valid_event = true end respond_to do |format| @@ -127,12 +142,11 @@ class MapsController < ApplicationController authorize @map @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| - object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) - } + @allcollaborators = @map.editors + @alltopics = @map.topics.to_a.delete_if {|t| not policy(t).show? } + @allsynapses = @map.synapses.to_a.delete_if {|s| not policy(s).show? } + @allmappings = @map.mappings.to_a.delete_if {|m| not policy(m).show? } + @json = Hash.new() @json['map'] = @map @@ -140,6 +154,7 @@ class MapsController < ApplicationController @json['synapses'] = @allsynapses @json['mappings'] = @allmappings @json['mappers'] = @allmappers + @json['collaborators'] = @allcollaborators @json['messages'] = @map.messages.sort_by(&:created_at) respond_to do |format| @@ -215,6 +230,36 @@ class MapsController < ApplicationController end end + # POST maps/:id/access + def access + @map = Map.find(params[:id]) + authorize @map + userIds = params[:access] || [] + added = userIds.select { |uid| + user = User.find(uid) + if user.nil? || (current_user && user == current_user) + false + else + not @map.collaborators.include?(user) + end + } + removed = @map.collaborators.select { |user| not userIds.include?(user.id.to_s) }.map(&:id) + added.each { |uid| + um = UserMap.create({ user_id: uid.to_i, map_id: @map.id }) + user = User.find(uid.to_i) + MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later + } + removed.each { |uid| + @map.user_maps.select{ |um| um.user_id == uid }.each{ |um| um.destroy } + } + + respond_to do |format| + format.json do + render :json => { :message => "Successfully altered edit permissions" } + end + end + end + # POST maps/:id/upload_screenshot def screenshot @map = Map.find(params[:id]) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 4440872f..b8ccfc5f 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -51,7 +51,7 @@ class SynapsesController < ApplicationController def destroy @synapse = Synapse.find(params[:id]) authorize @synapse - @synapse.delete + @synapse.destroy respond_to do |format| format.json { head :no_content } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 9911a7fe..88e9cef4 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -150,7 +150,7 @@ puts @allsynapses.length @topic = Topic.find(params[:id]) authorize @topic - @topic.delete + @topic.destroy respond_to do |format| format.json { head :no_content } end @@ -159,6 +159,6 @@ puts @allsynapses.length private def topic_params - params.require(:topic).permit(:id, :name, :desc, :link, :permission, :user_id, :metacode_id) + params.require(:topic).permit(:id, :name, :desc, :link, :permission, :user_id, :metacode_id, :defer_to_map_id) end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..0d9431a3 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "team@metamaps.cc" + layout 'mailer' +end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb new file mode 100644 index 00000000..dd164cf3 --- /dev/null +++ b/app/mailers/map_mailer.rb @@ -0,0 +1,10 @@ +class MapMailer < ApplicationMailer + default from: "team@metamaps.cc" + + def invite_to_edit_email(map, inviter, invitee) + @inviter = inviter + @map = map + subject = @map.name + ' - Invitation to edit' + mail(to: invitee.email, subject: subject) + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 0ad350ea..66431b61 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,5 +1,5 @@ class Event < ActiveRecord::Base - KINDS = %w[conversation_started_on_map topic_added_to_map synapse_added_to_map] + KINDS = %w[user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map] #has_many :notifications, dependent: :destroy belongs_to :eventable, polymorphic: true diff --git a/app/models/events/user_present_on_map.rb b/app/models/events/user_present_on_map.rb new file mode 100644 index 00000000..e40df243 --- /dev/null +++ b/app/models/events/user_present_on_map.rb @@ -0,0 +1,18 @@ +class Events::UserPresentOnMap < Event + #after_create :notify_users! + + def self.publish!(map, user) + create!(kind: "user_present_on_map", + eventable: map, + map: map, + user: user) + end + + private + + #def notify_users! + # unless comment_vote.user == comment_vote.comment_user + # notify!(comment_vote.comment_user) + # end + #end +end diff --git a/app/models/map.rb b/app/models/map.rb index d9eb6a18..8ffe14d6 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -8,6 +8,9 @@ class Map < ActiveRecord::Base has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" has_many :messages, as: :resource, dependent: :destroy + has_many :user_maps, dependent: :destroy + has_many :collaborators, through: :user_maps, source: :user + has_many :webhooks, as: :hookable has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy @@ -45,6 +48,10 @@ class Map < ActiveRecord::Base return contributors end + def editors + collaborators + [self.user] + end + def topic_count topics.length end @@ -65,6 +72,10 @@ class Map < ActiveRecord::Base contributors.length end + def collaborator_ids + collaborators.map(&:id) + end + def screenshot_url screenshot.url(:thumb) end @@ -78,7 +89,7 @@ class Map < ActiveRecord::Base end def as_json(options={}) - json = super(:methods =>[:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :screenshot_url], :except => [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) + json = super(:methods =>[:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], :except => [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str json[:updated_at_clean] = updated_at_str json diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 540376bb..7e361af3 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,5 +1,6 @@ class Synapse < ActiveRecord::Base belongs_to :user + belongs_to :defer_to_map, :class_name => 'Map', :foreign_key => 'defer_to_map_id' belongs_to :topic1, :class_name => "Topic", :foreign_key => "node1_id" belongs_to :topic2, :class_name => "Topic", :foreign_key => "node2_id" @@ -32,9 +33,29 @@ class Synapse < ActiveRecord::Base end # :nocov: + # :nocov: + def collaborator_ids + if defer_to_map + defer_to_map.editors.select{|mapper| not mapper == self.user }.map(&:id) + else + [] + end + end + # :nocov: + + # :nocov: + def calculated_permission + if defer_to_map + defer_to_map.permission + else + permission + end + end + # :nocov: + # :nocov: def as_json(options={}) - super(:methods =>[:user_name, :user_image]) + super(:methods =>[:user_name, :user_image, :calculated_permission, :collaborator_ids]) end # :nocov: diff --git a/app/models/topic.rb b/app/models/topic.rb index f1a73c1b..61f405de 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -2,6 +2,7 @@ class Topic < ActiveRecord::Base include TopicsHelper belongs_to :user + belongs_to :defer_to_map, :class_name => 'Map', :foreign_key => 'defer_to_map_id' has_many :synapses1, :class_name => 'Synapse', :foreign_key => 'node1_id', dependent: :destroy has_many :synapses2, :class_name => 'Synapse', :foreign_key => 'node2_id', dependent: :destroy @@ -11,6 +12,8 @@ class Topic < ActiveRecord::Base has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings + belongs_to :metacode + validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -39,8 +42,6 @@ class Topic < ActiveRecord::Base topics1 + topics2 end - belongs_to :metacode - scope :relatives1, ->(topic_id = nil) { includes(:topics1) .where('synapses.node1_id = ?', topic_id) @@ -77,8 +78,24 @@ class Topic < ActiveRecord::Base maps.map(&:id) end + def calculated_permission + if defer_to_map + defer_to_map.permission + else + permission + end + end + def as_json(options={}) - super(:methods =>[:user_name, :user_image, :map_count, :synapse_count, :inmaps, :inmapsLinks]) + super(:methods =>[:user_name, :user_image, :map_count, :synapse_count, :inmaps, :inmapsLinks, :calculated_permission, :collaborator_ids]) + end + + def collaborator_ids + if defer_to_map + defer_to_map.editors.select{|mapper| not mapper == self.user }.map(&:id) + else + [] + end end # TODO move to a decorator? diff --git a/app/models/user.rb b/app/models/user.rb index 10a0a71c..a38d7177 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,7 +7,9 @@ class User < ActiveRecord::Base has_many :maps has_many :mappings has_many :tokens - + has_many :user_maps, dependent: :destroy + has_many :shared_maps, through: :user_maps, source: :map + after_create :generate_code devise :database_authenticatable, :recoverable, :rememberable, :trackable, :registerable diff --git a/app/models/user_map.rb b/app/models/user_map.rb new file mode 100644 index 00000000..5e91ecc2 --- /dev/null +++ b/app/models/user_map.rb @@ -0,0 +1,4 @@ +class UserMap < ActiveRecord::Base + belongs_to :map + belongs_to :user +end diff --git a/app/models/webhooks/slack/user_present_on_map.rb b/app/models/webhooks/slack/user_present_on_map.rb new file mode 100644 index 00000000..b8d0fac5 --- /dev/null +++ b/app/models/webhooks/slack/user_present_on_map.rb @@ -0,0 +1,27 @@ +class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base + + def text + "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" + end + # todo: it would be sweet if it sends it with the metacode as the icon_url + + def attachment_fallback + "" #{}"*#{eventable.name}*\n#{eventable.description}\n" + end + + def attachment_title + "" #proposal_link(eventable) + end + + def attachment_text + "" # "#{eventable.description}\n" + end + + def attachment_fields + [{ + title: "nothing", + value: "nothing" + }] #[motion_vote_field] + end + +end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index b1ece0e3..17943086 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -4,7 +4,8 @@ class MapPolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'maps.permission IN (?)' if user - scope.where(permission + ' OR maps.user_id = ?', visible, user.id) + shared_maps = user.shared_maps.map(&:id) + scope.where(permission + ' OR maps.id IN (?) OR maps.user_id = ?', visible, shared_maps, user.id) else scope.where(permission, visible) end @@ -28,7 +29,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user end def export? @@ -48,7 +49,12 @@ class MapPolicy < ApplicationPolicy end def update? - user.present? && (record.permission == 'commons' || record.user == user) + user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) + end + + def access? + # note that this is to edit access + user.present? && record.user == user end def screenshot? diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 042c9a75..9b1a8524 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -4,7 +4,7 @@ class SynapsePolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'synapses.permission IN (?)' if user - scope.where(permission + ' OR synapses.user_id = ?', visible, user.id) + scope.where(permission + ' OR synapses.defer_to_map_id IN (?) OR synapses.user_id = ?', visible, user.shared_maps.map(&:id), user.id) else scope.where(permission, visible) end @@ -17,14 +17,29 @@ class SynapsePolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + if record.defer_to_map.present? + map_policy.show? + else + record.permission == 'commons' || record.permission == 'public' || record.user == user + end end def update? - user.present? && (record.permission == 'commons' || record.user == user) + if not user.present? + false + elsif record.defer_to_map.present? + map_policy.update? + else + record.permission == 'commons' || record.user == user + end end def destroy? record.user == user || admin_override end + + # Helpers + def map_policy + @map_policy ||= Pundit.policy(user, record.defer_to_map) + end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 335a2ed2..2eb2abb6 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -4,7 +4,7 @@ class TopicPolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'topics.permission IN (?)' if user - scope.where(permission + ' OR topics.user_id = ?', visible, user.id) + scope.where(permission + ' OR topics.defer_to_map_id IN (?) OR topics.user_id = ?', visible, user.shared_maps.map(&:id), user.id) else scope.where(permission, visible) end @@ -16,11 +16,21 @@ class TopicPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + if record.defer_to_map.present? + map_policy.show? + else + record.permission == 'commons' || record.permission == 'public' || record.user == user + end end def update? - user.present? && (record.permission == 'commons' || record.user == user) + if not user.present? + false + elsif record.defer_to_map.present? + map_policy.update? + else + record.permission == 'commons' || record.user == user + end end def destroy? @@ -42,4 +52,9 @@ class TopicPolicy < ApplicationPolicy def relatives? show? end + + # Helpers + def map_policy + @map_policy ||= Pundit.policy(user, record.defer_to_map) + end end diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb index 9b2ff400..3c56f82f 100644 --- a/app/serializers/new_map_serializer.rb +++ b/app/serializers/new_map_serializer.rb @@ -12,5 +12,6 @@ class NewMapSerializer < ActiveModel::Serializer has_many :synapses, serializer: NewSynapseSerializer has_many :mappings, serializer: NewMappingSerializer has_many :contributors, root: :users, serializer: NewUserSerializer + has_many :collaborators, root: :users, serializer: NewUserSerializer end diff --git a/app/views/layouts/_newmap.html.erb b/app/views/layouts/_newmap.html.erb index 63825b57..5ebce204 100644 --- a/app/views/layouts/_newmap.html.erb +++ b/app/views/layouts/_newmap.html.erb @@ -25,7 +25,7 @@
    - Collaborate with other mappers on editing this map. Those without accounts can view this map. + Anyone with an account can edit this map. Anyone without an account can only view it.

    COMMONS

    @@ -33,7 +33,7 @@
    - Anyone, with or without an account, can view this map but not edit anything. + Only people you allow can edit this map. Anyone can view it.

    PUBLIC

    @@ -41,14 +41,14 @@
    - Only you can view or edit this map. + Only people you allow can edit this map. No one else can view it.

    PRIVATE

    -

    Collaborate with other mappers on editing this map. Those without accounts can view this map.

    +

    Anyone with an account can edit this map. Anyone without an account can only view it.

    diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index b3099ad9..7e5a6293 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -12,7 +12,7 @@
    - {{contributor_count}} + {{contributor_count}}
    {{{contributor_list}}}
    @@ -181,6 +181,18 @@
    + + diff --git a/app/views/maps/show.html.erb b/app/views/maps/show.html.erb index 5d6859b7..9a74b337 100644 --- a/app/views/maps/show.html.erb +++ b/app/views/maps/show.html.erb @@ -10,6 +10,7 @@ Metamaps.currentPage = <%= @map.id.to_s %>; Metamaps.Active.Map = <%= @map.to_json.html_safe %>; Metamaps.Mappers = <%= @allmappers.to_json.html_safe %>; + Metamaps.Collaborators = <%= @allcollaborators.to_json.html_safe %>; Metamaps.Topics = <%= @alltopics.to_json.html_safe %>; Metamaps.Synapses = <%= @allsynapses.to_json.html_safe %>; Metamaps.Mappings = <%= @allmappings.to_json.html_safe %>; diff --git a/app/views/shared/_forkmap.html.erb b/app/views/shared/_forkmap.html.erb index 78a7aec4..1a262d74 100644 --- a/app/views/shared/_forkmap.html.erb +++ b/app/views/shared/_forkmap.html.erb @@ -26,7 +26,7 @@
    - Collaborate with other mappers on editing this map. Those without accounts can view this map. + Anyone with an account can edit this map. Anyone without an account can only view it.

    COMMONS

    @@ -34,7 +34,7 @@
    - Anyone, with or without an account, can view this map but not edit anything. + Only people you allow can edit this map. Anyone can view it.

    PUBLIC

    @@ -42,14 +42,14 @@
    - Only you can view or edit this map. + Only people you allow can edit this map. No one else can view it.

    PRIVATE

    -

    Collaborate with other mappers on editing this map. Those without accounts can view this map.

    +

    Anyone with an account can edit this map. Anyone without an account can only view it.

    diff --git a/bin/jenkins-test.sh b/bin/jenkins-test.sh index 0afd3f66..f70a1e50 100755 --- a/bin/jenkins-test.sh +++ b/bin/jenkins-test.sh @@ -3,6 +3,7 @@ # jenkins machine prerequisites # sudo aptitude -q -y install libpq-dev # install rvm with user gemsets +# install node and npm source "$HOME/.rvm/scripts/rvm" rvm use $(cat .ruby-version) || \ @@ -19,10 +20,15 @@ export RAILS_ENV=test cp .example-env .env sed -i -e "s/DB_USERNAME='.*'/DB_USERNAME='jenkins'/" .env -#test +# rspec tests bundle install rake db:drop rake db:create rake db:schema:load rake db:migrate COVERAGE=on bundle exec rspec + +# javascript tests +cd app/assets/javascripts +npm install +npm test diff --git a/config/application.rb b/config/application.rb index 0431e58b..570b6959 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,13 +2,9 @@ require File.expand_path('../boot', __FILE__) require 'csv' require 'rails/all' -require 'dotenv' Bundler.require(*Rails.groups) -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -Dotenv.load ".env.#{ENV["RAILS_ENV"]}", '.env' - module Metamaps class Application < Rails::Application config.active_job.queue_adapter = :delayed_job diff --git a/config/environments/development.rb b/config/environments/development.rb index 593fbd3c..bad33ab9 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -30,7 +30,7 @@ Metamaps::Application.configure do port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USERNAME'], password: ENV['SMTP_PASSWORD'], - #domain: ENV['SMTP_DOMAIN'] + domain: ENV['SMTP_DOMAIN'], authentication: 'plain', enable_starttls_auto: true, openssl_verify_mode: 'none' } @@ -41,6 +41,8 @@ Metamaps::Application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log + config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' + # Expands the lines which load the assets config.assets.debug = true end diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 9c1fb05e..83877c08 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Metamaps::Application.config.secret_key_base = '267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c8005f85ee99d6d01b1ab6394cdee9ca7f8c9213a0cf91d3d8d3350f096123e2caccbcc0924f' +Metamaps::Application.config.secret_key_base = ENV['SECRET_KEY_BASE'] diff --git a/config/routes.rb b/config/routes.rb index 345547f4..a60749b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,12 +40,13 @@ Metamaps::Application.routes.draw do post 'maps/:id/events/:event', to: 'maps#events' get 'maps/:id/contains', to: 'maps#contains', as: :contains post 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot + post 'maps/:id/access', to: 'maps#access', as: :access, defaults: {format: :json} get 'explore/active', to: 'maps#activemaps' get 'explore/featured', to: 'maps#featuredmaps' get 'explore/mine', to: 'maps#mymaps' + get 'explore/shared', to: 'maps#sharedmaps' get 'explore/mapper/:id', to: 'maps#usermaps' - devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, :skip => :sessions diff --git a/db/migrate/20160331181959_create_user_maps.rb b/db/migrate/20160331181959_create_user_maps.rb new file mode 100644 index 00000000..2af6e87a --- /dev/null +++ b/db/migrate/20160331181959_create_user_maps.rb @@ -0,0 +1,10 @@ +class CreateUserMaps < ActiveRecord::Migration + def change + create_table :user_maps do |t| + t.references :user, index: true + t.references :map, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb b/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb new file mode 100644 index 00000000..afcd6df2 --- /dev/null +++ b/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb @@ -0,0 +1,6 @@ +class AddDefersToMapToTopicsAndSynapses < ActiveRecord::Migration + def change + add_column :topics, :defer_to_map_id, :integer + add_column :synapses, :defer_to_map_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ad745a2..d3dc9634 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160318141618) do +ActiveRecord::Schema.define(version: 20160401133937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -180,8 +180,9 @@ ActiveRecord::Schema.define(version: 20160318141618) do t.integer "node1_id" t.integer "node2_id" t.integer "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "defer_to_map_id" end add_index "synapses", ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree @@ -217,11 +218,22 @@ ActiveRecord::Schema.define(version: 20160318141618) do t.string "audio_content_type" t.integer "audio_file_size" t.datetime "audio_updated_at" + t.integer "defer_to_map_id" end add_index "topics", ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree add_index "topics", ["user_id"], name: "index_topics_on_user_id", using: :btree + create_table "user_maps", force: :cascade do |t| + t.integer "user_id" + t.integer "map_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "user_maps", ["map_id"], name: "index_user_maps_on_map_id", using: :btree + add_index "user_maps", ["user_id"], name: "index_user_maps_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "name" t.string "email" diff --git a/doc/RspecTesting.md b/doc/RspecTesting.md index 82cf74f7..d17dc0dd 100644 --- a/doc/RspecTesting.md +++ b/doc/RspecTesting.md @@ -1,3 +1,7 @@ +## Testing Javascript + +Javascript tests are under construction, but you can read more in the README in `app/assets/javascripts`. + ## Testing with RSpec RSpec is a ruby gem that allows you to test your code base. This is great - diff --git a/public/famous/main.js b/public/famous/main.js index 2a147e83..7ec38c5c 100644 --- a/public/famous/main.js +++ b/public/famous/main.js @@ -303,7 +303,7 @@ Metamaps.Famous.build = function () { var loggedIn = Metamaps.Active.Mapper ? 'Auth' : ''; - if (section === "mine" || section === "active" || section === "featured") { + if (section === "mine" || section === "shared" || section === "active" || section === "featured") { f.explore.surf.setContent(templates[section + loggedIn + 'Content']); } else if (section === "mapper") { diff --git a/public/famous/templates.js b/public/famous/templates.js index 66e6b9ac..fa1024bb 100644 --- a/public/famous/templates.js +++ b/public/famous/templates.js @@ -19,16 +19,17 @@ t.logoContent += ''; /* logged in explore maps bars */ t.mineAuthContent = '
    My Maps
    '; + t.mineAuthContent += '
    Shared With Me
    '; t.mineAuthContent += '
    Recently Active
    '; - t.mineAuthContent += '
    Featured
    '; + + t.sharedAuthContent = '
    My Maps
    '; + t.sharedAuthContent += '
    Shared With Me
    '; + t.sharedAuthContent += '
    Recently Active
    '; t.activeAuthContent = '
    My Maps
    '; + t.activeAuthContent += '
    Shared With Me
    '; t.activeAuthContent += '
    Recently Active
    '; - t.activeAuthContent += '
    Featured
    '; - t.featuredAuthContent = '
    My Maps
    '; - t.featuredAuthContent += '
    Recently Active
    '; - t.featuredAuthContent += '
    Featured
    '; /* apps bars */ t.registeredAppsContent = '
    Registered Apps
    '; diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb new file mode 100644 index 00000000..8572c8f5 --- /dev/null +++ b/spec/mailers/map_mailer_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe MapMailer, type: :mailer do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb new file mode 100644 index 00000000..60310bf4 --- /dev/null +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -0,0 +1,6 @@ +# Preview all emails at http://localhost:3000/rails/mailers/map_mailer +class MapMailerPreview < ActionMailer::Preview + def invite_to_edit_email + MapMailer.invite_to_edit_email(Map.first, User.first, User.second) + end +end