/* 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() // for mobile $('#header_content').html(map.get('name')) } $.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()) var querystring = window.location.search.replace(/^\?/, '') if (querystring == 'new') { self.open() window.setTimeout(self.requestMapTitle, 3000) } }, requestMapTitle: function () { var t = prompt('Please enter a name for your map. You can also set it any time using the map info panel.') if (t) { Metamaps.Active.Map.set('name', t) Metamaps.Active.Map.save() $('#mapInfoName span').text(t) document.title = t + ' | Metamaps' } }, 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 () { if (!Metamaps.Active.Map) return 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