diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index b3b9cdfe..2fd7ab6a 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -25,6 +25,7 @@
//= require ./src/views/room
//= require ./src/JIT
//= require ./src/Metamaps
+//= require ./src/Metamaps.Import
//= require ./src/Metamaps.JIT
//= require_directory ./shims
//= require_directory ./require
diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb
new file mode 100644
index 00000000..bfce14bd
--- /dev/null
+++ b/app/assets/javascripts/src/Metamaps.Import.js.erb
@@ -0,0 +1,303 @@
+/*
+ * Example tab-separated input:
+ * Some fields will be ignored
+ *
+ * Topics
+ * Id Name Metacode X Y Description Link User Permission
+ * 8 topic8 Action -231 131 admin commons
+ * 5 topic Action -229 -131 admin commons
+ * 7 topic7.1 Action -470 -55 hey admin commons
+ * 2 topic2 Event -57 -63 admin commons
+ * 1 topic1 Catalyst -51 50 admin commons
+ * 6 topic6 Action -425 63 admin commons
+ *
+ * Synapses
+ * Topic1 Topic2 Category Description User Permission
+ * 6 2 from-to admin commons
+ * 6 1 from-to admin commons
+ * 6 5 from-to admin commons
+ * 2 7 from-to admin commons
+ * 8 6 from-to admin commons
+ * 8 1 from-to admin commons
+ *
+ */
+
+Metamaps.Import = {
+ // note that user is not imported
+ topicWhitelist: [
+ 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission'
+ ],
+ synapseWhitelist: [
+ 'topic1', 'topic2', 'category', 'desc', 'description', 'permission'
+ ],
+ cidMappings: {}, //to be filled by import_id => cid mappings
+
+ init: function() {
+ var self = Metamaps.Import;
+ $('body').bind('paste', function(e) {
+ var text = e.originalEvent.clipboardData.getData('text/plain');
+
+ var results;
+ if (text[0] === '{') {
+ try {
+ results = JSON.parse(text);
+ } catch (Error e) {
+ results = false;
+ }
+ } else {
+ results = self.parseTabbedString(text);
+ }
+ if (results === false) return;
+
+ var topics = results.topics;
+ var synapses = results.synapses;
+
+ if (topics.length > 0 || synapses.length > 0) {
+ if (confirm("Are you sure you want to create " + topics.length +
+ " new topics and " + synapses.length + " new synapses?")) {
+ self.importTopics(topics);
+ self.importSynapses(synapses);
+ }//if
+ }//if
+ });
+ },
+
+ abort: function(message) {
+ alert("Sorry, something went wrong!\n\n" + message);
+ console.error(message);
+ },
+
+ simplify: function(string) {
+ return string
+ .replace(/(^\s*|\s*$)/g, '')
+ .toLowerCase();
+ },
+
+ parseTabbedString: function(text) {
+ var self = Metamaps.Import;
+
+ // determine line ending and split lines
+ var delim = "\n";
+ if (text.indexOf("\r\n") !== -1) {
+ delim = "\r\n";
+ } else if (text.indexOf("\r") !== -1) {
+ delim = "\r";
+ }//if
+
+ var STATES = {
+ ABORT: -1,
+ UNKNOWN: 0,
+ TOPICS_NEED_HEADERS: 1,
+ SYNAPSES_NEED_HEADERS: 2,
+ TOPICS: 3,
+ SYNAPSES: 4,
+ };
+
+ // state & lines determine parser behaviour
+ var state = STATES.UNKNOWN;
+ var lines = text.split(delim);
+ var results = { topics: [], synapses: [] }
+ var topicHeaders = [];
+ var synapseHeaders = [];
+
+ lines.forEach(function(line_raw, index) {
+ var line = line_raw.split("\t");
+ var noblanks = line.filter(function(elt) {
+ return elt !== "";
+ });
+ switch(state) {
+ case STATES.UNKNOWN:
+ if (noblanks.length === 0) {
+ state = STATES.UNKNOWN;
+ break;
+ } else if (noblanks.length === 1 && self.simplify(line[0]) === 'topics') {
+ state = STATES.TOPICS_NEED_HEADERS;
+ break;
+ } else if (noblanks.length === 1 && self.simplify(line[0]) === 'synapses') {
+ state = STATES.SYNAPSES_NEED_HEADERS;
+ break;
+ }
+ state = STATES.TOPICS_NEED_HEADERS;
+ // FALL THROUGH - if we're not sure what to do, pretend
+ // we're on the TOPICS_NEED_HEADERS state and parse some headers
+
+ case STATES.TOPICS_NEED_HEADERS:
+ if (noblanks.length < 2) {
+ self.abort("Not enough topic headers on line " + index);
+ state = STATES.ABORT;
+ }
+ topicHeaders = line.map(function(header, index) {
+ return header.toLowerCase().replace('description', 'desc');
+ });
+ state = STATES.TOPICS;
+ break;
+
+ case STATES.SYNAPSES_NEED_HEADERS:
+ if (noblanks.length < 2) {
+ self.abort("Not enough synapse headers on line " + index);
+ state = STATES.ABORT;
+ }
+ synapseHeaders = line.map(function(header, index) {
+ return header.toLowerCase().replace('description', 'desc');
+ });
+ state = STATES.SYNAPSES;
+ break;
+
+ case STATES.TOPICS:
+ if (noblanks.length === 0) {
+ state = STATES.UNKNOWN;
+ } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') {
+ state = STATES.TOPICS_NEED_HEADERS;
+ } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') {
+ state = STATES.SYNAPSES_NEED_HEADERS;
+ } else {
+ var topic = {};
+ line.forEach(function(field, index) {
+ var header = topicHeaders[index];
+ if (self.topicWhitelist.indexOf(header) === -1) return;
+ topic[header] = field;
+ if (['id', 'x', 'y'].indexOf(header) !== -1) {
+ topic[header] = parseInt(topic[header]);
+ }//if
+ });
+ results.topics.push(topic);
+ }
+ break;
+
+ case STATES.SYNAPSES:
+ if (noblanks.length === 0) {
+ state = STATES.UNKNOWN;
+ } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') {
+ state = STATES.TOPICS_NEED_HEADERS;
+ } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') {
+ state = STATES.SYNAPSES_NEED_HEADERS;
+ } else {
+ var synapse = {};
+ line.forEach(function(field, index) {
+ var header = synapseHeaders[index];
+ if (self.synapseWhitelist.indexOf(header) === -1) return;
+ synapse[header] = field;
+ if (['id', 'topic1', 'topic2'].indexOf(header) !== -1) {
+ synapse[header] = parseInt(synapse[header]);
+ }//if
+ });
+ results.synapses.push(synapse);
+ }
+ break;
+ case STATES.ABORT:
+ ;
+ default:
+ self.abort("Invalid state while parsing import data. Check code.");
+ state = STATES.ABORT;
+ }
+ });
+
+ if (state === STATES.ABORT) {
+ return false;
+ } else {
+ return results;
+ }
+ },
+
+
+ importTopics: function(parsedTopics) {
+ var self = Metamaps.Import;
+
+ // up to 25 topics: scale 100
+ // up to 81 topics: scale 200
+ // up to 169 topics: scale 300
+ var scale = Math.floor((Math.sqrt(parsedTopics.length) - 1) / 4) * 100;
+ if (scale < 100) scale = 100;
+ var autoX = -scale;
+ var autoY = -scale;
+
+ parsedTopics.forEach(function(topic) {
+ var x, y;
+ if (topic.x && topic.y) {
+ x = topic.x;
+ y = topic.y;
+ } else {
+ x = autoX;
+ y = autoY;
+ autoX += 50;
+ if (autoX > scale) {
+ autoY += 50;
+ autoX = -scale;
+ }
+ }
+
+ self.createTopicWithParameters(
+ topic.name, topic.metacode, topic.permission,
+ topic.desc, topic.link, x, y, topic.id
+ );
+ });
+ },
+
+ importSynapses: function(parsedSynapses) {
+ var self = Metamaps.Import;
+
+ parsedSynapses.forEach(function(synapse) {
+ self.createSynapseWithParameters(
+ synapse.desc, synapse.category, synapse.permission,
+ synapse.topic1, synapse.topic2
+ );
+ });
+ },
+
+ createTopicWithParameters: function(name, metacode_name, permission, desc,
+ link, xloc, yloc, import_id) {
+ var self = Metamaps.Import;
+ $(document).trigger(Metamaps.Map.events.editedByActiveMapper);
+ var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null;
+ if (metacode === null) return console.error("metacode not found");
+
+ var topic = new Metamaps.Backbone.Topic({
+ name: name,
+ metacode_id: metacode.id,
+ permission: permission || Metamaps.Active.Map.get('permission'),
+ desc: desc,
+ link: link,
+ });
+ Metamaps.Topics.add(topic);
+ self.cidMappings[import_id] = topic.cid;
+
+ var mapping = new Metamaps.Backbone.Mapping({
+ xloc: xloc,
+ yloc: yloc,
+ mappable_id: topic.cid,
+ mappable_type: "Topic",
+ });
+ Metamaps.Mappings.add(mapping);
+
+ // this function also includes the creation of the topic in the database
+ Metamaps.Topic.renderTopic(mapping, topic, true, true);
+
+
+ Metamaps.Famous.viz.hideInstructions();
+ },
+
+ createSynapseWithParameters: function(description, category, permission,
+ node1_id, node2_id) {
+ var self = Metamaps.Import;
+ var topic1 = Metamaps.Topics.get(self.cidMappings[node1_id]);
+ var topic2 = Metamaps.Topics.get(self.cidMappings[node2_id]);
+ var node1 = topic1.get('node');
+ var node2 = topic2.get('node');
+ // TODO check if topic1 and topic2 were sucessfully found...
+
+ var synapse = new Metamaps.Backbone.Synapse({
+ desc: description,
+ category: category,
+ permission: permission,
+ node1_id: node1.id,
+ node2_id: node2.id,
+ });
+
+ var mapping = new Metamaps.Backbone.Mapping({
+ mappable_type: "Synapse",
+ mappable_id: synapse.id,
+ });
+
+ Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true);
+ },
+};
diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb
index 2eb08b70..85b12d48 100644
--- a/app/assets/javascripts/src/Metamaps.js.erb
+++ b/app/assets/javascripts/src/Metamaps.js.erb
@@ -377,7 +377,7 @@ Metamaps.Backbone.init = function () {
mappable_id: this.isNew() ? this.cid : this.id
});
},
- createEdge: function () {
+ createEdge: function (providedMapping) {
var mapping, mappingID;
var synapseID = this.isNew() ? this.cid : this.id;
@@ -391,7 +391,7 @@ Metamaps.Backbone.init = function () {
};
if (Metamaps.Active.Map) {
- mapping = this.getMapping();
+ mapping = providedMapping || this.getMapping();
mappingID = mapping.isNew() ? mapping.cid : mapping.id;
edge.data.$mappings = [];
edge.data.$mappingIDs = [mappingID];
@@ -4614,7 +4614,7 @@ Metamaps.Synapse = {
var edgeOnViz;
- var newedge = synapse.createEdge();
+ var newedge = synapse.createEdge(mapping);
Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data);
edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id);
diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb
index fa743d93..131e5959 100644
--- a/app/controllers/maps_controller.rb
+++ b/app/controllers/maps_controller.rb
@@ -85,11 +85,24 @@ class MapsController < ApplicationController
respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map)
}
format.json { render json: @map }
- format.csv { send_data @map.to_csv }
- format.xls
+ format.csv { redirect_to action: :export, format: :csv }
+ format.xls { redirect_to action: :export, format: :xls }
end
end
+ # GET maps/:id/export
+ def export
+ map = Map.find(params[:id])
+ authorize map
+ exporter = MapExportService.new(current_user, map)
+ respond_to do |format|
+ format.json { render json: exporter.json }
+ format.csv { send_data exporter.csv }
+ format.xls { @spreadsheet = exporter.xls }
+ end
+ end
+
+
# GET maps/:id/contains
def contains
@map = Map.find(params[:id])
diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb
index 77f9ba54..3480c4cd 100644
--- a/app/controllers/metacodes_controller.rb
+++ b/app/controllers/metacodes_controller.rb
@@ -1,5 +1,5 @@
class MetacodesController < ApplicationController
- before_action :require_admin, except: [:index]
+ before_action :require_admin, except: [:index, :show]
# GET /metacodes
# GET /metacodes.json
@@ -18,6 +18,18 @@ class MetacodesController < ApplicationController
end
end
+ # GET /metacodes/1.json
+ # GET /metacodes/Action.json
+ # GET /metacodes/action.json
+ def show
+ @metacode = Metacode.where('DOWNCASE(name) = ?', downcase(params[:name])).first if params[:name]
+ @metacode = Metacode.find(params[:id]) unless @metacode
+
+ respond_to do |format|
+ format.json { render json: @metacode }
+ end
+ end
+
# GET /metacodes/new
# GET /metacodes/new.json
def new
diff --git a/app/models/map.rb b/app/models/map.rb
index 5cd30bbe..d9eb6a18 100644
--- a/app/models/map.rb
+++ b/app/models/map.rb
@@ -84,24 +84,6 @@ class Map < ActiveRecord::Base
json
end
- def to_csv(options = {})
- CSV.generate(options) do |csv|
- csv << ["id", "name", "metacode", "desc", "link", "user.name", "permission", "synapses"]
- self.topics.each do |topic|
- csv << [
- topic.id,
- topic.name,
- topic.metacode.name,
- topic.desc,
- topic.link,
- topic.user.name,
- topic.permission,
- topic.synapses_csv("text")
- ]
- end
- end
- end
-
def decode_base64(imgBase64)
decoded_data = Base64.decode64(imgBase64)
diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb
index 65f721bf..5b4bbfa9 100644
--- a/app/policies/map_policy.rb
+++ b/app/policies/map_policy.rb
@@ -31,6 +31,10 @@ class MapPolicy < ApplicationPolicy
record.permission == 'commons' || record.permission == 'public' || record.user == user
end
+ def export?
+ show?
+ end
+
def contains?
show?
end
diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb
new file mode 100644
index 00000000..94a0cf17
--- /dev/null
+++ b/app/services/map_export_service.rb
@@ -0,0 +1,84 @@
+class MapExportService < Struct.new(:user, :map)
+ def json
+ # marshal_dump turns OpenStruct into a Hash
+ {
+ topics: exportable_topics.map(&:marshal_dump),
+ synapses: exportable_synapses.map(&:marshal_dump)
+ }
+ end
+
+ def csv(options = {})
+ CSV.generate(options) do |csv|
+ to_spreadsheet.each do |line|
+ csv << line
+ end
+ end
+ end
+
+ def xls
+ to_spreadsheet
+ end
+
+ private
+
+ def topic_headings
+ [:id, :name, :metacode, :x, :y, :description, :link, :user, :permission]
+ end
+ def synapse_headings
+ [:topic1, :topic2, :category, :description, :user, :permission]
+ end
+
+ def exportable_topics
+ visible_topics ||= Pundit.policy_scope!(user, map.topics)
+ topic_mappings = Mapping.includes(mappable: [:metacode, :user])
+ .where(mappable: visible_topics, map: map)
+ topic_mappings.map do |mapping|
+ topic = mapping.mappable
+ OpenStruct.new(
+ id: topic.id,
+ name: topic.name,
+ metacode: topic.metacode.name,
+ x: mapping.xloc,
+ y: mapping.yloc,
+ description: topic.desc,
+ link: topic.link,
+ user: topic.user.name,
+ permission: topic.permission
+ )
+ end
+ end
+
+ def exportable_synapses
+ visible_synapses = Pundit.policy_scope!(user, map.synapses)
+ visible_synapses.map do |synapse|
+ OpenStruct.new(
+ topic1: synapse.node1_id,
+ topic2: synapse.node2_id,
+ category: synapse.category,
+ description: synapse.desc,
+ user: synapse.user.name,
+ permission: synapse.permission
+ )
+ end
+ end
+
+ def to_spreadsheet
+ spreadsheet = []
+ spreadsheet << ["Topics"]
+ spreadsheet << topic_headings.map(&:capitalize)
+ exportable_topics.each do |topics|
+ # convert exportable_topics into an array of arrays
+ spreadsheet << topic_headings.map { |h| topics.send(h) }
+ end
+
+ spreadsheet << []
+ spreadsheet << ["Synapses"]
+ spreadsheet << synapse_headings.map(&:capitalize)
+ exportable_synapses.each do |synapse|
+ # convert exportable_synapses into an array of arrays
+ spreadsheet << synapse_headings.map { |h| synapse.send(h) }
+ end
+
+ spreadsheet
+ end
+end
diff --git a/app/views/maps/export.xls.erb b/app/views/maps/export.xls.erb
new file mode 100644
index 00000000..7030d501
--- /dev/null
+++ b/app/views/maps/export.xls.erb
@@ -0,0 +1,9 @@
+
+ <% @spreadsheet.each do |line| %>
+
+ <% line.each do |field| %>
+ <%= field %> |
+ <% end %>
+
+ <% end %>
+
diff --git a/app/views/maps/show.xls.erb b/app/views/maps/show.xls.erb
deleted file mode 100644
index d00dd36e..00000000
--- a/app/views/maps/show.xls.erb
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- ID |
- Name |
- Metacode |
- Description |
- Link |
- Username |
- Permission |
- Synapses |
-
- <% @map.topics.each do |topic| %>
-
- <%= topic.id %> |
- <%= topic.name %> |
- <%= topic.metacode.name %> |
- <%= topic.desc %> |
- <%= topic.link %> |
- <%= topic.user.name %> |
- <%= topic.permission %> |
- <% topic.synapses_csv.each do |s_text| %>
- <%= s_text %> |
- <% end %>
-
- <% end %>
-
diff --git a/config/routes.rb b/config/routes.rb
index c68091ed..a9f82d9c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -23,7 +23,10 @@ Metamaps::Application.routes.draw do
resources :messages, only: [:show, :create, :update, :destroy]
resources :mappings, except: [:index, :new, :edit]
resources :metacode_sets, :except => [:show]
- resources :metacodes, :except => [:show, :destroy]
+
+ resources :metacodes, :except => [:destroy]
+ get 'metacodes/:name', to: 'metacodes#show'
+
resources :synapses, except: [:index, :new, :edit]
resources :topics, except: [:index, :new, :edit] do
get :autocomplete_topic, :on => :collection
@@ -33,6 +36,8 @@ Metamaps::Application.routes.draw do
get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives
resources :maps, except: [:index, :new, :edit]
+ get 'maps/:id/export', to: 'maps#export'
+
get 'explore/active', to: 'maps#activemaps'
get 'explore/featured', to: 'maps#featuredmaps'
get 'explore/mine', to: 'maps#mymaps'