├── lib ├── tasks │ ├── .gitkeep │ ├── mongo.rake │ └── sample_weather_bootstrap.rake └── modules │ └── randomizer.rb ├── public ├── favicon.ico ├── stylesheets │ ├── .gitkeep │ ├── reset.css │ ├── fonts.css │ ├── imgareaselect-default.css │ ├── jquery.annotate.css │ └── styles.css ├── images │ ├── 1.jpeg │ ├── 2.jpeg │ ├── rails.png │ ├── border-h.gif │ ├── border-v.gif │ ├── cancel.png │ ├── bgtexture.jpg │ ├── border-anim-h.gif │ └── border-anim-v.gif ├── javascripts │ ├── .DS_Store │ ├── application.js │ ├── transcription.js │ ├── rails.js │ ├── jquery.imgareaselect.pack.js │ ├── jquery.imgareaselect.js │ └── jquery.annotate.js ├── robots.txt ├── 422.html ├── 404.html └── 500.html ├── vendor └── plugins │ └── .gitkeep ├── spec └── javascripts │ ├── helpers │ ├── .gitkeep │ └── jasmine_jquery-1.2.0.js │ ├── jquery.annotate.js │ └── support │ ├── jasmine_config.rb │ ├── jasmine_runner.rb │ └── jasmine.yml ├── app ├── helpers │ ├── home_helper.rb │ ├── templates_helper.rb │ ├── application_helper.rb │ ├── transcriptions_helper.rb │ └── asset_collections_helper.rb ├── controllers │ ├── assets_controller.rb │ ├── home_controller.rb │ ├── asset_collections_controller.rb │ ├── templates_controller.rb │ ├── application_controller.rb │ └── transcriptions_controller.rb ├── views │ ├── home │ │ ├── about.html.erb │ │ └── index.html.erb │ ├── templates │ │ ├── show.html.erb │ │ └── new.html.erb │ ├── shared │ │ ├── _flash.html.erb │ │ └── _banner.html.erb │ ├── transcriptions │ │ ├── show.html.erb │ │ ├── add_entity.js.erb │ │ ├── index.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── layouts │ │ ├── application.html.erb │ │ ├── asset_collections.html.erb │ │ ├── templates.html.erb │ │ └── transcriptions.html.erb │ └── asset_collections │ │ ├── index.html.erb │ │ └── show.html.erb └── models │ ├── template.rb │ ├── annotation.rb │ ├── zooniverse_user.rb │ ├── entity.rb │ ├── field.rb │ ├── asset_collection.rb │ ├── transcription.rb │ └── asset.rb ├── test ├── unit │ ├── helpers │ │ ├── books_helper_test.rb │ │ ├── home_helper_test.rb │ │ ├── annotations_helper_test.rb │ │ ├── templates_helper_test.rb │ │ └── transcriptions_helper_test.rb │ ├── field_test.rb │ ├── annotation_test.rb │ ├── template_test.rb │ ├── entity_test.rb │ ├── transcription_test.rb │ ├── zooniverse_user_test.rb │ ├── asset_collection_test.rb │ └── asset_test.rb ├── functional │ ├── asset_collections_controller_test.rb │ ├── home_controller_test.rb │ ├── transcriptions_controller_test.rb │ └── templates_controller_test.rb ├── performance │ └── browsing_test.rb ├── factories.rb └── test_helper.rb ├── config.ru ├── .gitignore ├── config ├── environment.rb ├── initializers │ ├── mongo_mapper.rb │ ├── mime_types.rb │ ├── inflections.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── secret_token.rb │ └── barista_config.rb ├── site_settings.hudson.yml ├── mongodb.hudson.yml ├── locales │ └── en.yml ├── boot.rb ├── routes.rb ├── database.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── doc └── README_FOR_APP ├── Rakefile ├── script └── rails ├── db └── seeds.rb ├── Gemfile ├── README.md ├── Gemfile.lock └── license.txt /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/templates_helper.rb: -------------------------------------------------------------------------------- 1 | module TemplatesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/transcriptions_helper.rb: -------------------------------------------------------------------------------- 1 | module TranscriptionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/asset_collections_helper.rb: -------------------------------------------------------------------------------- 1 | module AssetCollectionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/javascripts/jquery.annotate.js: -------------------------------------------------------------------------------- 1 | descibe("jquery.annotation.js", function(){ 2 | 3 | }); -------------------------------------------------------------------------------- /app/controllers/assets_controller.rb: -------------------------------------------------------------------------------- 1 | class AssetsController < ApplicationController 2 | 3 | end -------------------------------------------------------------------------------- /public/images/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/1.jpeg -------------------------------------------------------------------------------- /public/images/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/2.jpeg -------------------------------------------------------------------------------- /public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/rails.png -------------------------------------------------------------------------------- /public/images/border-h.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/border-h.gif -------------------------------------------------------------------------------- /public/images/border-v.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/border-v.gif -------------------------------------------------------------------------------- /public/images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/cancel.png -------------------------------------------------------------------------------- /public/images/bgtexture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/bgtexture.jpg -------------------------------------------------------------------------------- /public/javascripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/javascripts/.DS_Store -------------------------------------------------------------------------------- /public/images/border-anim-h.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/border-anim-h.gif -------------------------------------------------------------------------------- /public/images/border-anim-v.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooniverse-glacier/Scribe/HEAD/public/images/border-anim-v.gif -------------------------------------------------------------------------------- /test/unit/helpers/books_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BooksHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/home_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HomeHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/annotations_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AnnotationsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/templates_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TemplatesHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/transcriptions_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranscriptionsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Scribe::Application 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | db/scribe* 4 | .DS_Store 5 | log/*.log 6 | tmp/**/* 7 | db/mongod.lock 8 | .project 9 | config/mongodb.yml 10 | config/site_settings.yml 11 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Scribe::Application.initialize! 6 | -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /app/views/home/about.html.erb: -------------------------------------------------------------------------------- 1 |

This is an about page

2 | 3 |
4 |

This is where we can describe the project etc 5 |

6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /config/initializers/mongo_mapper.rb: -------------------------------------------------------------------------------- 1 | config = YAML.load_file(Rails.root + 'config' + 'mongodb.yml') 2 | MongoMapper.setup(config, Rails.env, { :logger => nil }) 3 | MongoMapper.handle_passenger_forking 4 | -------------------------------------------------------------------------------- /app/views/templates/show.html.erb: -------------------------------------------------------------------------------- 1 | Template: <%= @template.name %> 2 | 3 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | before_filter CASClient::Frameworks::Rails::GatewayFilter 3 | 4 | def index 5 | 6 | end 7 | 8 | def about 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/site_settings.hudson.yml: -------------------------------------------------------------------------------- 1 | common: 2 | application_name: "Scribe" 3 | application_name_short: "Scribe" 4 | release_name: "Gilgamesh" 5 | location: "0.0.0.0:3000/" 6 | 7 | development: 8 | 9 | production: 10 | 11 | test: 12 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /app/views/shared/_flash.html.erb: -------------------------------------------------------------------------------- 1 | <%- [:error, :warning, :notice].each do |level| -%> 2 | <%- if flash[level] -%> 3 |
4 |

<%= flash[level] %>

5 |
6 | <%- end -%> 7 | <%- end -%> -------------------------------------------------------------------------------- /test/functional/asset_collections_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AssetCollectionsControllerTest < ActionController::TestCase 4 | # Replace this with your real tests. 5 | test "the truth" do 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Scribe::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/performance_test_help' 3 | 4 | # Profiling results for each test method are written to tmp/performance. 5 | class BrowsingTest < ActionDispatch::PerformanceTest 6 | def test_homepage 7 | get '/' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/mongodb.hudson.yml: -------------------------------------------------------------------------------- 1 | development: &global_settings 2 | host: 127.0.0.1 3 | database: scribe-development 4 | port: 27017 5 | 6 | test: 7 | database: scribe-test 8 | <<: *global_settings 9 | 10 | production: 11 | host: 127.0.0.1 12 | database: scribe-production 13 | 14 | <<: *global_settings 15 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /test/unit/field_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FieldTest < ActiveSupport::TestCase 4 | context "A Field" do 5 | setup do 6 | @field = Factory :text_field 7 | end 8 | 9 | should_associate :entity 10 | should_have_keys :name, :field_key, :kind, :initial_value, :options 11 | 12 | end 13 | end -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | controllers: 6 | application: 7 | not_authorised: "You do not have sufficient privileges to access this page" 8 | -------------------------------------------------------------------------------- /app/models/template.rb: -------------------------------------------------------------------------------- 1 | # Template defines the entities that need transcribing 2 | class Template 3 | include MongoMapper::Document 4 | 5 | key :name, String 6 | key :description, String 7 | key :project, String 8 | 9 | key :default_zoom, Float 10 | 11 | timestamps! 12 | 13 | many :assets 14 | many :entities 15 | end 16 | -------------------------------------------------------------------------------- /test/unit/annotation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AnnotationTest < ActiveSupport::TestCase 4 | context "An Annotion" do 5 | setup do 6 | @annotion = Factory :annotation 7 | end 8 | 9 | should_associate :transcription, :entity 10 | should_have_keys :bounds, :data, :created_at, :updated_at 11 | 12 | end 13 | end -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /test/unit/template_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TemplateTest < ActiveSupport::TestCase 4 | context "A Template" do 5 | setup do 6 | @template = Factory :template 7 | end 8 | 9 | should_associate :assets, :entities 10 | should_have_keys :name, :description, :project, :default_zoom, :created_at, :updated_at 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/shared/_banner.html.erb: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /app/models/annotation.rb: -------------------------------------------------------------------------------- 1 | # A collection of Annotations makes up a Transcription 2 | class Annotation 3 | include MongoMapper::Document 4 | 5 | key :bounds, Hash # this is x-rel, y-rel, with-rel, height-rel measure (0..1) 6 | key :data, Hash # A hash looking something like :field_key => "Some value" 7 | 8 | timestamps! 9 | 10 | belongs_to :transcription 11 | belongs_to :entity 12 | end -------------------------------------------------------------------------------- /test/unit/entity_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EntityTest < ActiveSupport::TestCase 4 | context "An Entity" do 5 | setup do 6 | @entity = Factory :entity 7 | end 8 | 9 | should_associate :template, :fields 10 | should_have_keys :name, :description, :help, :height, :resizeable, :width, :bounds, :zoom, :created_at, :updated_at 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | <%= SiteConfig.application_name %> 4 |

<%= link_to "Click here", "/transcribe" %> to start transcribing.

5 |

<%= link_to "Click here", "/about" %> to learn about the project.

6 | 7 |
8 | 9 | <%= render :partial => "shared/flash", :locals => { :flash => flash } %> 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/transcriptions/show.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= SiteConfig.application_name %> 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag 'jquery-1.4.4.js' %> 7 | <%= javascript_include_tag 'rails' %> 8 | 9 | <%= csrf_meta_tag %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | 17 | <%=render :partial=>"/shared/banner"%> 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /lib/tasks/mongo.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :test do 3 | task :prepare do 4 | # Stub out for MongoDB 5 | end 6 | end 7 | end 8 | 9 | # TODO more indexes please 10 | task :build_indexes => :environment do 11 | puts "Building indexes for Asset" 12 | drop_indexes_on(Asset) 13 | Asset.ensure_index [['random_number', 1]] 14 | 15 | end 16 | 17 | def drop_indexes_on(model) 18 | model.collection.drop_indexes if model.count > 0 19 | end -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Scribe::Application.config.session_store :cookie_store, :key => '_scribe_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Scribe::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /app/models/zooniverse_user.rb: -------------------------------------------------------------------------------- 1 | class ZooniverseUser 2 | include MongoMapper::Document 3 | 4 | key :zooniverse_user_id, Integer, :required => true 5 | key :name, String, :required => true 6 | key :public_name, String 7 | key :email, String 8 | key :admin, Boolean, :default => false 9 | 10 | timestamps! 11 | 12 | many :transcriptions 13 | 14 | # True if user is an admin or moderator 15 | def privileged? 16 | self.admin? 17 | end 18 | end -------------------------------------------------------------------------------- /app/views/layouts/asset_collections.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scribe 4 | <%= stylesheet_link_tag :all %> 5 | <%= javascript_include_tag 'jquery-1.4.4.js' %> 6 | <%= javascript_include_tag 'jquery-ui-1.8.9.min' %> 7 | <%= javascript_include_tag 'rails' %> 8 | 9 | <%= csrf_meta_tag %> 10 | 11 | <%= yield :head %> 12 | 13 | 14 | 15 | <%= yield %> 16 | 17 | <%=render :partial=>"/shared/banner"%> 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/controllers/asset_collections_controller.rb: -------------------------------------------------------------------------------- 1 | class AssetCollectionsController < ApplicationController 2 | 3 | #refactor this 4 | def index 5 | @collections = AssetCollection.all.select{|b| b.assets.count>0} 6 | end 7 | 8 | def show 9 | @collection = AssetCollection.find(params[:id]) 10 | respond_to do |format| 11 | format.html 12 | format.json { render :json => @collection.to_json(:include =>:assets) } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Scribe::Application.routes.draw do 2 | resources :templates 3 | 4 | resources :assets do 5 | resource :template 6 | end 7 | 8 | resources :annotations 9 | 10 | resources :asset_collections 11 | 12 | resources :transcriptions do 13 | collection do 14 | post :add_entity 15 | end 16 | end 17 | 18 | match 'transcribe' => "transcriptions#new" 19 | match 'about' => 'home#about' 20 | 21 | root :to => 'home#index' 22 | end 23 | -------------------------------------------------------------------------------- /app/views/transcriptions/add_entity.js.erb: -------------------------------------------------------------------------------- 1 | if ($('#transcription_entity_<%= @view_params[:count] %>').length == 0){ 2 | $('#transcription_container').append("<%= escape_javascript(render :partial => 'transcription')%>"); 3 | } else { 4 | } 5 | 6 | 7 | 8 | // $('#transcription_entity_<%= @count %>').draggable(); 9 | // $('#remove_transcription_entity_<%= @count %>').bind('click', function(event){ 10 | // $('#transcription_entity_<%= @count %>').remove(); 11 | // event.stopPropagation(); 12 | // }) 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/views/asset_collections/index.html.erb: -------------------------------------------------------------------------------- 1 |

Collections

2 | 3 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Scribe::Application.config.secret_token = '18364154248fa7d3bec21ccb8163d5decdb5208a9766a74b57029376f6099924a92a97f0f3bea23a820886413088e06d3531ce362d65f5f79318d4e0dfc75b87' 8 | -------------------------------------------------------------------------------- /app/views/transcriptions/index.html.erb: -------------------------------------------------------------------------------- 1 |

My Transcriptions

2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/models/entity.rb: -------------------------------------------------------------------------------- 1 | # An Entity is the 'thing' being transcribed e.g. a weather observation and is composed of many Fields 2 | class Entity 3 | include MongoMapper::Document 4 | 5 | # For the UI - can be used to build a tutorial 6 | key :name, String 7 | key :description, String 8 | key :help, String 9 | 10 | # Can this entity be resized in the UI? 11 | key :resizeable, Boolean, :default => false 12 | key :width, Integer 13 | key :height, Integer 14 | key :bounds, Array 15 | key :zoom, Float 16 | 17 | timestamps! 18 | 19 | belongs_to :template 20 | many :fields 21 | end -------------------------------------------------------------------------------- /public/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2006, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 0.10.0 6 | */ 7 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,p,blockquote,th,td{margin:0;padding:0;} 8 | table{border-collapse:collapse;border-spacing:0;} 9 | fieldset,img{border:0;} 10 | address,caption,cite,code,dfn,th,var{font-style:normal;font-weight:normal;} 11 | ol,ul {list-style:none;} 12 | caption,th {text-align:left;} 13 | h1,h2,h3,h4,h5,h6{font-size:100%;} 14 | q:before,q:after{content:'';} -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine_config.rb: -------------------------------------------------------------------------------- 1 | module Jasmine 2 | class Config 3 | 4 | # Add your overrides or custom config code here 5 | 6 | end 7 | end 8 | 9 | 10 | # Note - this is necessary for rspec2, which has removed the backtrace 11 | module Jasmine 12 | class SpecBuilder 13 | def declare_spec(parent, spec) 14 | me = self 15 | example_name = spec["name"] 16 | @spec_ids << spec["id"] 17 | backtrace = @example_locations[parent.description + " " + example_name] 18 | parent.it example_name, {} do 19 | me.report_spec(spec["id"]) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/field.rb: -------------------------------------------------------------------------------- 1 | # The idea of the field is that it defines the layout of the thing being transcribed at the most fine level. e.g. a text-field 2 | class Field 3 | include MongoMapper::EmbeddedDocument 4 | 5 | key :name, String 6 | key :field_key, String 7 | key :kind, String # text/select 8 | key :initial_value, String 9 | 10 | # This options hash has the descripition of the field with options. 11 | key :options, Hash 12 | key :validations, Array 13 | 14 | # TODO - should validate within scope of entity 15 | # validates_uniqueness_of :field_key, :scope => 'entity_id' ? 16 | 17 | belongs_to :entity 18 | end -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /app/views/layouts/templates.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scribe 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag 'transcription' %> 7 | <%= javascript_include_tag 'jquery-1.4.4.js' %> 8 | <%= javascript_include_tag 'jquery-ui-1.8.9.min' %> 9 | <%= javascript_include_tag 'jquery.imgareaselect' %> 10 | <%= javascript_include_tag 'jquery.annotate'%> 11 | <%= javascript_include_tag 'rails' %> 12 | 13 | <%= csrf_meta_tag %> 14 | 15 | <%= yield :head %> 16 | 17 | 18 | 19 | <%= yield %> 20 | 21 | <%=render :partial=>"/shared/banner"%> 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/views/layouts/transcriptions.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scribe 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag 'transcription' %> 7 | <%= javascript_include_tag 'jquery-1.4.4.js' %> 8 | <%= javascript_include_tag 'jquery-ui-1.8.9.min' %> 9 | <%= javascript_include_tag 'jquery.imgareaselect' %> 10 | <%= javascript_include_tag 'jquery.annotate'%> 11 | <%= javascript_include_tag 'rails' %> 12 | 13 | <%= csrf_meta_tag %> 14 | 15 | <%= yield :head %> 16 | 17 | 18 | 19 | <%= yield %> 20 | 21 | <%=render :partial=>"/shared/banner"%> 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/javascripts/transcription.js: -------------------------------------------------------------------------------- 1 | function initialise_drag_box(){ 2 | var entity_count = $('.transcription_entity').length; 3 | var drag_box_id = entity_count; 4 | $('#transcription_image').imgAreaSelect({ 5 | handles: true, 6 | areaId: drag_box_id, 7 | onSelectEnd: function render_options(img, box){ 8 | $.post( 9 | '/transcriptions/add_entity', 10 | { x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, count: entity_count } 11 | ); 12 | } 13 | }); 14 | } 15 | 16 | function render_options(img, box){ 17 | alert(val(box.x1)); 18 | } 19 | function remove_transcription_entity(element){ 20 | $(element).remove(); 21 | } -------------------------------------------------------------------------------- /app/models/asset_collection.rb: -------------------------------------------------------------------------------- 1 | class AssetCollection 2 | include MongoMapper::Document 3 | key :title, String, :required => true 4 | key :author, String, :required => false 5 | key :extern_ref, String 6 | 7 | many :assets 8 | 9 | def front_page 10 | self.assets.where.order(:order).first 11 | end 12 | 13 | def next_unseen_for_user(user) 14 | seen = user.transcriptions.collect{|t| t.asset_id} 15 | self.assets.active.where(:id.nin=>seen).first 16 | end 17 | 18 | def remaining_active 19 | self.assets.where(:done=>false).count 20 | end 21 | 22 | def active? 23 | self.remaining_active != 0 24 | end 25 | end -------------------------------------------------------------------------------- /test/unit/transcription_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranscriptionTest < ActiveSupport::TestCase 4 | context "A Transcription" do 5 | setup do 6 | @asset = Factory :asset 7 | @user = Factory :zooniverse_user 8 | @transcription = Factory :transcription, :zooniverse_user => @user, :asset => @asset 9 | 10 | end 11 | 12 | should_associate :asset, :zooniverse_user, :annotations 13 | should_have_keys :page_data, :created_at, :updated_at 14 | 15 | should "incremented classification count" do 16 | assert_equal @transcription.asset.classification_count, 1 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine_runner.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(ENV['JASMINE_GEM_PATH']) if ENV['JASMINE_GEM_PATH'] # for gem testing purposes 2 | 3 | require 'rubygems' 4 | require 'jasmine' 5 | require 'rspec' 6 | jasmine_config_overrides = File.expand_path(File.join(File.dirname(__FILE__), 'jasmine_config.rb')) 7 | require jasmine_config_overrides if File.exists?(jasmine_config_overrides) 8 | 9 | jasmine_config = Jasmine::Config.new 10 | spec_builder = Jasmine::SpecBuilder.new(jasmine_config) 11 | 12 | should_stop = false 13 | 14 | RSpec.configuration.after(:suite) do 15 | spec_builder.stop if should_stop 16 | end 17 | 18 | spec_builder.start 19 | should_stop = true 20 | spec_builder.declare_suites -------------------------------------------------------------------------------- /test/unit/zooniverse_user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ZooniverseUserTest < ActiveSupport::TestCase 4 | context "A Zooniverse User" do 5 | setup do 6 | @zooniverse_user = Factory :zooniverse_user 7 | end 8 | 9 | should_associate :transcriptions 10 | should_have_keys :zooniverse_user_id, :name, :public_name, :email, :admin, :created_at, :updated_at 11 | 12 | should "not be privileged" do 13 | assert !@zooniverse_user.privileged? 14 | end 15 | end 16 | 17 | context "An Admin Zooniverse User" do 18 | setup do 19 | @zooniverse_user = Factory :admin_user 20 | end 21 | 22 | should "be privileged" do 23 | assert @zooniverse_user.privileged? 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/models/transcription.rb: -------------------------------------------------------------------------------- 1 | # A Transcription is a user-transcription of an Asset and is composed of many Annotations 2 | class Transcription 3 | include MongoMapper::Document 4 | 5 | after_save :update_classification_count 6 | 7 | key :page_data , Hash 8 | 9 | timestamps! 10 | 11 | belongs_to :asset 12 | belongs_to :zooniverse_user 13 | 14 | many :annotations 15 | 16 | 17 | def update_classification_count 18 | self.asset.increment_classification_count 19 | end 20 | 21 | def add_annotations_from_json(new_annotations) 22 | unless new_annotations.blank? 23 | new_annotations.values.collect do |ann| 24 | entity = Entity.find_by_name ann["kind"] 25 | if entity 26 | self.annotations << Annotation.create(:data => ann[:data], :entity => entity, :bounds => ann[:bounds]) 27 | else 28 | puts "could not find entity type #{ann['kind']}" 29 | end 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /public/stylesheets/fonts.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2006, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 0.10.0 6 | */ 7 | 8 | /** 9 | * 84.5% for !IE, keywords for IE 10 | * Percents could work for IE, but for backCompat purposes, we are using keywords. 11 | * x-small is for IE < 6 and IE6 quirks mode. 12 | * 13 | */ 14 | body {font:12px arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;} 15 | table {font-size:inherit;font:100%;} 16 | 17 | /** 18 | * 99% for safari; 100% is too large 19 | */ 20 | select, input, textarea {font:99% arial,helvetica,clean,sans-serif;} 21 | 22 | /** 23 | * Bump up !IE to get to 13px equivalent 24 | */ 25 | pre, code {font:115% monospace;*font-size:100%;} 26 | 27 | /** 28 | * Default line-height based on font-size rather than "computed-value" 29 | * see: http://www.w3.org/TR/CSS21/visudet.html#line-height 30 | */ 31 | body * {line-height:1.22em;} 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '3.0.7' 4 | gem 'rubycas-client', '~> 2.2.1' 5 | gem 'rake', '0.8.7' 6 | gem 'bson_ext' 7 | gem 'mongo_mapper', :git => 'https://github.com/jnunemaker/mongomapper.git', :branch => 'rails3' 8 | #gem 'barista' 9 | 10 | gem 'heroku' 11 | 12 | 13 | # To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+) 14 | # gem 'ruby-debug' 15 | # gem 'ruby-debug19' 16 | 17 | # Bundle the extra gems: 18 | # gem 'bj' 19 | # gem 'nokogiri' 20 | # gem 'sqlite3-ruby', :require => 'sqlite3' 21 | # gem 'aws-s3', :require => 'aws/s3' 22 | 23 | # Bundle gems for the local environment. Make sure to 24 | # put test-only gems in this group so their generators 25 | # and rake tasks are available in development mode: 26 | group :development, :test do 27 | gem 'webrat' 28 | gem 'shoulda' 29 | gem 'factory_girl_rails' 30 | gem 'mocha' 31 | gem 'autotest' 32 | gem 'autotest-rails' 33 | gem 'jasmine' 34 | 35 | end 36 | 37 | -------------------------------------------------------------------------------- /public/stylesheets/imgareaselect-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * imgAreaSelect default style 3 | */ 4 | 5 | .imgareaselect-border1 { 6 | background: url('../images/border-v.gif') repeat-y left top; 7 | } 8 | 9 | .imgareaselect-border2 { 10 | background: url('../images/border-h.gif') repeat-x left top; 11 | } 12 | 13 | .imgareaselect-border3 { 14 | background: url('../images/border-v.gif') repeat-y right top; 15 | } 16 | 17 | .imgareaselect-border4 { 18 | background: url('../images/border-h.gif') repeat-x left bottom; 19 | } 20 | 21 | .imgareaselect-border1, .imgareaselect-border2, 22 | .imgareaselect-border3, .imgareaselect-border4 { 23 | opacity: 0.5; 24 | filter: alpha(opacity=50); 25 | } 26 | 27 | .imgareaselect-handle { 28 | background-color: #fff; 29 | border: solid 1px #000; 30 | opacity: 0.5; 31 | filter: alpha(opacity=50); 32 | } 33 | 34 | .imgareaselect-outer { 35 | background-color: #000; 36 | opacity: 0.3; 37 | filter: alpha(opacity=50); 38 | } 39 | 40 | .imgareaselect-selection { 41 | } -------------------------------------------------------------------------------- /test/functional/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HomeControllerTest < ActionController::TestCase 4 | context "Home controller" do 5 | setup do 6 | @controller = HomeController.new 7 | @request = ActionController::TestRequest.new 8 | @response = ActionController::TestResponse.new 9 | end 10 | 11 | context "#index logged in" do 12 | setup do 13 | standard_cas_login_without_stub 14 | CASClient::Frameworks::Rails::GatewayFilter.stubs(:filter).returns(true) 15 | get :index 16 | end 17 | 18 | should respond_with :success 19 | end 20 | 21 | context "#index not logged in" do 22 | setup do 23 | get :index 24 | end 25 | 26 | should respond_with :redirect 27 | 28 | should "redirect_to '/cas server with gateway set to true'" do 29 | assert_redirected_to 'https://login.zooniverse.org/login?service=http%3A%2F%2Ftest.host%2F&gateway=true' 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Scribe::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | end 26 | 27 | -------------------------------------------------------------------------------- /app/controllers/templates_controller.rb: -------------------------------------------------------------------------------- 1 | class TemplatesController < ApplicationController 2 | before_filter CASClient::Frameworks::Rails::GatewayFilter 3 | before_filter :require_privileged_user, :except => [ :show ] 4 | 5 | def index 6 | @templates = Template.all 7 | end 8 | 9 | def show 10 | @asset = Asset.find(params[:asset_id]) 11 | 12 | respond_to do |format| 13 | format.json { 14 | render :json => @asset.template.to_json(:include => { :entities => { :include => :fields }}) 15 | } 16 | end 17 | end 18 | 19 | def new 20 | 21 | end 22 | 23 | def create 24 | template = params['template'] 25 | entities_data = template['entities'].values 26 | 27 | entities=entities_data.collect do |e| 28 | fields_data = e["fields"].values 29 | fields = fields_data.collect do |f| 30 | f=Field.new(:name=>f["f_name"], :field_key=>f["f_name"], :kind => f["f_type"]) 31 | end 32 | Entity.create(:name=>e['name'], :description => e['description'], :help=>e['help'], :fields=>fields, :default_zoom=>e['default_zoom']) 33 | end 34 | 35 | Template.create(:name => template['name'], :description => template['description'], :entities => entities) 36 | 37 | redirect_to '/transcribe' 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/functional/transcriptions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranscriptionsControllerTest < ActionController::TestCase 4 | context "Transcriptions controller" do 5 | setup do 6 | @controller = TranscriptionsController.new 7 | @request = ActionController::TestRequest.new 8 | @response = ActionController::TestResponse.new 9 | end 10 | 11 | context "#new not logged in" do 12 | setup do 13 | @template = Factory :template 14 | @asset_collection = Factory :asset_collection 15 | @asset = Factory :asset, :asset_collection=>@asset_collection 16 | 17 | get :new 18 | end 19 | 20 | should respond_with :redirect 21 | 22 | should "redirect_to '/cas server'" do 23 | assert_redirected_to "https://login.zooniverse.org/login?service=http%3A%2F%2Ftest.host%2Ftranscriptions%2Fnew" 24 | end 25 | end 26 | 27 | context "#new logged in" do 28 | setup do 29 | @template = Factory :template 30 | @asset_collection = Factory :asset_collection 31 | @asset = Factory :asset, :asset_collection=>@asset_collection 32 | 33 | standard_cas_login_without_stub 34 | CASClient::Frameworks::Rails::Filter.stubs(:filter).returns(true) 35 | get :new 36 | end 37 | 38 | should respond_with :success 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/functional/templates_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TemplatesControllerTest < ActionController::TestCase 4 | context "Templates controller" do 5 | setup do 6 | @controller = TemplatesController.new 7 | @request = ActionController::TestRequest.new 8 | @response = ActionController::TestResponse.new 9 | end 10 | 11 | context "#show for an asset" do 12 | setup do 13 | standard_cas_login 14 | @template = Factory :template 15 | @asset = Factory :asset, :template => @template 16 | get :show, { :asset_id => @asset.id, :format => "json" } 17 | end 18 | 19 | should respond_with 200 20 | 21 | should "be have correct template structure" do 22 | template = JSON.parse(@response.body) 23 | %w{default_zoom description name project}.each do |element| 24 | assert_equal @template.send(element), template[element] 25 | end 26 | end 27 | 28 | should "be have correct template entities" do 29 | template = JSON.parse(@response.body) 30 | assert template['entities'].is_a?(Array) 31 | assert_equal template['entities'].length, 1 32 | end 33 | 34 | should "have correct entity structure" do 35 | template = JSON.parse(@response.body) 36 | entity = template['entities'].first 37 | %w{bounds description}.each do |element| 38 | assert_equal @template.entities.first.send(element), entity[element] 39 | end 40 | 41 | assert entity['fields'].is_a?(Array) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Scribe::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /app/models/asset.rb: -------------------------------------------------------------------------------- 1 | # The image being transcribed 2 | class Asset 3 | include MongoMapper::Document 4 | include Randomizer 5 | 6 | # What is the native size of the image 7 | key :height, Integer, :required => true 8 | key :width, Integer, :required => true 9 | 10 | # What size should the image be displayed at 11 | key :display_width, Integer, :required => true 12 | 13 | key :location, String, :required => true 14 | key :ext_ref, String 15 | key :order, Integer 16 | key :template_id, ObjectId 17 | 18 | key :done, Boolean, :default => false 19 | key :classification_count, Integer , :default => 0 20 | 21 | scope :active, :conditions => { :done => false } 22 | scope :in_collection, lambda { |asset_collection| where(:asset_collection_id => asset_collection.id)} 23 | 24 | timestamps! 25 | 26 | belongs_to :template 27 | belongs_to :asset_collection 28 | 29 | many :transcriptions 30 | 31 | # keeping this for if we need a random asset 32 | def self.random_for_transcription 33 | Asset.random(:limit => 1).first 34 | end 35 | 36 | def self.next_unseen_for_user(user) 37 | seen = user.transcriptions.collect{|t| t.asset_id} 38 | Asset.active.where(:id.nin => seen).first 39 | end 40 | 41 | def self.classification_limit 42 | 5 43 | end 44 | 45 | # Don't want the image to be squashed 46 | def display_height 47 | (display_width.to_f / width.to_f) * height 48 | end 49 | 50 | def increment_classification_count 51 | self.classification_count = self.classification_count+1 52 | if self.classification_count > 5 53 | self.done=true 54 | end 55 | self.save 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/views/asset_collections/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | 46 | <% end %> 47 | 48 |
49 |

<%= @collection.title %>

50 | <%- if @collection.author -%> 51 |

by <%=@collection.author %>

52 | <%- end -%> 53 |

page 0 of <%= @collection.assets.count%> pages

54 |
55 |
56 |

prev

57 |

next

58 | 59 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine.yml: -------------------------------------------------------------------------------- 1 | # src_files 2 | # 3 | # Return an array of filepaths relative to src_dir to include before jasmine specs. 4 | # Default: [] 5 | # 6 | # EXAMPLE: 7 | # 8 | # src_files: 9 | # - lib/source1.js 10 | # - lib/source2.js 11 | # - dist/**/*.js 12 | # 13 | src_files: 14 | - public/javascripts/prototype.js 15 | - public/javascripts/effects.js 16 | - public/javascripts/controls.js 17 | - public/javascripts/dragdrop.js 18 | - public/javascripts/application.js 19 | - public/javascripts/**/*.js 20 | 21 | # stylesheets 22 | # 23 | # Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. 24 | # Default: [] 25 | # 26 | # EXAMPLE: 27 | # 28 | # stylesheets: 29 | # - css/style.css 30 | # - stylesheets/*.css 31 | # 32 | stylesheets: 33 | - stylesheets/**/*.css 34 | 35 | # helpers 36 | # 37 | # Return an array of filepaths relative to spec_dir to include before jasmine specs. 38 | # Default: ["helpers/**/*.js"] 39 | # 40 | # EXAMPLE: 41 | # 42 | # helpers: 43 | # - helpers/**/*.js 44 | # 45 | helpers: 46 | - helpers/**/*.js 47 | 48 | # spec_files 49 | # 50 | # Return an array of filepaths relative to spec_dir to include. 51 | # Default: ["**/*[sS]pec.js"] 52 | # 53 | # EXAMPLE: 54 | # 55 | # spec_files: 56 | # - **/*[sS]pec.js 57 | # 58 | spec_files: 59 | - '**/*[sS]pec.js' 60 | 61 | # src_dir 62 | # 63 | # Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. 64 | # Default: project root 65 | # 66 | # EXAMPLE: 67 | # 68 | # src_dir: public 69 | # 70 | src_dir: 71 | 72 | # spec_dir 73 | # 74 | # Spec directory path. Your spec_files must be returned relative to this path. 75 | # Default: spec/javascripts 76 | # 77 | # EXAMPLE: 78 | # 79 | # spec_dir: spec/javascripts 80 | # 81 | spec_dir: spec/javascripts 82 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Scribe::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /public/stylesheets/jquery.annotate.css: -------------------------------------------------------------------------------- 1 | #scribe_transcription_area{ 2 | background-color:white; 3 | border:1px solid #3c3c3c; 4 | } 5 | 6 | 7 | #scribe_top_bar{ 8 | padding:5px; 9 | } 10 | 11 | #scribe_tab_bar{ 12 | height:auto; 13 | width:100%; 14 | list-style:none; 15 | font-size :10pt; 16 | } 17 | 18 | #scribe_tab_bar li{ 19 | float :left; 20 | margin-right:5px ; 21 | background-color:#F4F4F4; 22 | border-top: 2px solid #E0E0E0; 23 | border-left: 2px solid #E0E0E0; 24 | border-right: 2px solid #E0E0E0; 25 | color: #3c3c3c; 26 | text-align:center; 27 | padding:2px; 28 | } 29 | 30 | #scribe_tab_bar li:hover{ 31 | color:black; 32 | cursor:pointer; 33 | } 34 | 35 | .button{ 36 | position: absolute; 37 | right: 10px; 38 | bottom: 33px; 39 | } 40 | 41 | #scribe_transcription_area .ui-draggable-dragging { 42 | -moz-box-shadow: 3px 3px 4px #000; 43 | -webkit-box-shadow: 3px 3px 4px #000; 44 | box-shadow: 3px 3px 4px #000; 45 | } 46 | 47 | .scribe_input_field{ 48 | float:left; 49 | font-size:10pt; 50 | } 51 | 52 | .scribe_selected_tab{ 53 | /* background-color:#FF007E ! important ;*/ 54 | color:black ! important; 55 | border-color: #3c3c3c ! important ; 56 | } 57 | 58 | #scribe_bottom_area{ 59 | padding:5px; 60 | clear:both; 61 | } 62 | 63 | #scribe_zoom_box{ 64 | border-width:2px; 65 | border-style:solid; 66 | border-color:black; 67 | } 68 | 69 | .scribe_marker { 70 | position: absolute; 71 | z-index: 1; 72 | border-style: solid; 73 | border-width: 3px; 74 | border-color: #FF007E; 75 | -moz-box-shadow: 1px 1px 1px #3c3c3c; 76 | -webkit-box-shadow: 1px 1px 1px #3c3c3c; 77 | box-shadow: 1px 1px 1px #3c3c3c; 78 | } 79 | 80 | .scribe_marker a{ 81 | background-color:white; 82 | } 83 | 84 | #scribe_annotation_close_button{ 85 | float :right; 86 | } 87 | 88 | #scribe_annotation_help{ 89 | width:300px; 90 | height:100px; 91 | position:absolute; 92 | right:20px; 93 | background-color:white; 94 | border: 1px solid black; 95 | z-index:-1; 96 | padding:10px; 97 | } 98 | -------------------------------------------------------------------------------- /test/factories.rb: -------------------------------------------------------------------------------- 1 | require 'factory_girl' 2 | 3 | Factory.sequence :name do |n| 4 | "0000000".split('').zip("#{n}".reverse.split('')).reverse.collect{ |a| a[1] || a[0] }.join 5 | end 6 | 7 | Factory.define :asset do |a| 8 | a.height 1200 9 | a.width 600 10 | a.location "http://imageserver.org/assets/1.jpg" 11 | a.ext_ref "ref" 12 | a.display_width 400 13 | end 14 | 15 | Factory.define :template do |t| 16 | t.name { "#{ Factory.next(:name) }" } 17 | t.description "Something interesting" 18 | t.default_zoom 2.0 19 | t.entities { |entity| [entity.association(:entity)] } 20 | end 21 | 22 | Factory.define :zooniverse_user do |z| 23 | z.zooniverse_user_id 1234 24 | z.name "monkey" 25 | z.public_name "Monkey Man" 26 | z.email "monkey@gmail.com" 27 | end 28 | 29 | Factory.define :asset_collection do |ac| 30 | ac.title "music" 31 | ac.author "me" 32 | ac.extern_ref "2" 33 | end 34 | 35 | Factory.define :admin_user, :class => ZooniverseUser do |z| 36 | z.zooniverse_user_id 1234 37 | z.name "monkey" 38 | z.public_name "Monkey Man" 39 | z.email "monkey@gmail.com" 40 | z.admin true 41 | end 42 | 43 | Factory.define :transcription do |t| 44 | t.page_data { } 45 | end 46 | 47 | Factory.define :entity do |e| 48 | e.name { "#{ Factory.next(:name) }" } 49 | e.description "Something useful" 50 | e.help "Something helpful" 51 | e.resizeable true 52 | e.width 200 53 | e.height 50 54 | e.bounds [] 55 | e.zoom 1.5 56 | end 57 | 58 | Factory.define :text_field, :class => Field do |f| 59 | f.name { "#{ Factory.next(:name) }" } 60 | f.field_key "field_name_key" 61 | f.kind "text" 62 | f.initial_value "initial value" 63 | f.options {} 64 | end 65 | 66 | Factory.define :annotation do |a| 67 | a.bounds {} 68 | a.data {} 69 | end 70 | -------------------------------------------------------------------------------- /lib/modules/randomizer.rb: -------------------------------------------------------------------------------- 1 | # Provides random document selection to a model 2 | module Randomizer 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | key :random_number, Float 7 | before_create Proc.new{ |doc| doc.random_number = rand } 8 | end 9 | 10 | module ClassMethods 11 | # Finds sequential random documents. 12 | # When using the :selector option, ensure you're hitting an index that includes :random as the last listed key. 13 | # Additionally, do not use range queries in the selector: {http://bit.ly/gHxnLN MongoDB Indexing Advice}. 14 | # 15 | # @param [Hash] :limit => the number of documents to find, :selector => randomly select documents matching this criteria 16 | # @return [Array] the randomly selected documents 17 | def random(*args) 18 | opts = { :limit => 1, :selector => { } }.update(args.extract_options!) 19 | 20 | number = rand 21 | criteria = where(opts[:selector]).limit(opts[:limit]) 22 | result = criteria.all(:random_number => { :$gte => number }) 23 | 24 | criteria = criteria.limit(opts[:limit] - result.length) 25 | result += criteria.all(:random_number => { :$lte => number }) if result.length < opts[:limit] 26 | result 27 | end 28 | 29 | # Finds non-sequential random documents. 30 | # When using the :selector option, ensure you're hitting an index that includes :random as the last listed key. 31 | # Additionally, do not use range queries in the selector: {http://bit.ly/gHxnLN MongoDB Indexing Advice}. 32 | # 33 | # @param [Hash] :limit => the number of documents to find, :selector => randomly select documents matching the criteria 34 | # @return [Array] the randomly selected documents 35 | def really_random(*args) 36 | opts = { :limit => 1, :selector => { } }.update(args.extract_options!) 37 | 38 | [].tap do |results| 39 | opts[:limit].times do 40 | number = rand 41 | results << where(opts[:selector]).first(:random_number => { :$gte => number }) || 42 | where(opts[:selector]).first(:random_number => { :$lte => number }) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | attr_accessor :current_zooniverse_user 5 | 6 | def cas_logout 7 | CASClient::Frameworks::Rails::Filter.logout(self) 8 | end 9 | helper_method :cas_logout 10 | 11 | def cas_login 12 | "#{CASClient::Frameworks::Rails::Filter.client.login_url}?service=http%3A%2F%2F#{ request.host_with_port }#{ request.fullpath }" 13 | end 14 | 15 | helper_method :cas_login 16 | 17 | def require_privileged_user 18 | unless current_zooniverse_user && current_zooniverse_user.privileged? 19 | flash[:notice] = t 'controllers.application.not_authorised' 20 | redirect_to root_url 21 | return false 22 | end 23 | 24 | true 25 | end 26 | 27 | def zooniverse_user 28 | session[:cas_user] 29 | end 30 | helper_method :zooniverse_user 31 | 32 | def zooniverse_user_id 33 | session[:cas_extra_attributes]['id'] 34 | end 35 | helper_method :zooniverse_user_id 36 | 37 | def zooniverse_user_api_key 38 | session[:cas_extra_attributes]['api_key'] 39 | end 40 | helper_method :zooniverse_user_api_key 41 | 42 | def current_zooniverse_user 43 | @current_zooniverse_user ||= (ZooniverseUser.find_by_zooniverse_user_id(zooniverse_user_id) if zooniverse_user) 44 | end 45 | helper_method :current_zooniverse_user 46 | 47 | def ensure_current_user 48 | forbidden unless current_zooniverse_user && current_zooniverse_user.id.to_s == params[:user_id].to_s 49 | end 50 | 51 | def require_admin_user 52 | redirect_to root_url unless current_zooniverse_user && current_zooniverse_user.is_admin? 53 | end 54 | 55 | def require_api_user 56 | authenticate_or_request_with_http_basic do |username, password| 57 | SiteConfig.api_username == username && SiteConfig.api_password == password 58 | end 59 | end 60 | 61 | def check_or_create_zooniverse_user 62 | if zooniverse_user 63 | z = ZooniverseUser.find_or_create_by_zooniverse_user_id(zooniverse_user_id) 64 | z.update_attributes(:name => zooniverse_user, :api_key => zooniverse_user_api_key) if z.changed? 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/unit/asset_collection_test.rb: -------------------------------------------------------------------------------- 1 | class AssetCollectionTest < ActiveSupport::TestCase 2 | 3 | context "An asset collection" do 4 | setup do 5 | @asset_collection = Factory :asset_collection 6 | @asset1 = Factory :asset , :asset_collection => @asset_collection, :order => 1, :done=>true 7 | @asset2 = Factory :asset , :asset_collection => @asset_collection, :order => 2 8 | @asset3 = Factory :asset , :asset_collection => @asset_collection, :order => 3 9 | end 10 | 11 | should "report the correct front page" do 12 | assert_equal @asset_collection.front_page , @asset1 13 | end 14 | 15 | should "report the correct number of active assets" do 16 | assert_equal @asset_collection.remaining_active , 2 17 | end 18 | 19 | should "report that it is active" do 20 | assert @asset_collection.active? 21 | end 22 | 23 | context "from which a user requests an asset" do 24 | setup do 25 | @user = Factory :zooniverse_user 26 | @transcription1 = Transcription.create(:zooniverse_user=>@user, :asset=>@asset1) 27 | end 28 | 29 | should "return the next unseen asset " do 30 | assert_equal @asset_collection.next_unseen_for_user(@user), @asset2 31 | end 32 | 33 | context "and all the assets are done " do 34 | setup do 35 | [@asset1,@asset2,@asset3].each{|a| a.done=true; a.save} 36 | end 37 | 38 | should "return nil" do 39 | assert_nil @asset_collection.next_unseen_for_user(@user) 40 | end 41 | 42 | should "report that their are no assets" do 43 | assert !@asset_collection.active? 44 | end 45 | 46 | end 47 | 48 | context "and the user has seen all the assets" do 49 | setup do 50 | @transcription2 = Transcription.create(:zooniverse_user=>@user, :asset=>@asset2) 51 | @transcription3 = Transcription.create(:zooniverse_user=>@user, :asset=>@asset3) 52 | end 53 | 54 | should "return nil" do 55 | assert_nil @asset_collection.next_unseen_for_user(@user) 56 | end 57 | end 58 | 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /config/initializers/barista_config.rb: -------------------------------------------------------------------------------- 1 | # Configure barista. 2 | #Barista.configure do |c| 3 | 4 | # Change the root to use app/scripts 5 | # c.root = Rails.root.join("app", "scripts") 6 | 7 | # Change the output root, causing Barista to compile into public/coffeescripts 8 | # c.output_root = Rails.root.join("public", "coffeescripts") 9 | # 10 | # Disable auto compile, use generated file directly: 11 | # c.auto_compile = false 12 | 13 | # Add a new framework: 14 | 15 | # c.register :tests, :root => Rails.root.join('test', 'coffeescript'), :output_prefix => 'test' 16 | 17 | # Disable wrapping in a closure: 18 | # c.bare = true 19 | # ... or ... 20 | # c.bare! 21 | 22 | # Change the output root for a framework: 23 | 24 | # c.change_output_prefix! 'framework-name', 'output-prefix' 25 | 26 | # or for all frameworks... 27 | 28 | # c.each_framework do |framework| 29 | # c.change_output_prefix! framework, "vendor/#{framework.name}" 30 | # end 31 | 32 | # or, prefix the path for the app files: 33 | 34 | # c.change_output_prefix! :default, 'my-app-name' 35 | 36 | # or, change the directory the framework goes into full stop: 37 | 38 | # c.change_output_prefix! :tests, Rails.root.join('spec', 'javascripts') 39 | 40 | # or, hook into the compilation: 41 | 42 | # c.before_compilation { |path| puts "Barista: Compiling #{path}" } 43 | # c.on_compilation { |path| puts "Barista: Successfully compiled #{path}" } 44 | # c.on_compilation_error { |path, output| puts "Barista: Compilation of #{path} failed with:\n#{output}" } 45 | # c.on_compilation_with_warning { |path, output| puts "Barista: Compilation of #{path} had a warning:\n#{output}" } 46 | 47 | # Turn off preambles and exceptions on failure: 48 | 49 | # c.verbose = false 50 | 51 | # Or, make sure it is always on 52 | # c.verbose! 53 | 54 | # If you want to use a custom JS file, you can as well 55 | # e.g. vendoring CoffeeScript in your application: 56 | # c.js_path = Rails.root.join('public', 'javascripts', 'coffee-script.js') 57 | 58 | # Make helpers and the HAML filter output coffee-script instead of the compiled JS. 59 | # Used in combination with the coffeescript_interpreter_js helper in Rails. 60 | # c.embedded_interpreter = true 61 | 62 | #end 63 | -------------------------------------------------------------------------------- /test/unit/asset_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AssetTest < ActiveSupport::TestCase 4 | context "An Asset" do 5 | setup do 6 | @asset = Factory :asset, :width => 100, :height => 200, :display_width => 50 7 | end 8 | 9 | should_associate :template, :transcriptions 10 | should_have_keys :height, :width, :display_width, :location, :template_id, :created_at, :updated_at 11 | 12 | should "Calculate the correct display_height" do 13 | assert_equal @asset.display_height, 100 14 | end 15 | 16 | context "when incrementing classification count" do 17 | setup do 18 | @asset.increment_classification_count 19 | end 20 | 21 | should "increment its classification count" do 22 | assert_equal @asset.classification_count ,1 23 | end 24 | 25 | context "and the asset is almost at the classification limit" do 26 | setup do 27 | @asset.classification_count = Asset.classification_limit 28 | @asset.increment_classification_count 29 | end 30 | 31 | should "set the asset to done" do 32 | assert @asset.done 33 | end 34 | end 35 | end 36 | end 37 | 38 | context "When selecting an asset to show a user" do 39 | setup do 40 | @asset1 = Factory :asset 41 | @asset2 = Factory :asset 42 | @user = Factory :zooniverse_user 43 | @transcription = Transcription.create(:zooniverse_user=>@user, :asset=>@asset1) 44 | end 45 | 46 | should "return an unseen asset" do 47 | assert_equal Asset.next_unseen_for_user(@user), @asset2 48 | end 49 | 50 | context "and all the assets are done" do 51 | setup do 52 | @asset1.done = true 53 | @asset2.done = true 54 | @asset1.save 55 | @asset2.save 56 | end 57 | 58 | should "return nil" do 59 | assert_nil Asset.next_unseen_for_user(@user) 60 | end 61 | end 62 | 63 | context "and the user has seen them all" do 64 | setup do 65 | @transcription2 = Transcription.create(:zooniverse_user=>@user, :asset=>@asset2) 66 | end 67 | 68 | should "return nil" do 69 | assert_nil Asset.next_unseen_for_user(@user) 70 | end 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | require "action_mailer/railtie" 5 | require "active_resource/railtie" 6 | require "rails/test_unit/railtie" 7 | 8 | # If you have a Gemfile, require the gems listed there, including any gems 9 | # you've limited to :test, :development, or :production. 10 | Bundler.require(:default, Rails.env) if defined?(Bundler) 11 | 12 | module Scribe 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | # config.autoload_paths += %W(#{config.root}/extras) 20 | config.autoload_paths << File.join(config.root, "lib/modules") 21 | 22 | # Only load the plugins named here, in the order given (default is alphabetical). 23 | # :all can be used as a placeholder for all plugins not explicitly named. 24 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 25 | 26 | # Activate observers that should always be running. 27 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 28 | 29 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 30 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 31 | # config.time_zone = 'Central Time (US & Canada)' 32 | 33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 35 | # config.i18n.default_locale = :de 36 | 37 | # JavaScript files you want as :defaults (application.js is always included). 38 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 39 | 40 | # Configure the default encoding used in templates for Ruby 1.9. 41 | config.encoding = "utf-8" 42 | 43 | # Configure sensitive parameters which will be filtered from the log file. 44 | config.filter_parameters += [:password] 45 | end 46 | end 47 | 48 | require 'ostruct' 49 | site_settings = OpenStruct.new(YAML.load_file("#{Rails.root}/config/site_settings.yml")) 50 | env_config = site_settings.send(Rails.env) 51 | site_settings.common.update(env_config) unless env_config.nil? 52 | ::SiteConfig = OpenStruct.new(site_settings.common) 53 | 54 | require 'casclient' 55 | require 'casclient/frameworks/rails/filter' 56 | CASClient::Frameworks::Rails::Filter.configure( 57 | :cas_base_url => "https://login.zooniverse.org" 58 | ) 59 | -------------------------------------------------------------------------------- /app/controllers/transcriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class TranscriptionsController < ApplicationController 2 | before_filter CASClient::Frameworks::Rails::Filter, :only => [:new, :index, :edit] 3 | before_filter :check_or_create_zooniverse_user, :only => [:new, :index, :edit] 4 | before_filter :get_or_assign_collection, :get_or_assign_asset, :only => [:new] 5 | after_filter :clear_session, :only =>[ :create ] 6 | 7 | def new 8 | @user = current_zooniverse_user 9 | end 10 | 11 | def show 12 | @transcription = Transcription.find(params[:id]) 13 | end 14 | 15 | def index 16 | @transcriptions = current_zooniverse_user.transcriptions.all 17 | end 18 | 19 | def edit 20 | @transcription = Transcription.find(params[:id]) 21 | @asset = @transcription.asset 22 | @user = current_zooniverse_user 23 | end 24 | 25 | def create 26 | transcription_params = params[:transcription] 27 | page_data = transcription_params[:page_data] 28 | asset = Asset.find(page_data[:asset_id]) 29 | 30 | transcription = Transcription.create( :zooniverse_user => current_zooniverse_user, 31 | :asset => asset, 32 | :page_data => page_data) 33 | 34 | annotations = transcription_params[:annotations] 35 | 36 | transcription.add_annotations_from_json(annotations) 37 | 38 | puts "#{transcription.to_json}" 39 | respond_to do |format| 40 | format.js { render :nothing => true, :status => :created } 41 | end 42 | end 43 | 44 | 45 | def update 46 | transcription = Transcription.find(params[:id]) 47 | logger.error "count not find transcription to update" unless transcription 48 | 49 | transcription_params = params[:transcription] 50 | 51 | transcription.annotations.delete_all 52 | transcription.add_annotations_from_json( transcription_params[:annotations]) 53 | 54 | respond_to do |format| 55 | format.js { render :nothing => true, :status => :created } 56 | end 57 | end 58 | 59 | def get_or_assign_collection 60 | @collection = AssetCollection.find(session[:collection_id]) 61 | unless @collection and @collection.active? 62 | @collection = Asset.next_unseen_for_user(current_zooniverse_user).try(:asset_collection) 63 | if @collection 64 | session[:collection_id] = @collection.id 65 | else 66 | self.clear_session 67 | flash[:notice]= "You have already seen everything" 68 | redirect_to :root 69 | end 70 | end 71 | end 72 | 73 | def get_or_assign_asset 74 | @asset=Asset.find(session[:asset_id]) 75 | #if we have no asset in the session 76 | unless @asset 77 | #try to get a new one from the current collection 78 | @asset = @collection.next_unseen_for_user current_zooniverse_user 79 | session[:asset_id] = @asset.id 80 | end 81 | end 82 | 83 | def clear_session 84 | [:asset_id, :collection_id].each {|a| session[a]=nil} 85 | end 86 | end 87 | 88 | -------------------------------------------------------------------------------- /app/views/transcriptions/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | 76 | 77 | <% end %> 78 | 79 | 80 | 81 |
82 | 83 | 84 |
85 |

Transcriptions:

86 |

Click and drag over the document to add annotations.

87 | 89 | Update → 90 |
91 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | 6 | def cas_login(request) 7 | "#{CASClient::Frameworks::Rails::Filter.client.login_url}?service=http%3A%2F%2F#{ request.host_with_port }#{ request.fullpath }" 8 | end 9 | 10 | module Shoulda 11 | class Context 12 | def should_have_keys(*keys) 13 | klass = described_type 14 | 15 | keys.each do |key| 16 | should "have key #{key}" do 17 | assert klass.key?(key), "#{klass.name} does not have key #{key}" 18 | end 19 | end 20 | end 21 | 22 | def should_associate(*klasses) 23 | klass = described_type 24 | 25 | klasses.each do |other_klass| 26 | should "have associated #{other_klass}" do 27 | assert_contains klass.associations.keys, other_klass.to_s 28 | end 29 | end 30 | end 31 | 32 | def should_include_modules(*modules) 33 | _should_include(*modules) do |klass, mod| 34 | should "include module #{mod}" do 35 | assert klass.include?(mod), "#{klass.name} does not include module #{mod}" 36 | end 37 | end 38 | end 39 | 40 | def should_include_plugins(*plugins) 41 | _should_include(*plugins) do |klass, plugin| 42 | should "include plugin #{plugin}" do 43 | assert klass.plugins.include?(plugin), "#{klass.name} does not include plugin #{plugin}" 44 | end 45 | end 46 | end 47 | 48 | private 49 | def _should_include(*args, &block) 50 | klass = described_type 51 | 52 | args.each do |arg| 53 | arg = arg.to_s.camelize.constantize 54 | yield(klass, arg) 55 | end 56 | end 57 | end 58 | end 59 | 60 | 61 | class ActiveSupport::TestCase 62 | 63 | def teardown 64 | MongoMapper.database.collections.each do |coll| 65 | coll.remove 66 | end 67 | end 68 | 69 | # Make sure that each test case has a teardown 70 | # method to clear the db after each test. 71 | def inherited(base) 72 | base.define_method teardown do 73 | super 74 | end 75 | end 76 | 77 | def standard_cas_login(user = nil) 78 | @user = user ||= Factory(:zooniverse_user) 79 | @request.session[:cas_user] = @user.name 80 | @request.session[:cas_extra_attributes] = {} 81 | @request.session[:cas_extra_attributes]['id'] = @user.zooniverse_user_id 82 | CASClient::Frameworks::Rails::Filter.stubs(:filter).returns(true) 83 | CASClient::Frameworks::Rails::GatewayFilter.stubs(:filter).returns(true) 84 | end 85 | 86 | def standard_cas_login_without_stub(user = nil) 87 | @user = user ||= Factory(:zooniverse_user) 88 | @request.session[:cas_user] = @user.name 89 | @request.session[:cas_extra_attributes] = {} 90 | @request.session[:cas_extra_attributes]['id'] = @user.zooniverse_user_id 91 | end 92 | 93 | def admin_cas_login 94 | @user = Factory :zooniverse_user, :admin => true 95 | standard_cas_login(@user) 96 | end 97 | 98 | def clear_cas 99 | @user = Factory :zooniverse_user 100 | @request.session[:cas_user] = {} 101 | @request.session[:cas_extra_attributes] = {} 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /app/views/transcriptions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | 75 | 76 | <% end %> 77 | 78 | 79 | 80 |
81 | <%if @collection%> 82 |
83 | <%=image_tag @collection.front_page.location, :width=>"200"%> 84 |

Currently transcribing

85 |

<%=@collection.title%>

86 |

Page <%=@asset.order%>

87 |
88 | <%end%> 89 | 90 | 91 |
92 |

Transcriptions:

93 |

Click and drag over the document to add annotations.

94 | 96 | Done → 97 |
98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Scribe 2 | 3 | Scribe is a framework for generating crowd sources transcriptions of image based documents. 4 | It provides a system for generating templates which combined with a magnification tool guide 5 | a user through the process of transcribing an asset (an image). 6 | 7 | ## Getting Started 8 | 9 | - We first need to install a mongodb server. This is then specified in config/mongodb.yml (see config/mongodb.hudson.yml for an example). Since databases are created lazily in MongoDB just specify the database name you want to use there. 10 | 11 | - Site settings (config/site_settings.hudson.yml) contains the application name and other detail about the project. You should rename site_settings.hudson.yml to site_settings.yml 12 | 13 | - To generate the templates for the project look at the lib/tasks/sample_weather_bootstrap.rake file. You need to specify each entity type you wish transcribed and its fields along with help text for the user for each. 14 | 15 | Run: 16 | 17 | `bundle exec rake sample_weather_bootstrap` 18 | 19 | - Run a webserver by typing: 20 | 21 | `bundle exec rails server` 22 | 23 | - profit! 24 | 25 | ## Domain Overview 26 | 27 | There are a number of domain entities in Scribe: 28 | 29 | - Asset 30 | - AssetCollection 31 | - Transcription 32 | - Annotation 33 | - Template 34 | - Field 35 | - Entity 36 | - ZooniverseUser 37 | 38 | ### Asset 39 | 40 | Assets are the objects which you wish to have the user transcribe. They contain a link to the image file to be shown, a desired width to be displayed at and a template_id to be applied to them. The Template that Asset belongs to defines the Fields that can be transcribed. 41 | 42 | Assets can optionally be organised in to asset_collections. These are linear (with the order being determined by asset.order) collections of assets which the user will look through in turn. 43 | 44 | ### AssetCollection 45 | 46 | A simple grouping class that links Assets. This can be used to model a book (e.g. the logs in Old Weather). 47 | 48 | ### Transcription 49 | 50 | These belong to ZooniverseUser and Asset. A Transcription is the result of a user interacting with an Asset. It is composed of many Annotations. 51 | 52 | ### Annotation 53 | 54 | An Annotation belongs to a parent Transcription and has many Entities. The data attribute persists the content of the individual user entry (such as a name, position, date etc.) 55 | 56 | ### Template 57 | 58 | A Template has many Assets and Entities and essentially defines what types (Fields) of records are to be collected from a given image (Asset). 59 | 60 | ### Field 61 | 62 | A Field belongs to an Entity. A Field has a key which is used in the Annotation data hash. The 'kind' defines how the transcription field is rendered in the UI (currently text/select/date are supported). 63 | 64 | ### Entity 65 | 66 | Entity belongs to Template and is composed of many Fields. An Entity might be something like 'position' which would be composed of two Fields: Latitude and Longitude. 67 | 68 | ### ZooniverseUser 69 | 70 | The user producing the Transcriptions. 71 | 72 | ## Classification rules 73 | 74 | Classification rules are set up in asset.rb. The classificaiton_limit method can be altered to change the number of classifications an asset required before it is "done". 75 | 76 | ## Interface 77 | 78 | The main interface element is a JQuery UI plugin annotate.jquery.js . This plugin takes a template in json format, an asset location and display options and will generate transcriptions based on the user interaction. At the end of transcription the results will be posted back as json to the specified end point. More details can be found in (need to write more documents). 79 | 80 | ## API endpoints 81 | 82 | - `/templates/:template_id` returns JSON for a given template 83 | - `/assets/:asset_id` returns JSON for a given asset 84 | - `/transcription/new` will save valid transcriptions which are POSTed to it. -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/jnunemaker/mongomapper.git 3 | revision: b3ab248dd9071a7b45578a5415aab37f5f40e656 4 | branch: rails3 5 | specs: 6 | mongo_mapper (0.8.6) 7 | activemodel (~> 3.0.0) 8 | activesupport (~> 3.0.0) 9 | plucky (~> 0.3.6) 10 | 11 | GEM 12 | remote: http://rubygems.org/ 13 | specs: 14 | ZenTest (4.5.0) 15 | abstract (1.0.0) 16 | actionmailer (3.0.7) 17 | actionpack (= 3.0.7) 18 | mail (~> 2.2.15) 19 | actionpack (3.0.7) 20 | activemodel (= 3.0.7) 21 | activesupport (= 3.0.7) 22 | builder (~> 2.1.2) 23 | erubis (~> 2.6.6) 24 | i18n (~> 0.5.0) 25 | rack (~> 1.2.1) 26 | rack-mount (~> 0.6.14) 27 | rack-test (~> 0.5.7) 28 | tzinfo (~> 0.3.23) 29 | activemodel (3.0.7) 30 | activesupport (= 3.0.7) 31 | builder (~> 2.1.2) 32 | i18n (~> 0.5.0) 33 | activerecord (3.0.7) 34 | activemodel (= 3.0.7) 35 | activesupport (= 3.0.7) 36 | arel (~> 2.0.2) 37 | tzinfo (~> 0.3.23) 38 | activeresource (3.0.7) 39 | activemodel (= 3.0.7) 40 | activesupport (= 3.0.7) 41 | activesupport (3.0.7) 42 | arel (2.0.10) 43 | autotest (4.4.6) 44 | ZenTest (>= 4.4.1) 45 | autotest-rails (4.1.0) 46 | ZenTest 47 | bson (1.3.1) 48 | bson_ext (1.3.1) 49 | builder (2.1.2) 50 | childprocess (0.1.9) 51 | ffi (~> 1.0.6) 52 | configuration (1.3.1) 53 | diff-lcs (1.1.2) 54 | erubis (2.6.6) 55 | abstract (>= 1.0.0) 56 | factory_girl (1.3.3) 57 | factory_girl_rails (1.0.1) 58 | factory_girl (~> 1.3) 59 | railties (>= 3.0.0) 60 | ffi (1.0.9) 61 | heroku (2.3.6) 62 | launchy (>= 0.3.2) 63 | rest-client (~> 1.6.1) 64 | term-ansicolor (~> 1.0.5) 65 | i18n (0.5.0) 66 | jasmine (1.0.2.1) 67 | json_pure (>= 1.4.3) 68 | rack (>= 1.1) 69 | rspec (>= 1.3.1) 70 | selenium-webdriver (>= 0.1.3) 71 | json_pure (1.5.3) 72 | launchy (0.4.0) 73 | configuration (>= 0.0.5) 74 | rake (>= 0.8.1) 75 | mail (2.2.19) 76 | activesupport (>= 2.3.6) 77 | i18n (>= 0.4.0) 78 | mime-types (~> 1.16) 79 | treetop (~> 1.4.8) 80 | mime-types (1.16) 81 | mocha (0.9.12) 82 | mongo (1.3.1) 83 | bson (>= 1.3.1) 84 | nokogiri (1.5.0) 85 | plucky (0.3.8) 86 | mongo (~> 1.3) 87 | polyglot (0.3.1) 88 | rack (1.2.3) 89 | rack-mount (0.6.14) 90 | rack (>= 1.0.0) 91 | rack-test (0.5.7) 92 | rack (>= 1.0) 93 | rails (3.0.7) 94 | actionmailer (= 3.0.7) 95 | actionpack (= 3.0.7) 96 | activerecord (= 3.0.7) 97 | activeresource (= 3.0.7) 98 | activesupport (= 3.0.7) 99 | bundler (~> 1.0) 100 | railties (= 3.0.7) 101 | railties (3.0.7) 102 | actionpack (= 3.0.7) 103 | activesupport (= 3.0.7) 104 | rake (>= 0.8.7) 105 | thor (~> 0.14.4) 106 | rake (0.8.7) 107 | rest-client (1.6.3) 108 | mime-types (>= 1.16) 109 | rspec (2.6.0) 110 | rspec-core (~> 2.6.0) 111 | rspec-expectations (~> 2.6.0) 112 | rspec-mocks (~> 2.6.0) 113 | rspec-core (2.6.4) 114 | rspec-expectations (2.6.0) 115 | diff-lcs (~> 1.1.2) 116 | rspec-mocks (2.6.0) 117 | rubycas-client (2.2.1) 118 | activesupport 119 | rubyzip (0.9.4) 120 | selenium-webdriver (2.1.0) 121 | childprocess (>= 0.1.9) 122 | ffi (>= 1.0.7) 123 | json_pure 124 | rubyzip 125 | shoulda (2.11.3) 126 | term-ansicolor (1.0.5) 127 | thor (0.14.6) 128 | treetop (1.4.9) 129 | polyglot (>= 0.3.1) 130 | tzinfo (0.3.29) 131 | webrat (0.7.3) 132 | nokogiri (>= 1.2.0) 133 | rack (>= 1.0) 134 | rack-test (>= 0.5.3) 135 | 136 | PLATFORMS 137 | ruby 138 | 139 | DEPENDENCIES 140 | autotest 141 | autotest-rails 142 | bson_ext 143 | factory_girl_rails 144 | heroku 145 | jasmine 146 | mocha 147 | mongo_mapper! 148 | rails (= 3.0.7) 149 | rake (= 0.8.7) 150 | rubycas-client (~> 2.2.1) 151 | shoulda 152 | webrat 153 | -------------------------------------------------------------------------------- /app/views/templates/new.html.erb: -------------------------------------------------------------------------------- 1 | <%=content_for :head do%> 2 | 3 | 107 | 125 | <%end%> 126 | 127 | 128 | 129 | 130 |
131 |

Design a new template for this document

132 | <% form_tag do%> 133 |

Template name

134 | <%=text_field_tag "name" %> 135 |

Description

136 | <%=text_area_tag "description"%> 137 |

Default zoom level

138 | <%=text_field_tag "default_zoom"%> 139 | New entity 140 | Done 141 | <%end%> 142 |
-------------------------------------------------------------------------------- /public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unobtrusive scripting adapter for jQuery 3 | * 4 | * Requires jQuery 1.4.3 or later. 5 | * https://github.com/rails/jquery-ujs 6 | */ 7 | 8 | (function($) { 9 | // Triggers an event on an element and returns the event result 10 | function fire(obj, name, data) { 11 | var event = new $.Event(name); 12 | obj.trigger(event, data); 13 | return event.result !== false; 14 | } 15 | 16 | // Submits "remote" forms and links with ajax 17 | function handleRemote(element) { 18 | var method, url, data, 19 | dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType); 20 | 21 | if (element.is('form')) { 22 | method = element.attr('method'); 23 | url = element.attr('action'); 24 | data = element.serializeArray(); 25 | // memoized value from clicked submit button 26 | var button = element.data('ujs:submit-button'); 27 | if (button) { 28 | data.push(button); 29 | element.data('ujs:submit-button', null); 30 | } 31 | } else { 32 | method = element.attr('data-method'); 33 | url = element.attr('href'); 34 | data = null; 35 | } 36 | 37 | $.ajax({ 38 | url: url, type: method || 'GET', data: data, dataType: dataType, 39 | // stopping the "ajax:beforeSend" event will cancel the ajax request 40 | beforeSend: function(xhr, settings) { 41 | if (settings.dataType === undefined) { 42 | xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); 43 | } 44 | return fire(element, 'ajax:beforeSend', [xhr, settings]); 45 | }, 46 | success: function(data, status, xhr) { 47 | element.trigger('ajax:success', [data, status, xhr]); 48 | }, 49 | complete: function(xhr, status) { 50 | element.trigger('ajax:complete', [xhr, status]); 51 | }, 52 | error: function(xhr, status, error) { 53 | element.trigger('ajax:error', [xhr, status, error]); 54 | } 55 | }); 56 | } 57 | 58 | // Handles "data-method" on links such as: 59 | // Delete 60 | function handleMethod(link) { 61 | var href = link.attr('href'), 62 | method = link.attr('data-method'), 63 | csrf_token = $('meta[name=csrf-token]').attr('content'), 64 | csrf_param = $('meta[name=csrf-param]').attr('content'), 65 | form = $('
'), 66 | metadata_input = ''; 67 | 68 | if (csrf_param !== undefined && csrf_token !== undefined) { 69 | metadata_input += ''; 70 | } 71 | 72 | form.hide().append(metadata_input).appendTo('body'); 73 | form.submit(); 74 | } 75 | 76 | function disableFormElements(form) { 77 | form.find('input[data-disable-with]').each(function() { 78 | var input = $(this); 79 | input.data('ujs:enable-with', input.val()) 80 | .val(input.attr('data-disable-with')) 81 | .attr('disabled', 'disabled'); 82 | }); 83 | } 84 | 85 | function enableFormElements(form) { 86 | form.find('input[data-disable-with]').each(function() { 87 | var input = $(this); 88 | input.val(input.data('ujs:enable-with')).removeAttr('disabled'); 89 | }); 90 | } 91 | 92 | function allowAction(element) { 93 | var message = element.attr('data-confirm'); 94 | return !message || (fire(element, 'confirm') && confirm(message)); 95 | } 96 | 97 | function requiredValuesMissing(form) { 98 | var missing = false; 99 | form.find('input[name][required]').each(function() { 100 | if (!$(this).val()) missing = true; 101 | }); 102 | return missing; 103 | } 104 | 105 | $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) { 106 | var link = $(this); 107 | if (!allowAction(link)) return false; 108 | 109 | if (link.attr('data-remote') != undefined) { 110 | handleRemote(link); 111 | return false; 112 | } else if (link.attr('data-method')) { 113 | handleMethod(link); 114 | return false; 115 | } 116 | }); 117 | 118 | $('form').live('submit.rails', function(e) { 119 | var form = $(this), remote = form.attr('data-remote') != undefined; 120 | if (!allowAction(form)) return false; 121 | 122 | // skip other logic when required values are missing 123 | if (requiredValuesMissing(form)) return !remote; 124 | 125 | if (remote) { 126 | handleRemote(form); 127 | return false; 128 | } else { 129 | disableFormElements(form); 130 | } 131 | }); 132 | 133 | $('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() { 134 | var button = $(this); 135 | if (!allowAction(button)) return false; 136 | // register the pressed submit button 137 | var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null; 138 | button.closest('form').data('ujs:submit-button', data); 139 | }); 140 | 141 | $('form').live('ajax:beforeSend.rails', function(event) { 142 | if (this == event.target) disableFormElements($(this)); 143 | }); 144 | 145 | $('form').live('ajax:complete.rails', function(event) { 146 | if (this == event.target) enableFormElements($(this)); 147 | }); 148 | })( jQuery ); -------------------------------------------------------------------------------- /lib/tasks/sample_weather_bootstrap.rake: -------------------------------------------------------------------------------- 1 | task :sample_weather_bootstrap => :environment do 2 | Template.delete_all 3 | Entity.delete_all 4 | Asset.delete_all 5 | 6 | template = Template.create( :name => "My Transcription Template", 7 | :description => "A template for transcribing weather recordds", 8 | :project => "My great project", 9 | :display_width => 600, 10 | :default_zoom => 1.5) 11 | 12 | 13 | 14 | weather_entity = Entity.create( :name => "Weather Observation", 15 | :description => "", 16 | :help => "Please fill in all of the values", 17 | :resizeable => false, 18 | :width => 450, 19 | :height => 80) 20 | 21 | 22 | wind_field = Field.new( :name => "Wind", 23 | :field_key => "wind_direction", 24 | :kind => "select", 25 | :initial_value => "--", 26 | :options => { :select => ['North', 'South', 'East', 'West'] }) 27 | 28 | force_field = Field.new(:name => "Force", 29 | :field_key => "wind_force", 30 | :kind => "text", 31 | :initial_value => "--", 32 | :options => { :text => { :max_length => 2, :min_length => 0 } }) 33 | 34 | air_temperature = Field.new(:name => "Air", 35 | :field_key => "air_temperature", 36 | :kind => "text", 37 | :initial_value => "--", 38 | :options => { :text => { :max_length => 3, :min_length => 0 } }) 39 | 40 | sea_temperature = Field.new(:name => "Air", 41 | :field_key => "sea_temperature", 42 | :kind => "text", 43 | :initial_value => "--", 44 | :options => { :text => { :max_length => 3, :min_length => 0 } }) 45 | 46 | weather_entity.fields << wind_field 47 | weather_entity.fields << force_field 48 | weather_entity.fields << air_temperature 49 | weather_entity.fields << sea_temperature 50 | weather_entity.save 51 | 52 | date_entity = Entity.create(:name => "Date", 53 | :description => "", 54 | :help => "Please fill in the day, month and year", 55 | :resizeable => true, 56 | :width => 450, 57 | :height => 80) 58 | 59 | date_field = Field.new( :name => "Date", 60 | :field_key => "date", 61 | :kind => "date", 62 | :initial_value => "", 63 | :options => {}) 64 | 65 | date_entity.fields << date_field 66 | date_entity.save 67 | 68 | location_entity = Entity.create(:name => "Location", 69 | :description => "", 70 | :help => "Please fill in the latitude and longitude or the port name", 71 | :resizeable => true, 72 | :width => 450, 73 | :height => 80) 74 | 75 | latitude_field = Field.new( :name => "Latitude", 76 | :field_key => "latitude", 77 | :kind => "text", 78 | :initial_value => "--", 79 | :options => {}) 80 | 81 | longitude_field = Field.new(:name => "Longitude", 82 | :field_key => "longitude", 83 | :kind => "text", 84 | :initial_value => "--", 85 | :options => {}) 86 | 87 | location_entity.fields << latitude_field 88 | location_entity.fields << longitude_field 89 | location_entity.save 90 | 91 | template.entities << date_entity 92 | template.entities << location_entity 93 | template.entities << weather_entity 94 | 95 | template.save 96 | 97 | #generate a single asset and a single user for testing just now 98 | voyage = AssetCollection.create(:title => "HMS Attack", :author => "", :extern_ref => "http://en.wikipedia.org/wiki/HMS_Attack_(1911)") 99 | 100 | Asset.create(:location => "/images/1.jpeg", :display_width => 800, :height => 2126, :width => 1388, :template => template, :asset_collection => voyage) 101 | Asset.create(:location => "/images/2.jpeg", :display_width => 800, :height => 2107, :width => 1380, :template => template, :asset_collection => voyage) 102 | 103 | ZooniverseUser.create() 104 | 105 | end -------------------------------------------------------------------------------- /spec/javascripts/helpers/jasmine_jquery-1.2.0.js: -------------------------------------------------------------------------------- 1 | var readFixtures = function() { 2 | return jasmine.getFixtures().proxyCallTo_('read', arguments); 3 | }; 4 | 5 | var loadFixtures = function() { 6 | jasmine.getFixtures().proxyCallTo_('load', arguments); 7 | }; 8 | 9 | var setFixtures = function(html) { 10 | jasmine.getFixtures().set(html); 11 | }; 12 | 13 | var sandbox = function(attributes) { 14 | return jasmine.getFixtures().sandbox(attributes); 15 | }; 16 | 17 | var spyOnEvent = function(selector, eventName) { 18 | jasmine.JQuery.events.spyOn(selector, eventName); 19 | } 20 | 21 | jasmine.getFixtures = function() { 22 | return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures(); 23 | }; 24 | 25 | jasmine.Fixtures = function() { 26 | this.containerId = 'jasmine-fixtures'; 27 | this.fixturesCache_ = {}; 28 | this.fixturesPath = 'spec/javascripts/fixtures'; 29 | }; 30 | 31 | jasmine.Fixtures.prototype.set = function(html) { 32 | this.cleanUp(); 33 | this.createContainer_(html); 34 | }; 35 | 36 | jasmine.Fixtures.prototype.load = function() { 37 | this.cleanUp(); 38 | this.createContainer_(this.read.apply(this, arguments)); 39 | }; 40 | 41 | jasmine.Fixtures.prototype.read = function() { 42 | var htmlChunks = []; 43 | 44 | var fixtureUrls = arguments; 45 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 46 | htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])); 47 | } 48 | 49 | return htmlChunks.join(''); 50 | }; 51 | 52 | jasmine.Fixtures.prototype.clearCache = function() { 53 | this.fixturesCache_ = {}; 54 | }; 55 | 56 | jasmine.Fixtures.prototype.cleanUp = function() { 57 | $('#' + this.containerId).remove(); 58 | }; 59 | 60 | jasmine.Fixtures.prototype.sandbox = function(attributes) { 61 | var attributesToSet = attributes || {}; 62 | return $('
').attr(attributesToSet); 63 | }; 64 | 65 | jasmine.Fixtures.prototype.createContainer_ = function(html) { 66 | var container = $('
'); 67 | container.html(html); 68 | $('body').append(container); 69 | }; 70 | 71 | jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { 72 | if (typeof this.fixturesCache_[url] == 'undefined') { 73 | this.loadFixtureIntoCache_(url); 74 | } 75 | return this.fixturesCache_[url]; 76 | }; 77 | 78 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { 79 | var self = this; 80 | var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl; 81 | $.ajax({ 82 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 83 | cache: false, 84 | dataType: 'html', 85 | url: url, 86 | success: function(data) { 87 | self.fixturesCache_[relativeUrl] = data; 88 | } 89 | }); 90 | }; 91 | 92 | jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { 93 | return this[methodName].apply(this, passedArguments); 94 | }; 95 | 96 | 97 | jasmine.JQuery = function() {}; 98 | 99 | jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { 100 | return $('
').append(html).html(); 101 | }; 102 | 103 | jasmine.JQuery.elementToString = function(element) { 104 | return $('
').append(element.clone()).html(); 105 | }; 106 | 107 | jasmine.JQuery.matchersClass = {}; 108 | 109 | (function(namespace) { 110 | var data = { 111 | spiedEvents: {}, 112 | handlers: [] 113 | }; 114 | 115 | namespace.events = { 116 | spyOn: function(selector, eventName) { 117 | var handler = function(e) { 118 | data.spiedEvents[[selector, eventName]] = e; 119 | }; 120 | $(selector).bind(eventName, handler); 121 | data.handlers.push(handler); 122 | }, 123 | 124 | wasTriggered: function(selector, eventName) { 125 | return !!(data.spiedEvents[[selector, eventName]]); 126 | }, 127 | 128 | cleanUp: function() { 129 | data.spiedEvents = {}; 130 | data.handlers = []; 131 | } 132 | } 133 | })(jasmine.JQuery); 134 | 135 | (function(){ 136 | var jQueryMatchers = { 137 | toHaveClass: function(className) { 138 | return this.actual.hasClass(className); 139 | }, 140 | 141 | toBeVisible: function() { 142 | return this.actual.is(':visible'); 143 | }, 144 | 145 | toBeHidden: function() { 146 | return this.actual.is(':hidden'); 147 | }, 148 | 149 | toBeSelected: function() { 150 | return this.actual.is(':selected'); 151 | }, 152 | 153 | toBeChecked: function() { 154 | return this.actual.is(':checked'); 155 | }, 156 | 157 | toBeEmpty: function() { 158 | return this.actual.is(':empty'); 159 | }, 160 | 161 | toExist: function() { 162 | return this.actual.size() > 0; 163 | }, 164 | 165 | toHaveAttr: function(attributeName, expectedAttributeValue) { 166 | return hasProperty(this.actual.attr(attributeName), expectedAttributeValue); 167 | }, 168 | 169 | toHaveId: function(id) { 170 | return this.actual.attr('id') == id; 171 | }, 172 | 173 | toHaveHtml: function(html) { 174 | return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html); 175 | }, 176 | 177 | toHaveText: function(text) { 178 | if (text && jQuery.isFunction(text.test)) { 179 | return text.test(this.actual.text()); 180 | } else { 181 | return this.actual.text() == text; 182 | } 183 | }, 184 | 185 | toHaveValue: function(value) { 186 | return this.actual.val() == value; 187 | }, 188 | 189 | toHaveData: function(key, expectedValue) { 190 | return hasProperty(this.actual.data(key), expectedValue); 191 | }, 192 | 193 | toBe: function(selector) { 194 | return this.actual.is(selector); 195 | }, 196 | 197 | toContain: function(selector) { 198 | return this.actual.find(selector).size() > 0; 199 | }, 200 | 201 | toBeDisabled: function(selector){ 202 | return this.actual.attr("disabled") == true; 203 | } 204 | }; 205 | 206 | var hasProperty = function(actualValue, expectedValue) { 207 | if (expectedValue === undefined) { 208 | return actualValue !== undefined; 209 | } 210 | return actualValue == expectedValue; 211 | }; 212 | 213 | var bindMatcher = function(methodName) { 214 | var builtInMatcher = jasmine.Matchers.prototype[methodName]; 215 | 216 | jasmine.JQuery.matchersClass[methodName] = function() { 217 | if (this.actual instanceof jQuery) { 218 | var result = jQueryMatchers[methodName].apply(this, arguments); 219 | this.actual = jasmine.JQuery.elementToString(this.actual); 220 | return result; 221 | } 222 | 223 | if (builtInMatcher) { 224 | return builtInMatcher.apply(this, arguments); 225 | } 226 | 227 | return false; 228 | }; 229 | }; 230 | 231 | for(var methodName in jQueryMatchers) { 232 | bindMatcher(methodName); 233 | } 234 | })(); 235 | 236 | beforeEach(function() { 237 | this.addMatchers(jasmine.JQuery.matchersClass); 238 | this.addMatchers({ 239 | toHaveBeenTriggeredOn: function(selector) { 240 | this.message = function() { 241 | return [ 242 | "Expected event " + this.actual + " to have been triggered on" + selector, 243 | "Expected event " + this.actual + " not to have been triggered on" + selector 244 | ]; 245 | }; 246 | return jasmine.JQuery.events.wasTriggered(selector, this.actual); 247 | } 248 | }) 249 | }); 250 | 251 | afterEach(function() { 252 | jasmine.getFixtures().cleanUp(); 253 | jasmine.JQuery.events.cleanUp(); 254 | }); 255 | -------------------------------------------------------------------------------- /public/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | @import url(reset.css); 2 | @import url(fonts.css); 3 | 4 | body{ 5 | width:100%; 6 | height:100%; 7 | margin: 0; 8 | padding: 0; 9 | background: url('../images/bgtexture.jpg'); 10 | background-color: #52708B; 11 | } 12 | 13 | #page{ 14 | width:900px; 15 | margin:25px auto; 16 | -moz-box-shadow: 3px 3px 4px #000; 17 | -webkit-box-shadow: 3px 3px 4px #000; 18 | box-shadow: 3px 3px 4px #000; 19 | } 20 | 21 | #header{ 22 | border-bottom:1px solid gray; 23 | } 24 | 25 | h1,h2,h3,h4,h5,h6 { 26 | font-weight:normal; 27 | font-family:Georgia, serif; 28 | } 29 | 30 | h1{ 31 | font-size:48px; 32 | } 33 | 34 | h2{ 35 | font-size:20px; 36 | } 37 | 38 | h2{ 39 | font-size:18px; 40 | } 41 | 42 | .stats_panel{ 43 | float:left; 44 | padding:5px; 45 | } 46 | 47 | .pad{ 48 | padding-top:30px; 49 | } 50 | 51 | .half_chart{ 52 | margin:10px 0px; 53 | width:440px; 54 | height:250px; 55 | } 56 | 57 | ul#recent_users{ 58 | width:440px; 59 | border: 1px solid #3c3c3c; 60 | } 61 | 62 | ul#recent_users li{ 63 | padding:5px; 64 | } 65 | 66 | ul#recent_users li.odd{ 67 | background:#efefef; 68 | } 69 | 70 | #paginator{ 71 | padding-top:5px; 72 | text-align:center; 73 | } 74 | 75 | /* Front page */ 76 | 77 | #welcome{ 78 | position: absolute; 79 | width: 500px; 80 | height: 300px; 81 | margin: 50% 50%; 82 | top: -150px; 83 | left: -250px; 84 | font-family:Georgia, serif; 85 | text-align: center; 86 | } 87 | 88 | #welcome p, #welcome p a{ 89 | font-size: 24px; 90 | color: #fff; 91 | } 92 | 93 | span.big_header { 94 | color: #fff; 95 | font-size: 90px; 96 | } 97 | 98 | /* Default styles for classification UI */ 99 | 100 | #classify_asset{ 101 | float:left; 102 | padding:5px; 103 | } 104 | 105 | #question_container{ 106 | width:400px; 107 | margin:30px 0px 0px 40px; 108 | float:left; 109 | } 110 | 111 | #question{ 112 | font-size:20px; 113 | background: #FF9; 114 | border:1px solid #2c2c2c; 115 | padding:10px; 116 | } 117 | 118 | 119 | /* flashes */ 120 | 121 | .flash p{ 122 | font-size: 0.9em; 123 | line-height:0.9em; 124 | text-align:center; 125 | padding:10px 10px 10px 10px; 126 | margin:0; 127 | } 128 | 129 | .notice { 130 | background: #cfe7a6; 131 | border: 1px dashed #76b900; 132 | margin:5px; 133 | } 134 | 135 | .warning { 136 | background: #fdf4a6; 137 | border: 1px dashed #fadf00; 138 | margin:5px; 139 | } 140 | 141 | .error { 142 | background: #ffcccc; 143 | border: 1px dashed #d81f2a; 144 | margin:5px; 145 | } 146 | 147 | 148 | 149 | body{ 150 | font-family: helvetica, sans-serif; 151 | } 152 | 153 | #transcription_container img{ 154 | padding:0px; 155 | margin:0px; 156 | border:0px; 157 | cursor:crosshair; 158 | display:inline; 159 | } 160 | 161 | #transcription_container{ 162 | float:left; 163 | position:relative; 164 | } 165 | 166 | .transcription_entity{ 167 | position:absolute; 168 | background:white; 169 | border:1px solid #3c3c3c; 170 | } 171 | 172 | form.new_annotation{ 173 | width:200px; 174 | } 175 | 176 | td.label{ 177 | font-size: 12px; 178 | text-align: right; 179 | } 180 | 181 | form.new_annotation input{ 182 | width: 80px; 183 | } 184 | 185 | #banner { 186 | position: fixed; 187 | background: transparent; 188 | z-index: 99; 189 | margin: 0; 190 | bottom: 10px; 191 | left: 10px; 192 | z-index: 99; 193 | } 194 | 195 | #banner h1{ 196 | font-size:38px; 197 | } 198 | 199 | #banner, #banner a { 200 | color: #FFF; 201 | text-decoration: none; 202 | } 203 | 204 | #banner span.version { 205 | font-size: 14px; 206 | position: relative; 207 | bottom: 0px; 208 | right: -3px; 209 | } 210 | 211 | #banner p{ 212 | font-size: 14px; 213 | position:relative; 214 | bottom:5px; 215 | left:60px; 216 | } 217 | 218 | #banner p.nav{ 219 | float:left; 220 | font-size:12px; 221 | padding-right:3px; 222 | } 223 | 224 | #banner p.user{ 225 | font-size:14px; 226 | top:10px; 227 | } 228 | 229 | #banner p.nav:hover{ 230 | text-decoration:underline; 231 | } 232 | 233 | #controls{ 234 | position:fixed; 235 | top:20px; 236 | left:100%; 237 | margin-left: -340px; 238 | display:block; 239 | background: #EEE; 240 | border:2px solid #2c2c2c; 241 | padding:10px; 242 | z-index: 99; 243 | -moz-box-shadow: 3px 3px 4px #3c3c3c; 244 | -webkit-box-shadow: 3px 3px 4px #3c3c3c; 245 | box-shadow: 3px 3px 4px #3c3c3c; 246 | } 247 | 248 | #controls h3 { 249 | margin-bottom: 5px; 250 | font-size: 18px; 251 | } 252 | 253 | #results{ 254 | width:300px; 255 | } 256 | 257 | @-webkit-keyframes colorfade { 258 | from { 259 | border-color: black; 260 | } 261 | 262 | to { 263 | border-color: #FF007E; 264 | } 265 | } 266 | 267 | #results li{ 268 | background: #DDD; 269 | border: 2px solid black; 270 | margin-bottom: 5px; 271 | padding: 5px; 272 | font-size: 11px; 273 | overflow: hidden; 274 | 275 | } 276 | 277 | #results li.editing_annotation{ 278 | /* border-color:#FF007E;*/ 279 | -webkit-animation-name: colorfade; 280 | -webkit-animation-duration: 2s; 281 | -webkit-animation-iteration-count: infinite; 282 | -webkit-animation-direction: alternate; 283 | -webkit-animation-timing-function: ease-in-out; 284 | } 285 | 286 | #results li div.annotation_item { 287 | float: left; 288 | width: 200px; 289 | 290 | } 291 | 292 | #results li div.annotation_button { 293 | width: 10px; 294 | height: 10px; 295 | background: #555; 296 | float: right; 297 | } 298 | 299 | #scribe_done_button{ 300 | font-size:12px; 301 | padding: 5px 0px 0px 0px; 302 | position: relative; 303 | float: right; 304 | } 305 | #transcription_list{ 306 | margin-top:10px; 307 | width:80%; 308 | margin-left:30%; 309 | margin-right:auto; 310 | color:white; 311 | } 312 | 313 | h1{ 314 | color:white; 315 | } 316 | #transcription_list{ 317 | margin-top:50px; 318 | } 319 | #transcription_list li{ 320 | float:left; 321 | margin-right:20px; 322 | } 323 | #transcription_list a{ 324 | color:white; 325 | 326 | } 327 | 328 | #collections_list{ 329 | margin-top:10px; 330 | width:80%; 331 | margin-left:30%; 332 | margin-right:auto; 333 | color:white; 334 | } 335 | #collections_list li{ 336 | float:left; 337 | margin-right:20px; 338 | margin-top:20px; 339 | width:200px; 340 | } 341 | #collections_list a{ 342 | color:white; 343 | 344 | } 345 | 346 | 347 | .scribe_marker_controls{ 348 | display:none; 349 | right:2px; 350 | bottom:0px; 351 | position:absolute; 352 | } 353 | 354 | .scribe_marker_controls a{ 355 | margin-left:5px; 356 | } 357 | 358 | 359 | .scribe_marker:hover .scribe_marker_controls{ 360 | display:block; 361 | } 362 | 363 | 364 | #project_about_title{ 365 | margin:30px auto; 366 | text-align:center; 367 | width:900px; 368 | } 369 | 370 | #project_about p{ 371 | font-weight:normal; 372 | font-family:Georgia, serif; 373 | color:white ; 374 | font-size:20px; 375 | margin:25px auto; 376 | width:700px; 377 | } 378 | 379 | 380 | 381 | #collection_details{ 382 | width :40%; 383 | margin-right:auto; 384 | margin-left: auto; 385 | color:white; 386 | font-weight:normal; 387 | font-family:Georgia, serif; 388 | text-align:center; 389 | } 390 | 391 | #collection_details #collection_title{ 392 | font-size:20pt; 393 | } 394 | 395 | #collection_page{ 396 | margin-right:auto; 397 | margin-left:auto; 398 | width:40%; 399 | } 400 | 401 | #collection_page p { 402 | color:white; 403 | cursor:pointer; 404 | } 405 | #collection_page #prev_page{ 406 | float:left; 407 | } 408 | 409 | #collection_page #next_page{ 410 | float:right; 411 | } 412 | 413 | #collections_header{ 414 | margin-left:20px; 415 | float:left; 416 | } 417 | 418 | #collection_front_page{ 419 | position:absolute; 420 | bottom:40%; 421 | left:30px; 422 | width:200px; 423 | color :white; 424 | text-align:center; 425 | } 426 | 427 | 428 | -------------------------------------------------------------------------------- /public/javascripts/jquery.imgareaselect.pack.js: -------------------------------------------------------------------------------- 1 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(m($){1l W=2s.4L,D=2s.4K,G=2s.4J,I=2s.4I;m U(){B $("<4H/>")};$.P=m(Y,g){1l Q=$(Y),2C,v=U(),1j=U(),J=U().u(U()).u(U()).u(U()),A=U().u(U()).u(U()).u(U()),F=$([]),1G,H,q,1f,S,O,1k,1e,13=0,1F="1D",2h,2g,2a,29,1R=10,M,1A,1z,2m,2l,15,1J,a,c,l,j,f={a:0,c:0,l:0,j:0,C:0,K:0},2r=T.4G,$p,d,i,o,w,h,2n;m 1n(x){B x+1f.E-1e.E};m 1m(y){B y+1f.q-1e.q};m 1a(x){B x-1f.E+1e.E};m 19(y){B y-1f.q+1e.q};m 1y(3H){B 3H.4F-1e.E};m 1x(3G){B 3G.4E-1e.q};m 14(2Z){1l 1h=2Z||2a,1g=2Z||29;B{a:I(f.a*1h),c:I(f.c*1g),l:I(f.l*1h),j:I(f.j*1g),C:I(f.l*1h)-I(f.a*1h),K:I(f.j*1g)-I(f.c*1g)}};m 2w(a,c,l,j,2Y){1l 1h=2Y||2a,1g=2Y||29;f={a:I(a/1h),c:I(c/1g),l:I(l/1h),j:I(j/1g)};f.C=f.l-f.a;f.K=f.j-f.c};m 1d(){b(!Q.C()){B}1f={E:I(Q.2q().E),q:I(Q.2q().q)};S=Q.C();O=Q.K();1A=g.4D||0;1z=g.4C||0;2m=G(g.4B||1<<24,S);2l=G(g.4A||1<<24,O);b($().4z=="1.3.2"&&1F=="1Y"&&!2r["4y"]){1f.q+=D(T.1r.2o,2r.2o);1f.E+=D(T.1r.2p,2r.2p)}1e=$.4x(1k.r("1p"),["1D","4w"])+1?{E:I(1k.2q().E)-1k.2p(),q:I(1k.2q().q)-1k.2o()}:1F=="1Y"?{E:$(T).2p(),q:$(T).2o()}:{E:0,q:0};H=1n(0);q=1m(0);b(f.l>S||f.j>O){1N()}};m 1O(3D){b(!1J){B}v.r({E:1n(f.a),q:1m(f.c)}).u(1j).C(w=f.C).K(h=f.K);1j.u(J).u(F).r({E:0,q:0});J.C(D(w-J.2X()+J.3B(),0)).K(D(h-J.3F()+J.4v(),0));$(A[0]).r({E:H,q:q,C:f.a,K:O});$(A[1]).r({E:H+f.a,q:q,C:w,K:f.c});$(A[2]).r({E:H+f.l,q:q,C:S-f.l,K:O});$(A[3]).r({E:H+f.a,q:q+f.j,C:w,K:O-f.j});w-=F.2X();h-=F.3F();2L(F.3a){16 8:$(F[4]).r({E:w/2});$(F[5]).r({E:w,q:h/2});$(F[6]).r({E:w/2,q:h});$(F[7]).r({q:h/2});16 4:F.3E(1,3).r({E:w});F.3E(2,4).r({q:h})}b(3D!==Z){b($.P.1W!=2O){$(T).V($.P.1W,$.P.3C)}b(g.1M){$(T)[$.P.1W]($.P.3C=2O)}}b($.1q.1V&&J.2X()-J.3B()==2){J.r("3A",0);3t(m(){J.r("3A","4u")},0)}};m 1Z(3z){1d();1O(3z);a=1n(f.a);c=1m(f.c);l=1n(f.l);j=1m(f.j)};m 22(2W,2t){g.1I?2W.4t(g.1I,2t):2W.1t()};m 1b(2V){1l x=1a(1y(2V))-f.a,y=19(1x(2V))-f.c;b(!2n){1d();2n=12;v.1i("4s",m(){2n=Z})}M="";b(g.2A){b(y<=1R){M="n"}X{b(y>=f.K-1R){M="s"}}b(x<=1R){M+="w"}X{b(x>=f.C-1R){M+="e"}}}v.r("2U",M?M+"-18":g.21?"4r":"");b(1G){1G.4q()}};m 2R(4p){$("1r").r("2U","");b(g.4o||f.C*f.K==0){22(v.u(A),m(){$(N).1t()})}g.2d(Y,14());$(T).V("R",2i);v.R(1b)};m 2z(1Q){b(1Q.3v!=1){B Z}1d();b(M){$("1r").r("2U",M+"-18");a=1n(f[/w/.2k(M)?"l":"a"]);c=1m(f[/n/.2k(M)?"j":"c"]);$(T).R(2i).1i("1P",2R);v.V("R",1b)}X{b(g.21){2h=H+f.a-1y(1Q);2g=q+f.c-1x(1Q);v.V("R",1b);$(T).R(2S).1i("1P",m(){g.2d(Y,14());$(T).V("R",2S);v.R(1b)})}X{Q.1s(1Q)}}B Z};m 1w(3y){b(15){b(3y){l=D(H,G(H+S,a+W(j-c)*15*(l>a||-1)));j=I(D(q,G(q+O,c+W(l-a)/15*(j>c||-1))));l=I(l)}X{j=D(q,G(q+O,c+W(l-a)/15*(j>c||-1)));l=I(D(H,G(H+S,a+W(j-c)*15*(l>a||-1))));j=I(j)}}};m 1N(){a=G(a,H+S);c=G(c,q+O);b(W(l-a)<1A){l=a-1A*(lH+S){a=H+S-1A}}}b(W(j-c)<1z){j=c-1z*(jq+O){c=q+O-1z}}}l=D(H,G(l,H+S));j=D(q,G(j,q+O));1w(W(l-a)2m){l=a-2m*(l2l){j=c-2l*(j=0){F.C(5).K(5)}b(o=g.2H){F.r({2H:o,2E:"3i"})}1K(F,{3j:"2G-23",3h:"2F-23",3k:"1c"})}2a=g.4e/S||1;29=g.4d/O||1;b(L.a!=3m){2w(L.a,L.c,L.l,L.j);L.2D=!L.1t}b(L.1M){g.1M=$.28({27:1,26:"18"},L.1M)}A.25(g.1L+"-4c");1j.25(g.1L+"-4b");3l(i=0;i++<4;){$(J[i-1]).25(g.1L+"-2G"+i)}1K(1j,{4a:"2F-23",49:"1c"});1K(J,{3k:"1c",2H:"2G-C"});1K(A,{48:"2F-23",47:"1c"});b(o=g.3j){$(J[0]).r({2E:"3i",3g:o})}b(o=g.3h){$(J[1]).r({2E:"46",3g:o})}v.3f(1j.u(J).u(F).u(1G));b($.1q.1V){b(o=A.r("3e").3d(/1c=([0-9]+)/)){A.r("1c",o[1]/1U)}b(o=J.r("3e").3d(/1c=([0-9]+)/)){J.r("1c",o[1]/1U)}}b(L.1t){22(v.u(A))}X{b(L.2D&&2C){1J=12;v.u(A).2B(g.1I||0);1Z()}}15=(d=(g.45||"").44(/:/))[0]/d[1];Q.u(A).V("1s",20);b(g.1T||g.1C===Z){v.V("R",1b).V("1s",2z);$(3c).V("18",2y)}X{b(g.1C||g.1T===Z){b(g.2A||g.21){v.R(1b).1s(2z)}$(3c).18(2y)}b(!g.43){Q.u(A).1s(20)}}g.1C=g.1T=1S};N.1o=m(){Q.V("1s",20);v.u(A).1o()};N.42=m(){B g};N.30=2x;N.41=14;N.3Z=2w;N.3Y=1Z;$p=Q;3b($p.3a){13=D(13,!1H($p.r("z-36"))?$p.r("z-36"):13);b($p.r("1p")=="1Y"){1F="1Y"}$p=$p.1X(":35(1r)")}13=g.1E||13;b($.1q.1V){Q.3X("3W","3V")}$.P.1W=$.1q.1V||$.1q.3U?"3T":"3S";b($.1q.3R){1G=U().r({C:"1U%",K:"1U%",1p:"1D",1E:13+2||2})}v.u(A).r({34:"33",1p:1F,3Q:"33",1E:13||"0"});v.r({1E:13+2||2});1j.u(J).r({1p:"1D",32:0});Y.31||Y.3P=="31"||!Q.2v("3O")?2u():Q.1i("3N",2u)};$.2t.P=m(11){11=11||{};N.3M(m(){b($(N).1B("P")){b(11.1o){$(N).1B("P").1o();$(N).3L("P")}X{$(N).1B("P").30(11)}}X{b(!11.1o){b(11.1C===1S&&11.1T===1S){11.1C=12}$(N).1B("P",3K $.P(N,11))}}});b(11.3J){B $(N).1B("P")}B N}})(3I);',62,296,'||||||||||x1|if|y1|||_24|_7|||y2||x2|function||||top|css|||add|_a|||||_d|return|width|_2|left|_e|_3|_10|_4|_c|height|_54|_1d|this|_13|imgAreaSelect|_8|mousemove|_12|document|_5|unbind|_1|else|_6|false||_55|true|_16|_2d|_22|case|_50|resize|_2a|_29|_3a|opacity|_31|_15|_11|sy|sx|one|_b|_14|var|_28|_27|remove|position|browser|body|mousedown|hide|break|_45|_42|evY|evX|_1f|_1e|data|enable|absolute|zIndex|_17|_f|isNaN|fadeSpeed|_23|_51|classPrefix|keys|_32|_33|mouseup|_40|_1c|undefined|disable|100|msie|keyPress|parent|fixed|_36|_4b|movable|_38|color||addClass|ctrl|shift|extend|_1b|_1a|option|altKey|onSelectEnd|onSelectChange|_4c|_19|_18|_3e|_48|test|_21|_20|_26|scrollTop|scrollLeft|offset|_25|Math|fn|_4e|is|_2f|_4f|_4d|_3f|resizable|fadeIn|_9|show|borderStyle|background|border|borderWidth|handles|_53|key|switch|alt|arrows|_35|_4a|_49|_3c|_41|_44|cursor|_3b|_39|outerWidth|_30|_2e|setOptions|complete|fontSize|hidden|visibility|not|index||||length|while|window|match|filter|append|borderColor|borderColor2|solid|borderColor1|borderOpacity|for|null|_52|default|originalEvent|ctrlKey|shiftKey|onInit|setTimeout|onSelectStart|which|_47|_46|_43|_37|margin|innerWidth|onKeyPress|_34|slice|outerHeight|_2c|_2b|jQuery|instance|new|removeData|each|load|img|readyState|overflow|opera|keypress|keydown|safari|on|unselectable|attr|update|setSelection||getSelection|getOptions|persistent|split|aspectRatio|dashed|outerOpacity|outerColor|selectionOpacity|selectionColor|selection|outer|imageHeight|imageWidth|parseInt|handle|corners|in|keyCode|imgareaselect|animated|visible|preventDefault|autoHide|_3d|toggle|move|mouseout|fadeOut|auto|innerHeight|relative|inArray|getBoundingClientRect|jquery|maxHeight|maxWidth|minHeight|minWidth|pageY|pageX|documentElement|div|round|min|max|abs'.split('|'))) 2 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2011, Citizen Science Alliance 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /public/javascripts/jquery.imgareaselect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * imgAreaSelect jQuery plugin 3 | * version 0.9.3 4 | * 5 | * Copyright (c) 2008-2010 Michal Wojciechowski (odyniec.net) 6 | * 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * and GPL (GPL-LICENSE.txt) licenses. 9 | * 10 | * http://odyniec.net/projects/imgareaselect/ 11 | * 12 | */ 13 | 14 | (function($) { 15 | 16 | var abs = Math.abs, 17 | max = Math.max, 18 | min = Math.min, 19 | round = Math.round; 20 | 21 | function div() { 22 | return $('
'); 23 | } 24 | 25 | $.imgAreaSelect = function (img, options) { 26 | var 27 | 28 | $img = $(img), 29 | 30 | imgLoaded, 31 | 32 | $box = div(), 33 | $area = div(), 34 | $border = div().add(div()).add(div()).add(div()), 35 | $outer = div().add(div()).add(div()).add(div()), 36 | $handles = $([]), 37 | 38 | $areaOpera, 39 | 40 | left, top, 41 | 42 | imgOfs, 43 | 44 | imgWidth, imgHeight, 45 | 46 | $parent, 47 | 48 | parOfs, 49 | 50 | zIndex = 0, 51 | 52 | position = 'absolute', 53 | 54 | startX, startY, 55 | 56 | scaleX, scaleY, 57 | 58 | resizeMargin = 10, 59 | 60 | resize, 61 | 62 | minWidth, minHeight, maxWidth, maxHeight, 63 | 64 | aspectRatio, 65 | 66 | shown, 67 | 68 | areaId, 69 | 70 | x1, y1, x2, y2, 71 | 72 | selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 }, 73 | 74 | docElem = document.documentElement, 75 | 76 | $p, d, i, o, w, h, adjusted; 77 | 78 | function viewX(x) { 79 | return x + imgOfs.left - parOfs.left; 80 | } 81 | 82 | function viewY(y) { 83 | return y + imgOfs.top - parOfs.top; 84 | } 85 | 86 | function selX(x) { 87 | return x - imgOfs.left + parOfs.left; 88 | } 89 | 90 | function selY(y) { 91 | return y - imgOfs.top + parOfs.top; 92 | } 93 | 94 | function evX(event) { 95 | return event.pageX - parOfs.left; 96 | } 97 | 98 | function evY(event) { 99 | return event.pageY - parOfs.top; 100 | } 101 | 102 | function getSelection(noScale) { 103 | var sx = noScale || scaleX, sy = noScale || scaleY; 104 | 105 | return { x1: round(selection.x1 * sx), 106 | y1: round(selection.y1 * sy), 107 | x2: round(selection.x2 * sx), 108 | y2: round(selection.y2 * sy), 109 | width: round(selection.x2 * sx) - round(selection.x1 * sx), 110 | height: round(selection.y2 * sy) - round(selection.y1 * sy) }; 111 | } 112 | 113 | function setSelection(x1, y1, x2, y2, noScale) { 114 | var sx = noScale || scaleX, sy = noScale || scaleY; 115 | 116 | selection = { 117 | x1: round(x1 / sx), 118 | y1: round(y1 / sy), 119 | x2: round(x2 / sx), 120 | y2: round(y2 / sy) 121 | }; 122 | 123 | selection.width = selection.x2 - selection.x1; 124 | selection.height = selection.y2 - selection.y1; 125 | } 126 | 127 | function adjust() { 128 | if (!$img.width()) 129 | return; 130 | 131 | imgOfs = { left: round($img.offset().left), top: round($img.offset().top) }; 132 | 133 | imgWidth = $img.width(); 134 | imgHeight = $img.height(); 135 | 136 | minWidth = options.minWidth || 0; 137 | minHeight = options.minHeight || 0; 138 | maxWidth = min(options.maxWidth || 1<<24, imgWidth); 139 | maxHeight = min(options.maxHeight || 1<<24, imgHeight); 140 | 141 | if ($().jquery == '1.3.2' && position == 'fixed' && 142 | !docElem['getBoundingClientRect']) 143 | { 144 | imgOfs.top += max(document.body.scrollTop, docElem.scrollTop); 145 | imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft); 146 | } 147 | 148 | parOfs = $.inArray($parent.css('position'), ['absolute', 'relative']) + 1 ? 149 | { left: round($parent.offset().left) - $parent.scrollLeft(), 150 | top: round($parent.offset().top) - $parent.scrollTop() } : 151 | position == 'fixed' ? 152 | { left: $(document).scrollLeft(), top: $(document).scrollTop() } : 153 | { left: 0, top: 0 }; 154 | 155 | left = viewX(0); 156 | top = viewY(0); 157 | 158 | if (selection.x2 > imgWidth || selection.y2 > imgHeight) 159 | doResize(); 160 | } 161 | 162 | function update(resetKeyPress) { 163 | if (!shown) return; 164 | 165 | $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) }) 166 | .add($area).width(w = selection.width).height(h = selection.height); 167 | 168 | $area.add($border).add($handles).css({ left: 0, top: 0 }); 169 | 170 | $border 171 | .width(max(w - $border.outerWidth() + $border.innerWidth(), 0)) 172 | .height(max(h - $border.outerHeight() + $border.innerHeight(), 0)); 173 | 174 | $($outer[0]).css({ left: left, top: top, 175 | width: selection.x1, height: imgHeight }); 176 | $($outer[1]).css({ left: left + selection.x1, top: top, 177 | width: w, height: selection.y1 }); 178 | $($outer[2]).css({ left: left + selection.x2, top: top, 179 | width: imgWidth - selection.x2, height: imgHeight }); 180 | $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2, 181 | width: w, height: imgHeight - selection.y2 }); 182 | 183 | w -= $handles.outerWidth(); 184 | h -= $handles.outerHeight(); 185 | 186 | switch ($handles.length) { 187 | case 8: 188 | $($handles[4]).css({ left: w / 2 }); 189 | $($handles[5]).css({ left: w, top: h / 2 }); 190 | $($handles[6]).css({ left: w / 2, top: h }); 191 | $($handles[7]).css({ top: h / 2 }); 192 | case 4: 193 | $handles.slice(1,3).css({ left: w }); 194 | $handles.slice(2,4).css({ top: h }); 195 | } 196 | 197 | if (resetKeyPress !== false) { 198 | if ($.imgAreaSelect.keyPress != docKeyPress) 199 | $(document).unbind($.imgAreaSelect.keyPress, 200 | $.imgAreaSelect.onKeyPress); 201 | 202 | if (options.keys) 203 | $(document)[$.imgAreaSelect.keyPress]( 204 | $.imgAreaSelect.onKeyPress = docKeyPress); 205 | } 206 | 207 | if ($.browser.msie && $border.outerWidth() - $border.innerWidth() == 2) { 208 | $border.css('margin', 0); 209 | setTimeout(function () { $border.css('margin', 'auto'); }, 0); 210 | } 211 | } 212 | 213 | function doUpdate(resetKeyPress) { 214 | adjust(); 215 | update(resetKeyPress); 216 | x1 = viewX(selection.x1); y1 = viewY(selection.y1); 217 | x2 = viewX(selection.x2); y2 = viewY(selection.y2); 218 | } 219 | 220 | function hide($elem, fn) { 221 | options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide(); 222 | 223 | } 224 | 225 | function areaMouseMove(event) { 226 | var x = selX(evX(event)) - selection.x1, 227 | y = selY(evY(event)) - selection.y1; 228 | 229 | if (!adjusted) { 230 | adjust(); 231 | adjusted = true; 232 | 233 | $box.one('mouseout', function () { adjusted = false; }); 234 | } 235 | 236 | resize = ''; 237 | 238 | if (options.resizable) { 239 | if (y <= resizeMargin) 240 | resize = 'n'; 241 | else if (y >= selection.height - resizeMargin) 242 | resize = 's'; 243 | if (x <= resizeMargin) 244 | resize += 'w'; 245 | else if (x >= selection.width - resizeMargin) 246 | resize += 'e'; 247 | } 248 | 249 | $box.css('cursor', resize ? resize + '-resize' : 250 | options.movable ? 'move' : ''); 251 | $box.attr('id', options.areaId); 252 | if ($areaOpera) 253 | $areaOpera.toggle(); 254 | } 255 | 256 | function docMouseUp(event) { 257 | $('body').css('cursor', ''); 258 | 259 | if (options.autoHide || selection.width * selection.height == 0) 260 | hide($box.add($outer), function () { $(this).hide(); }); 261 | 262 | options.onSelectEnd(img, getSelection()); 263 | 264 | $(document).unbind('mousemove', selectingMouseMove); 265 | $box.mousemove(areaMouseMove); 266 | } 267 | 268 | function areaMouseDown(event) { 269 | // When we start the drag then we need to hide the transcription box 270 | $('#transcription_entity_'+$box.attr('id')).hide(); 271 | if (event.which != 1) return false; 272 | 273 | adjust(); 274 | 275 | if (resize) { 276 | $('body').css('cursor', resize + '-resize'); 277 | 278 | x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']); 279 | y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']); 280 | 281 | $(document).mousemove(selectingMouseMove) 282 | .one('mouseup', docMouseUp); 283 | $box.unbind('mousemove', areaMouseMove); 284 | } 285 | else if (options.movable) { 286 | startX = left + selection.x1 - evX(event); 287 | startY = top + selection.y1 - evY(event); 288 | 289 | $box.unbind('mousemove', areaMouseMove); 290 | 291 | $(document).mousemove(movingMouseMove) 292 | .one('mouseup', function () { 293 | options.onSelectEnd(img, getSelection()); 294 | // Move the transcription entity and show it 295 | $('#transcription_entity_'+$box.attr('id')).css('background', 'red'); 296 | $('#transcription_entity_'+$box.attr('id')).css('top', selection.y2); 297 | $('#transcription_entity_'+$box.attr('id')).css('left', selection.x1); 298 | $('#transcription_entity_'+$box.attr('id')).css('width', selection.x2-selection.x1 + 'px'); 299 | $('#transcription_entity_'+$box.attr('id')).show(); 300 | $(document).unbind('mousemove', movingMouseMove); 301 | $box.mousemove(areaMouseMove); 302 | }); 303 | } 304 | else 305 | $img.mousedown(event); 306 | 307 | return false; 308 | } 309 | 310 | function fixAspectRatio(xFirst) { 311 | if (aspectRatio) 312 | if (xFirst) { 313 | x2 = max(left, min(left + imgWidth, 314 | x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))); 315 | 316 | y2 = round(max(top, min(top + imgHeight, 317 | y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)))); 318 | x2 = round(x2); 319 | } 320 | else { 321 | y2 = max(top, min(top + imgHeight, 322 | y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))); 323 | x2 = round(max(left, min(left + imgWidth, 324 | x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)))); 325 | y2 = round(y2); 326 | } 327 | } 328 | 329 | function doResize() { 330 | x1 = min(x1, left + imgWidth); 331 | y1 = min(y1, top + imgHeight); 332 | 333 | if (abs(x2 - x1) < minWidth) { 334 | x2 = x1 - minWidth * (x2 < x1 || -1); 335 | 336 | if (x2 < left) 337 | x1 = left + minWidth; 338 | else if (x2 > left + imgWidth) 339 | x1 = left + imgWidth - minWidth; 340 | } 341 | 342 | if (abs(y2 - y1) < minHeight) { 343 | y2 = y1 - minHeight * (y2 < y1 || -1); 344 | 345 | if (y2 < top) 346 | y1 = top + minHeight; 347 | else if (y2 > top + imgHeight) 348 | y1 = top + imgHeight - minHeight; 349 | } 350 | 351 | x2 = max(left, min(x2, left + imgWidth)); 352 | y2 = max(top, min(y2, top + imgHeight)); 353 | 354 | fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio); 355 | 356 | if (abs(x2 - x1) > maxWidth) { 357 | x2 = x1 - maxWidth * (x2 < x1 || -1); 358 | fixAspectRatio(); 359 | } 360 | 361 | if (abs(y2 - y1) > maxHeight) { 362 | y2 = y1 - maxHeight * (y2 < y1 || -1); 363 | fixAspectRatio(true); 364 | } 365 | 366 | selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)), 367 | y1: selY(min(y1, y2)), y2: selY(max(y1, y2)), 368 | width: abs(x2 - x1), height: abs(y2 - y1) }; 369 | 370 | update(); 371 | 372 | options.onSelectChange(img, getSelection()); 373 | } 374 | 375 | function selectingMouseMove(event) { 376 | x2 = resize == '' || /w|e/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2); 377 | y2 = resize == '' || /n|s/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2); 378 | 379 | doResize(); 380 | 381 | return false; 382 | 383 | } 384 | 385 | function doMove(newX1, newY1) { 386 | x2 = (x1 = newX1) + selection.width; 387 | y2 = (y1 = newY1) + selection.height; 388 | 389 | $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2), 390 | y2: selY(y2) }); 391 | 392 | update(); 393 | 394 | options.onSelectChange(img, getSelection()); 395 | } 396 | 397 | function movingMouseMove(event) { 398 | x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width)); 399 | y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height)); 400 | 401 | doMove(x1, y1); 402 | 403 | event.preventDefault(); 404 | 405 | return false; 406 | } 407 | 408 | function startSelection() { 409 | adjust(); 410 | 411 | x2 = x1; 412 | y2 = y1; 413 | 414 | doResize(); 415 | 416 | resize = ''; 417 | 418 | if ($outer.is(':not(:visible)')) 419 | $box.add($outer).hide().fadeIn(options.fadeSpeed||0); 420 | 421 | shown = true; 422 | 423 | $(document).unbind('mouseup', cancelSelection) 424 | .mousemove(selectingMouseMove).one('mouseup', docMouseUp); 425 | $box.unbind('mousemove', areaMouseMove); 426 | 427 | options.onSelectStart(img, getSelection()); 428 | } 429 | 430 | function cancelSelection() { 431 | $(document).unbind('mousemove', startSelection); 432 | hide($box.add($outer)); 433 | 434 | selection = { x1: selX(x1), y1: selY(y1), x2: selX(x1), y2: selY(y1), 435 | width: 0, height: 0 }; 436 | 437 | options.onSelectChange(img, getSelection()); 438 | options.onSelectEnd(img, getSelection()); 439 | } 440 | 441 | function imgMouseDown(event) { 442 | if (event.which != 1 || $outer.is(':animated')) return false; 443 | 444 | adjust(); 445 | startX = x1 = evX(event); 446 | startY = y1 = evY(event); 447 | 448 | $(document).one('mousemove', startSelection) 449 | .one('mouseup', cancelSelection); 450 | 451 | return false; 452 | } 453 | 454 | function windowResize() { 455 | doUpdate(false); 456 | } 457 | 458 | function imgLoad() { 459 | imgLoaded = true; 460 | 461 | setOptions(options = $.extend({ 462 | classPrefix: 'imgareaselect', 463 | movable: true, 464 | resizable: true, 465 | parent: 'body', 466 | onInit: function () {}, 467 | onSelectStart: function () {}, 468 | onSelectChange: function () {}, 469 | onSelectEnd: function () {} 470 | }, options)); 471 | 472 | $box.add($outer).css({ visibility: '' }); 473 | 474 | if (options.show) { 475 | shown = true; 476 | adjust(); 477 | update(); 478 | $box.add($outer).hide().fadeIn(options.fadeSpeed||0); 479 | } 480 | 481 | setTimeout(function () { options.onInit(img, getSelection()); }, 0); 482 | } 483 | 484 | var docKeyPress = function(event) { 485 | var k = options.keys, d, t, key = event.keyCode; 486 | 487 | d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt : 488 | !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl : 489 | !isNaN(k.shift) && event.shiftKey ? k.shift : 490 | !isNaN(k.arrows) ? k.arrows : 10; 491 | 492 | if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) || 493 | (k.ctrl == 'resize' && event.ctrlKey) || 494 | (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey))) 495 | { 496 | switch (key) { 497 | case 37: 498 | d = -d; 499 | case 39: 500 | t = max(x1, x2); 501 | x1 = min(x1, x2); 502 | x2 = max(t + d, x1); 503 | fixAspectRatio(); 504 | break; 505 | case 38: 506 | d = -d; 507 | case 40: 508 | t = max(y1, y2); 509 | y1 = min(y1, y2); 510 | y2 = max(t + d, y1); 511 | fixAspectRatio(true); 512 | break; 513 | default: 514 | return; 515 | } 516 | 517 | doResize(); 518 | } 519 | else { 520 | x1 = min(x1, x2); 521 | y1 = min(y1, y2); 522 | 523 | switch (key) { 524 | case 37: 525 | doMove(max(x1 - d, left), y1); 526 | break; 527 | case 38: 528 | doMove(x1, max(y1 - d, top)); 529 | break; 530 | case 39: 531 | doMove(x1 + min(d, imgWidth - selX(x2)), y1); 532 | break; 533 | case 40: 534 | doMove(x1, y1 + min(d, imgHeight - selY(y2))); 535 | break; 536 | default: 537 | return; 538 | } 539 | } 540 | 541 | return false; 542 | }; 543 | 544 | function styleOptions($elem, props) { 545 | for (option in props) 546 | if (options[option] !== undefined) 547 | $elem.css(props[option], options[option]); 548 | } 549 | 550 | function setOptions(newOptions) { 551 | if (newOptions.parent) 552 | ($parent = $(newOptions.parent)).append($box.add($outer)); 553 | 554 | $.extend(options, newOptions); 555 | 556 | adjust(); 557 | 558 | if (newOptions.handles != null) { 559 | $handles.remove(); 560 | $handles = $([]); 561 | 562 | i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0; 563 | 564 | while (i--) 565 | $handles = $handles.add(div()); 566 | 567 | $handles.addClass(options.classPrefix + '-handle').css({ 568 | position: 'absolute', 569 | fontSize: 0, 570 | zIndex: zIndex + 1 || 1 571 | }); 572 | 573 | if (!parseInt($handles.css('width')) >= 0) 574 | $handles.width(5).height(5); 575 | 576 | if (o = options.borderWidth) 577 | $handles.css({ borderWidth: o, borderStyle: 'solid' }); 578 | 579 | styleOptions($handles, { borderColor1: 'border-color', 580 | borderColor2: 'background-color', 581 | borderOpacity: 'opacity' }); 582 | } 583 | 584 | scaleX = options.imageWidth / imgWidth || 1; 585 | scaleY = options.imageHeight / imgHeight || 1; 586 | 587 | if (newOptions.x1 != null) { 588 | setSelection(newOptions.x1, newOptions.y1, newOptions.x2, 589 | newOptions.y2); 590 | newOptions.show = !newOptions.hide; 591 | } 592 | 593 | if (newOptions.keys) 594 | options.keys = $.extend({ shift: 1, ctrl: 'resize' }, 595 | newOptions.keys); 596 | 597 | $outer.addClass(options.classPrefix + '-outer'); 598 | $area.addClass(options.classPrefix + '-selection'); 599 | for (i = 0; i++ < 4;) 600 | $($border[i-1]).addClass(options.classPrefix + '-border' + i); 601 | 602 | styleOptions($area, { selectionColor: 'background-color', 603 | selectionOpacity: 'opacity' }); 604 | styleOptions($border, { borderOpacity: 'opacity', 605 | borderWidth: 'border-width' }); 606 | styleOptions($outer, { outerColor: 'background-color', 607 | outerOpacity: 'opacity' }); 608 | if (o = options.borderColor1) 609 | $($border[0]).css({ borderStyle: 'solid', borderColor: o }); 610 | if (o = options.borderColor2) 611 | $($border[1]).css({ borderStyle: 'dashed', borderColor: o }); 612 | 613 | $box.append($area.add($border).add($handles).add($areaOpera)); 614 | 615 | if ($.browser.msie) { 616 | if (o = $outer.css('filter').match(/opacity=([0-9]+)/)) 617 | $outer.css('opacity', o[1]/100); 618 | if (o = $border.css('filter').match(/opacity=([0-9]+)/)) 619 | $border.css('opacity', o[1]/100); 620 | } 621 | 622 | if (newOptions.hide) 623 | hide($box.add($outer)); 624 | else if (newOptions.show && imgLoaded) { 625 | shown = true; 626 | $box.add($outer).fadeIn(options.fadeSpeed||0); 627 | doUpdate(); 628 | } 629 | 630 | aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1]; 631 | 632 | $img.add($outer).unbind('mousedown', imgMouseDown); 633 | 634 | if (options.disable || options.enable === false) { 635 | $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown); 636 | $(window).unbind('resize', windowResize); 637 | } 638 | else { 639 | if (options.enable || options.disable === false) { 640 | if (options.resizable || options.movable) 641 | $box.mousemove(areaMouseMove).mousedown(areaMouseDown); 642 | 643 | $(window).resize(windowResize); 644 | } 645 | 646 | if (!options.persistent) 647 | $img.add($outer).mousedown(imgMouseDown); 648 | } 649 | 650 | options.enable = options.disable = undefined; 651 | } 652 | 653 | this.remove = function () { 654 | $img.unbind('mousedown', imgMouseDown); 655 | $box.add($outer).remove(); 656 | }; 657 | 658 | this.getOptions = function () { return options; }; 659 | 660 | this.setOptions = setOptions; 661 | 662 | this.getSelection = getSelection; 663 | 664 | this.setSelection = setSelection; 665 | 666 | this.update = doUpdate; 667 | 668 | $p = $img; 669 | 670 | while ($p.length) { 671 | zIndex = max(zIndex, 672 | !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex); 673 | if ($p.css('position') == 'fixed') 674 | position = 'fixed'; 675 | 676 | $p = $p.parent(':not(body)'); 677 | } 678 | 679 | zIndex = options.zIndex || zIndex; 680 | 681 | if ($.browser.msie) 682 | $img.attr('unselectable', 'on'); 683 | 684 | $.imgAreaSelect.keyPress = $.browser.msie || 685 | $.browser.safari ? 'keydown' : 'keypress'; 686 | 687 | if ($.browser.opera) 688 | $areaOpera = div().css({ width: '100%', height: '100%', 689 | position: 'absolute', zIndex: zIndex + 2 || 2 }); 690 | 691 | $box.add($outer).css({ visibility: 'hidden', position: position, 692 | overflow: 'hidden', zIndex: zIndex || '0' }); 693 | $box.css({ zIndex: zIndex + 2 || 2 }); 694 | $area.add($border).css({ position: 'absolute', fontSize: 0 }); 695 | 696 | img.complete || img.readyState == 'complete' || !$img.is('img') ? 697 | imgLoad() : $img.one('load', imgLoad); 698 | }; 699 | 700 | $.fn.imgAreaSelect = function (options) { 701 | options = options || {}; 702 | 703 | this.each(function () { 704 | if ($(this).data('imgAreaSelect')) { 705 | if (options.remove) { 706 | $(this).data('imgAreaSelect').remove(); 707 | $(this).removeData('imgAreaSelect'); 708 | } 709 | else 710 | $(this).data('imgAreaSelect').setOptions(options); 711 | } 712 | else if (!options.remove) { 713 | if (options.enable === undefined && options.disable === undefined) 714 | options.enable = true; 715 | 716 | $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options)); 717 | } 718 | }); 719 | 720 | if (options.instance) 721 | return $(this).data('imgAreaSelect'); 722 | 723 | return this; 724 | }; 725 | 726 | })(jQuery); 727 | -------------------------------------------------------------------------------- /public/javascripts/jquery.annotate.js: -------------------------------------------------------------------------------- 1 | $.widget("ui.annotate", { 2 | options: {'zoomLevel' : 1, 3 | 'assetScreenWidth' : 600, 4 | 'assetScreenHeight' : 900, 5 | 'annotationBoxWidth' : 500, 6 | 'annotationBoxHeight' : 100, 7 | 'zoomBoxWidth' : 500, 8 | 'zoomBoxHeight' : 200, 9 | 'markerIcon' : '/images/annotationMarker.png', 10 | zoomLevel : 1, 11 | onSubmitedPassed : null, 12 | onSubmitedFailed : null, 13 | onAnnotationAdded : null, 14 | onAnnotationRemoved : null, 15 | onAnnotationUpdated: null, 16 | onAnnotationEditedStarted : null, 17 | showHelp : false, 18 | initalEntity : null, 19 | annotationBox : null, 20 | image : null, 21 | page_data : {}, 22 | annotations : {}, 23 | annIdCounter : 0, 24 | editing_id : null, 25 | update : false, 26 | authenticity_token : null, 27 | orientation : "floatAbove", 28 | helpShowing : false 29 | }, 30 | _create: function() { 31 | var self= this; 32 | if (this.options.initalEntity==null){ 33 | this.options.initalEntity = this.options.template.entities[0].name.replace(/ /,"_"); 34 | } 35 | 36 | 37 | 38 | this.element.css("width",this.options.assetScreenWidth) 39 | .css("height",this.options.assetScreenHeight) 40 | .css("position","relative"); 41 | var image= $("").attr("id","scribe_main_image") 42 | .attr("src",this.options.imageURL) 43 | .css("width",this.options.assetScreenWidth) 44 | .css("height",this.options.assetScreenHeight) 45 | .css("position","relative") 46 | .css("margin", "0px auto") 47 | .css("left","0px") 48 | .css("top","0px"); 49 | 50 | 51 | image.imgAreaSelect({ 52 | handles: false, 53 | autoHide : true, 54 | onSelectEnd: function render_options(img, box){ 55 | if (self.options.annotationBox==null){ 56 | var midX=(box.x1+box.x2)/2.0; 57 | var midY=(box.y1+box.y2)/2.0; 58 | //console.log("showing box from select"); 59 | //console.log({x:midX,y:midY, width:box.width,height:box.height}); 60 | 61 | self.showBox({x:midX,y:midY, width:box.width,height:box.height}); 62 | } 63 | } 64 | }); 65 | 66 | this.options.image=image; 67 | 68 | this.element.append(image); 69 | 70 | if(this.options.doneButton && this.options.submitURL){ 71 | this.options.doneButton.click(jQuery.proxy(function(event){ 72 | event.preventDefault(); 73 | this.submitResults(this.options.submitURL); 74 | },this)) 75 | } 76 | 77 | 78 | this.options.page_data["asset_screen_width"] = this.options.assetScreenWidth; 79 | this.options.page_data["asset_screen_height"] = this.options.assetScreenHeight; 80 | this.options.page_data["asset_width"] = this.options.assetWidth; 81 | this.options.page_data["asset_height"] = this.options.assetHeight; 82 | this.options.page_data["zooniverse_user_id"] = this.options.userID; 83 | 84 | 85 | this.options.xZoom = this.options.assetWidth/this.options.assetScreenWidth; 86 | this.options.yZoom = this.options.assetHeight/this.options.assetScreenHeight; 87 | 88 | this._setUpAnnotations(); 89 | 90 | this.element.click(function(event){ 91 | //console.log(event); 92 | if(self.options.annotationBox==null){ 93 | self.showBox({x:event.offsetX,y:event.offsetY}); 94 | } 95 | }); 96 | }, 97 | _entity_name_for_id : function(id){ 98 | for(var i in this.options.template.entities ){ 99 | //console.log("testing "+this.options.template.entities[i].id+" and "+id); 100 | if (this.options.template.entities[i].id==id){ 101 | return this.options.template.entities[i].name.replace(/ /,"_"); 102 | } 103 | } 104 | return nil; 105 | }, 106 | _setUpAnnotations : function(){ 107 | //console.log("setting up annotations"); 108 | this.options.annIdCounter = this.options.annotations.length; 109 | for(var id in this.options.annotations){ 110 | //console.log("annotations "+id); 111 | //console.log(this.options.annotations[id].bounds); 112 | var bounds = this.options.annotations[id].bounds; 113 | //console.log(this._denormaliseBounds(bounds)); 114 | 115 | this.options.annotations[id].kind=this._entity_name_for_id(this.options.annotations[id].entity_id); 116 | 117 | this._generateMarker(this._denormaliseBounds(bounds), id); 118 | if (this.options.onAnnotationAdded!=null){ 119 | this.options.onAnnotationAdded.call(this, {annotation_id:id, data:this.options.annotations[id]}); 120 | } 121 | } 122 | }, 123 | 124 | _normaliseBounds : function(bounds){ 125 | var zoomLevel = this.options.zoomLevel; 126 | var normalized_bounds = {width: bounds.width/this.options.assetScreenWidth, 127 | height: bounds.height/this.options.assetScreenHeight, 128 | x : bounds.x/this.options.assetScreenWidth, 129 | y : bounds.y/this.options.assetScreenHeight, 130 | zoom_level:zoomLevel }; 131 | return normalized_bounds 132 | }, 133 | _denormaliseBounds : function(bounds){ 134 | var denorm = {width : bounds.width*this.options.assetScreenWidth, 135 | height : bounds.height*this.options.assetScreenHeight, 136 | x : bounds.x*this.options.assetScreenWidth, 137 | y : bounds.y*this.options.assetScreenHeight, 138 | zoom_level : bounds.zoomLevel} 139 | return denorm; 140 | }, 141 | showBox : function(position) { 142 | //console.log("showing box at"+position); 143 | 144 | this.options.annotationBox = $(this._generateAnnotationBox()); 145 | this.element.append(this.options.annotationBox); 146 | this.element.imgAreaSelect({disable:true}); 147 | if(position){ 148 | if(position.width && position.height){ 149 | var zoomLevel = this.options.zoomLevel; 150 | this.options.zoomBoxWidth= position.width*zoomLevel; 151 | this.options.zoomBoxHeight=position.height*zoomLevel; 152 | 153 | this.options.zoomBox.css("width",position.width*zoomLevel) 154 | .css("height",position.height*zoomLevel) 155 | .css("top", this.options.annotationBoxHeight+1) 156 | .css("left",this.options.annotationBoxWidth/2.0-this.options.zoomBoxWidth/2.0); 157 | 158 | } 159 | var xOffset = $(this.options.annotationBox).width()/2.0; 160 | var yOffset = $(this.options.annotationBox).height()+($(this.options.zoomBox).height())/2.0; 161 | var screenX = position.x-xOffset; 162 | var screenY = position.y-yOffset; 163 | this.options.annotationBox.css("left",position.x-xOffset); 164 | this.options.annotationBox.css("top",position.y-yOffset); 165 | this.options.annotationBox.css("position","absolute"); 166 | var zoomX = -1*(position.x*this.options.zoomLevel-this.options.zoomBoxWidth/2.0); 167 | var zoomY = -1*(position.y*this.options.zoomLevel-this.options.zoomBoxHeight/2.0); 168 | 169 | if(position.y> this.options.assetScreenHeight/2){ 170 | this.options.orientation="floatAbove"; 171 | $("#scribe_transcription_area").css("top",0); 172 | } 173 | else{ 174 | this.options.orientation="floatUnder"; 175 | $("#scribe_transcription_area").css("top",(this.options.zoomBoxHeight+this.options.annotationBoxHeight)); 176 | } 177 | 178 | 179 | $(this.options.zoomBox).find("img").css("top", zoomY ) 180 | .css("left", zoomX); 181 | this._selectEntity(this.options.initalEntity); 182 | 183 | 184 | } 185 | }, 186 | showBoxWithAnnotation : function(annotation) { 187 | //console.log("annotation for showing"); 188 | //console.log(annotation); 189 | //console.log("bounds for showing"); 190 | //console.log(annotation.bounds) 191 | zoom = this.options.zoomLevel; 192 | 193 | var bounds = this._denormaliseBounds(annotation.bounds); 194 | 195 | bounds = {x: bounds.x+bounds.width/2, 196 | y: bounds.y+bounds.height/2, 197 | width: bounds.width , 198 | height: bounds.height} 199 | //console.log(bounds); 200 | 201 | 202 | this.showBox(bounds); 203 | this._selectEntity(annotation.kind); 204 | //console.log(annotation.data); 205 | $("div.scribe_current_inputs input, div.scribe_current_inputs select").each(function(index,element){ 206 | var ell_id=$(element).attr("id").replace("scribe_field_",""); 207 | $(element).val(annotation.data[ell_id]); 208 | }); 209 | 210 | }, 211 | hideBox : function() { this._annatationBox.remove();}, 212 | getAnnotations : function() { return this.options.annotations}, 213 | setMarkerIcon : function(icon){}, 214 | submitResults : function(url){ 215 | var finalAnnotations=[]; 216 | for(var id in this.options.annotations){ 217 | if (this.options.annotations[id]!=null){ 218 | finalAnnotations.push(this.options.annotations[id]); 219 | } 220 | } 221 | this._trigger('resultsSubmited',{},this.options.annotations); 222 | type= this.options.update ? "PUT" : "POST" 223 | $.ajax({ 224 | url: url, 225 | data: {"transcription" :{"annotations" : finalAnnotations, "page_data": this.options.page_data}, "authenticity_token": this.options.authenticity_token}, 226 | type :type, 227 | success: jQuery.proxy(this._postAnnotationsSucceded, this), 228 | error: jQuery.proxy(this._postAnnotationsFailed, this) 229 | }); 230 | }, 231 | _postAnnotationsSucceded: function (){ 232 | if (this.options.onSubmitedPassed){ 233 | this.options.onSubmitedPassed.apply(this); 234 | } 235 | }, 236 | _postAnnotationsFailed : function (){ 237 | if (this.options.onSubmitedFailed){ 238 | this.options.onSubmitedFailed.apply(this); 239 | } 240 | }, 241 | _addAnnotation : function (event){ 242 | this.element.imgAreaSelect({disable:false}); 243 | 244 | event.preventDefault(); 245 | event.stopPropagation(); 246 | 247 | var image = $(this.options.zoomBox).find("img"); 248 | var zoomBox = $(this.options.zoomBox); 249 | var zoomLevel = this.options.zoomLevel; 250 | var location = {width : zoomBox.css("width").replace(/px/,'')/zoomLevel, 251 | height: zoomBox.css("height").replace(/px/,'')/zoomLevel, 252 | y : -1*image.css("top").replace(/px/,'')/zoomLevel, 253 | x : -1*image.css("left").replace(/px/,'')/zoomLevel, 254 | zoom_level: zoomLevel}; 255 | 256 | var annotation_data=this._serializeCurrentForm(); 257 | 258 | 259 | 260 | annotation_data["bounds"]= this._normaliseBounds(location); 261 | 262 | 263 | // this._trigger('annotationAdded', {annotation:annotation_data }); 264 | 265 | if (this.options.editing_id!=null){ 266 | $("#scribe_marker"+this.options.editing_id).remove(); 267 | 268 | this._generateMarker(location, this.options.editing_id); 269 | this.options.annotations[this.options.editing_id]=annotation_data; 270 | if (this.options.onAnnotationUpdated!=null){ 271 | this.options.onAnnotationUpdated.call(this, {annotation_id:this.options.editing_id, data:annotation_data}); 272 | } 273 | this.options.editing_id=null; 274 | } 275 | else{ 276 | 277 | this.options.annotations[this.options.annIdCounter]=annotation_data; 278 | this._generateMarker(location, this.options.annIdCounter); 279 | 280 | if (this.options.onAnnotationAdded!=null){ 281 | this.options.onAnnotationAdded.call(this, {annotation_id:this.options.annIdCounter, data:annotation_data}); 282 | } 283 | this.options.annIdCounter++; 284 | } 285 | 286 | this.options.annotationBox.remove(); 287 | this.options.annotationBox=null; 288 | }, 289 | _serializeCurrentForm : function(){ 290 | var targetInputs =$(".scribe_current_inputs input, .scribe_current_inputs select"); 291 | var parent = $(targetInputs[0]).parent().parent(); 292 | var annotationType = parent.attr("id").replace("scribe_input_","").replace(/_/," "); 293 | 294 | var result = {kind:annotationType, data:{}}; 295 | targetInputs.each(function(){ 296 | var fieldName= $(this).attr("id").replace("scribe_field_",""); 297 | result.data[fieldName]=$(this).val(); 298 | }); 299 | return result ; 300 | 301 | }, 302 | _generateMarker : function (position,marker_id){ 303 | var self=this; 304 | var marker = $("
").attr("id","scribe_marker"+marker_id) 305 | .attr("class","scribe_marker") 306 | .css("width",position.width) 307 | .css("height",position.height) 308 | .css("top", position.y) 309 | .css("left", position.x); 310 | marker.append($("

"+marker_id+"

")); 311 | var controls = $("
"); 312 | 313 | controls.append($("edit").click(function(event){ 314 | event.stopPropagation(); 315 | self._editAnnotation(marker_id); 316 | })); 317 | controls.append($("delete").click(function(event){ 318 | //console.log("running delete"); 319 | event.stopPropagation(); 320 | self._deleteAnnotation(marker_id); 321 | })); 322 | marker.append(controls); 323 | this.element.append(marker); 324 | }, 325 | 326 | _deleteAnnotation : function (annotation_id){ 327 | $("#scribe_marker"+annotation_id).remove(); 328 | this.options.annotations[annotation_id]=null; 329 | this._trigger('anotationDeleted',{},"message deleting"+annotation_id) 330 | if(this.options.onAnnotationRemoved!=null){ 331 | this.options.onAnnotationRemoved.call(this, annotation_id); 332 | } 333 | 334 | }, 335 | _editAnnotation : function (annotation_id){ 336 | var annotation = this.options.annotations[annotation_id]; 337 | this.options.editing_id=annotation_id; 338 | 339 | this._trigger('anotationEdited',{},"message editing"+annotation_id) 340 | if(this.options.onAnnotationEditedStarted!=null){ 341 | this.options.onAnnotationEditedStarted.call(this,{annotation_id:annotation_id, data:annotation}); 342 | } 343 | this.showBoxWithAnnotation(annotation); 344 | }, 345 | _generateField : function (field){ 346 | var inputDiv= $("
"); 347 | var label = $("

"+field.name+"

"); 348 | inputDiv.append(label) 349 | switch(field.kind){ 350 | case("text"): 351 | result=$(""); 352 | result.attr("kind",'text') 353 | .attr("id","scribe_field_"+field.field_key); 354 | if (field.options.text){ 355 | if(field.options.text.max_length){ 356 | result.attr("size",field.options.text.max_length); 357 | } 358 | } 359 | break; 360 | case("select"): 361 | var result = $(""); 371 | result.attr("kind","text") 372 | .attr('id', field.field_key); 373 | result.datepicker({ 374 | changeMonth: true, 375 | changeYear: true 376 | }); 377 | break; 378 | } 379 | return inputDiv.append(result); 380 | }, 381 | _selectEntity : function(entityName){ 382 | $("#scribe_tab_bar li").removeClass("scribe_selected_tab"); 383 | $("#scribe_tab_bar #scribe_tab_"+entityName).addClass("scribe_selected_tab"); 384 | $(".scribe_annotation_input").hide(); 385 | $("#scribe_input_"+entityName).show(); 386 | $(".scribe_current_inputs").removeClass("scribe_current_inputs"); 387 | $("#scribe_input_"+entityName+" .scribe_input_field").addClass("scribe_current_inputs"); 388 | $(".scribe_input_field").show(); 389 | $(".scribe_input_field").filter(".scribe_current_inputs"); 390 | this.changeHelp(entityName); 391 | }, 392 | _switchEntityType : function (event){ 393 | this._selectEntity(event.data); 394 | }, 395 | 396 | _updateWithDrag : function(position){ 397 | var x = position.left+ this.options.annotationBoxWidth/2; 398 | var y = position.top + this.options.annotationBoxHeight+ this.options.zoomBoxHeight/2.0; 399 | 400 | this._checkAndSwitchOrientation({x:x,y:y}); 401 | var zoomX = -1*(x*this.options.zoomLevel-this.options.zoomBoxWidth/2.0); 402 | var zoomY = -1*(y*this.options.zoomLevel-this.options.zoomBoxHeight/2.0); 403 | 404 | $(this.options.zoomBox).find("img").css("top", zoomY ) 405 | .css("left", zoomX); }, 406 | _checkAndSwitchOrientation : function(position){ 407 | if (this.options.orientation == "floatUnder" && position.y> this.options.assetScreenHeight/2){ 408 | this.options.orientation="floatAbove"; 409 | $("#scribe_transcription_area").animate({"top":"-="+(this.options.zoomBoxHeight +this.options.annotationBoxHeight )},500); 410 | if(this.options.helpShowing){ 411 | this.hideHelp(); 412 | this.showHelp(); 413 | } 414 | } 415 | 416 | if (this.options.orientation == "floatAbove" && position.y< this.options.assetScreenHeight/2){ 417 | this.options.orientation="floatUnder"; 418 | $("#scribe_transcription_area").animate({"top":"+="+(this.options.zoomBoxHeight+this.options.annotationBoxHeight)},500); 419 | if(this.options.helpShowing){ 420 | this.hideHelp(); 421 | this.showHelp(); 422 | } 423 | } 424 | 425 | } , 426 | _generateAnnotationBox : function(){ 427 | var self=this; 428 | var image = $(this.options.image); 429 | var imageLoc = image.offset(); 430 | //console.log(this.options); 431 | var totalHeight = this.options.zoomBoxHeight/2+ this.options.annotationBoxHeight; 432 | var containment = [imageLoc.left-this.options.annotationBoxWidth/2, imageLoc.top-totalHeight, imageLoc.left+image.width()-this.options.annotationBoxWidth/2, imageLoc.top+image.height()-totalHeight ]; 433 | //console.log(containment); 434 | var annotationBox = $("
").draggable(this,{ containment: containment , drag: function(event,ui){ 435 | self._updateWithDrag(ui.position); 436 | }}); 437 | 438 | annotationBox.css("cursor","move"); 439 | 440 | var topBar = $("
"); 441 | var tabBar = this._generateTabBar(this.options.template.entities); 442 | var help = this._generateHelp(this.options.template.entities); 443 | 444 | topBar.append(tabBar); 445 | topBar.append(help); 446 | 447 | var helpButton = $("show help").css("opactiy","0"); 448 | var closeButton = $("close"); 449 | 450 | closeButton.click(function(event){ 451 | event.stopPropagation(); 452 | self._dismissAnnotationBox(); 453 | }); 454 | 455 | 456 | helpButton.toggle( jQuery.proxy(this.showHelp, this), jQuery.proxy(this.hideHelp, this)); 457 | 458 | topBar.append(helpButton); 459 | topBar.append(closeButton); 460 | var bottomArea = $("
"); 461 | var inputBar = this._generateInputs(this.options.template.entities); 462 | bottomArea.append(inputBar); 463 | bottomArea.append($("").addClass("button").click(function(e){ self._addAnnotation(e) } )); 464 | 465 | var transcriptionArea= $("
").css("position","absolute").css("top",0); 466 | transcriptionArea.css("width",this.options.annotationBoxWidth+"px") 467 | .css("height",this.options.annotationBoxHeight+"px"); 468 | annotationBox .css("width",this.options.annotationBoxWidth+"px") 469 | .css("height",this.options.annotationBoxHeight+"px"); 470 | transcriptionArea.append(topBar); 471 | transcriptionArea.append(bottomArea); 472 | annotationBox.append(transcriptionArea); 473 | 474 | this.options.zoomBox=this._generateZoomBox(); 475 | annotationBox.append(this.options.zoomBox); 476 | annotationBox.css("z-index","2"); 477 | return annotationBox; 478 | 479 | }, 480 | _dismissAnnotationBox : function(){ 481 | if (this.options.editing_id!=null){ 482 | var annotation_data=this.options.annotations[this.options.editing_id]; 483 | if (this.options.onAnnotationUpdated!=null){ 484 | this.options.onAnnotationUpdated.call(this, {annotation_id:this.options.editing_id, data:annotation_data}); 485 | } 486 | this.options.editing_id=null; 487 | } 488 | this.options.annotationBox.remove(); 489 | this.options.annotationBox=null; 490 | }, 491 | _generateZoomBox : function(){ 492 | var imageWidth = this.options.assetScreenWidth*this.options.zoomLevel; 493 | var imageHeight = this.options.assetScreenHeight*this.options.zoomLevel; 494 | var image = $("").attr("src", this.options.imageURL) 495 | .css("width",imageWidth) 496 | .css('height',imageHeight) 497 | .css('position','absolute') 498 | .css('top',0) 499 | .css('left',0); 500 | var zoomBox = $("
").css("width", this.options.zoomBoxWidth) 501 | .css("height",this.options.zoomBoxHeight) 502 | .css("position","absolute") 503 | .css("overflow","hidden") 504 | .css("top", this.options.annotationBoxHeight) 505 | .css("left",this.options.annotationBoxWidth/2.0-this.options.zoomBoxWidth/2.0) 506 | .resizable(); 507 | return zoomBox.append(image); 508 | 509 | }, 510 | _generateHelp : function(entities){ 511 | var helpDiv = $("
").hide(); 512 | $.each(entities, function(){ 513 | helpDiv.append( $("
") 514 | .append(this.help) 515 | .hide() 516 | .addClass("scribe_help_content")); 517 | }); 518 | return helpDiv; 519 | }, 520 | _generateTabBar : function(entities){ 521 | var tabBar = $("
    "); 522 | var self=this; 523 | $.each(entities, function(){ 524 | var elementName= this.name.replace(/ /,"_"); 525 | var elementId = "scribe_tab_"+elementName; 526 | var tab = $("
  • "+elementName+"
  • "); 527 | tab.click(elementName,jQuery.proxy(self._switchEntityType, self) ); 528 | tabBar.append(tab); 529 | }); 530 | return tabBar; 531 | }, 532 | _generateInputs : function(entities){ 533 | var inputBar =$("
    "); 534 | var self = this; 535 | 536 | $.each(entities, function(entity_index,entity){ 537 | var currentInputPane = $("
    ").addClass("scribe_annotation_input").hide(); 539 | $.each(entity.fields, function(field_index,field){ 540 | var current_field = self._generateField(field); 541 | if(entity_index==0) {current_field.show();} 542 | else {current_field.hide();} 543 | currentInputPane.append(current_field); 544 | }); 545 | inputBar.append(currentInputPane); 546 | }); 547 | return inputBar; 548 | }, 549 | 550 | changeHelp : function(entity_name){ 551 | $(".scribe_help_content").hide(); 552 | $("#scribe_help_"+entity_name).show(); 553 | }, 554 | showHelp : function(){ 555 | this.options.helpShowing=true; 556 | if(this.options.orientation=="floatAbove"){ 557 | $("#scribe_annotation_help").stop().show().animate({"top":"-100","opacity":"100"},500); 558 | } 559 | else{ 560 | $("#scribe_annotation_help").stop().show().animate({"top":"100","opacity":"100"},500); 561 | 562 | } 563 | var helpID=$("#scribe_tab_bar .scribe_selected_tab").attr("id"); 564 | 565 | this.changeHelp(helpID.replace("scribe_tab_","")); 566 | // helpID=helpID.replace("tab","help"); 567 | // alert("showing help "+helpID); 568 | // $("#"+helpID).addClass("scribe_current_help"); 569 | // $(".scribe_current_help").stop().animate({top:'-80', opacity:"100"},500); 570 | // $("#scribe_annotation_help_button").html("hide help"); 571 | $("#scribe_annotation_help_button").html("hide help"); 572 | 573 | }, 574 | hideHelp : function(){ 575 | this.options.helpShowing=false; 576 | 577 | $("#scribe_annotation_help").stop().animate({"top":"0","opacity":"0"},500); 578 | // $(".scribe_current_help").stop().animate({top:'0',opacity:"0"},500); 579 | $("#scribe_annotation_help_button").html("show help"); 580 | } 581 | 582 | 583 | 584 | }); 585 | --------------------------------------------------------------------------------