├── .gitignore ├── Procfile ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── scala │ ├── JettyLauncher.scala │ ├── demo │ │ └── MyScalatraFilter.scala │ ├── download │ │ └── ImageDownloader.scala │ └── search │ │ ├── documents │ │ ├── Document.scala │ │ ├── MockDocument.scala │ │ ├── NationalArchiveDocument.scala │ │ └── QueryDocument.scala │ │ ├── indexing │ │ ├── InvertedIndex.scala │ │ └── SearchRanker.scala │ │ ├── managers │ │ ├── LuceneSearchManager.scala │ │ └── SearchManager.scala │ │ ├── parsing │ │ ├── Parser.scala │ │ └── stopWords.txt │ │ └── result │ │ ├── Result.scala │ │ └── Snippet.scala └── webapp │ ├── WEB-INF │ ├── scalate │ │ └── layouts │ │ │ └── default.scaml │ └── web.xml │ └── static │ ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── bootstrap2.css │ ├── jqueryui.css │ ├── lightbox.css │ ├── main.css │ ├── timeline.css │ └── timeline.png │ └── js │ ├── backbone.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── custom │ ├── masonryloader.js │ ├── search.js │ └── timelineloader.js │ ├── jquery-1.7.2.min.js │ ├── jquery-ui-1.8.16.custom.min.js │ ├── jquery.imagesloaded.min.js │ ├── jquery.isotope.min.js │ ├── jquery.knob.js │ ├── jquery.lazyload.min.js │ ├── jquery.masonry.min.js │ ├── lightbox.js │ ├── mustache.js │ ├── spin.min.js │ ├── storyjs-embed.js │ ├── timeline-min.js │ └── underscore.js ├── resources ├── PhotoMetaData10000.csv ├── documents │ ├── bible │ │ ├── 1Chronicles.txt │ │ ├── 1Corinthians.txt │ │ ├── 1John.txt │ │ ├── 1Kings.txt │ │ ├── 1Peter.txt │ │ ├── 1Samuel.txt │ │ ├── 1Thessalonians.txt │ │ ├── 1Timothy.txt │ │ ├── 2Chronicles.txt │ │ ├── 2Corinthians.txt │ │ ├── 2John.txt │ │ ├── 2Kings.txt │ │ ├── 2Peter.txt │ │ ├── 2Samuel.txt │ │ ├── 2Thessalonians.txt │ │ ├── 2Timothy.txt │ │ ├── 3John.txt │ │ ├── Acts.txt │ │ ├── Amos.txt │ │ ├── Colossians.txt │ │ ├── Daniel.txt │ │ ├── Dedicatory.txt │ │ ├── Deuteronomy.txt │ │ ├── Ecclesiastes.txt │ │ ├── Ephesians.txt │ │ ├── Esther.txt │ │ ├── Exodus.txt │ │ ├── Ezekiel.txt │ │ ├── Ezra.txt │ │ ├── Galatians.txt │ │ ├── Genesis.txt │ │ ├── Habakkuk.txt │ │ ├── Haggai.txt │ │ ├── Hebrews.txt │ │ ├── Hosea.txt │ │ ├── Isaiah.txt │ │ ├── James.txt │ │ ├── Jeremiah.txt │ │ ├── Job.txt │ │ ├── Joel.txt │ │ ├── John.txt │ │ ├── Jonah.txt │ │ ├── Joshua.txt │ │ ├── Jude.txt │ │ ├── Judges.txt │ │ ├── Lamentations.txt │ │ ├── Leviticus.txt │ │ ├── Luke.txt │ │ ├── Malachi.txt │ │ ├── Mark.txt │ │ ├── Matthew.txt │ │ ├── Micah.txt │ │ ├── Nahum.txt │ │ ├── Nehemiah.txt │ │ ├── Numbers.txt │ │ ├── Obadiah.txt │ │ ├── Philemon.txt │ │ ├── Philippians.txt │ │ ├── Preface.txt │ │ ├── Preface_w_footnotes.txt │ │ ├── Proverbs.txt │ │ ├── Psalms.txt │ │ ├── Revelation.txt │ │ ├── Romans.txt │ │ ├── Ruth.txt │ │ ├── SongofSolomon.txt │ │ ├── Titus.txt │ │ ├── Zechariah.txt │ │ └── Zephaniah.txt │ └── mopp │ │ ├── A_01_01.txt │ │ ├── A_01_03.txt │ │ ├── A_01_04.txt │ │ ├── A_01_05.txt │ │ ├── A_02_01.txt │ │ ├── A_02_02.txt │ │ ├── A_02_03.txt │ │ ├── A_02_04.txt │ │ ├── A_02_05.txt │ │ ├── A_02_06.txt │ │ ├── A_03_01.txt │ │ ├── A_03_02.txt │ │ ├── A_03_03.txt │ │ ├── A_03_04.txt │ │ ├── A_03_05.txt │ │ ├── A_03_06.txt │ │ ├── A_03_07.txt │ │ ├── A_03_08.txt │ │ ├── A_03_09.txt │ │ ├── A_04_01.txt │ │ ├── A_06_01.txt │ │ ├── A_07_01.txt │ │ ├── A_08_01.txt │ │ ├── A_08_02.txt │ │ ├── A_08_03.txt │ │ ├── A_08_04.txt │ │ ├── A_08_05.txt │ │ ├── A_08_06.txt │ │ ├── A_08_07.txt │ │ ├── A_08_08.txt │ │ ├── A_08_09.txt │ │ ├── A_09_01.txt │ │ ├── A_09_02.txt │ │ ├── A_09_03.txt │ │ ├── A_09_04.txt │ │ ├── A_09_05.txt │ │ ├── A_09_06.txt │ │ ├── A_09_07.txt │ │ ├── A_09_08.txt │ │ ├── B_01_01.txt │ │ ├── B_02_01.txt │ │ ├── B_02_02.txt │ │ ├── B_02_03.txt │ │ ├── B_03_01.txt │ │ ├── B_03_02.txt │ │ ├── B_03_03.txt │ │ ├── B_03_05.txt │ │ ├── B_03_06.txt │ │ ├── B_03_07.txt │ │ ├── B_03_08.txt │ │ ├── B_04_01.txt │ │ ├── B_04_02.txt │ │ ├── B_04_03.txt │ │ ├── B_04_04.txt │ │ ├── B_04_05.txt │ │ ├── B_04_06.txt │ │ ├── B_04_07.txt │ │ ├── B_04_08.txt │ │ ├── B_04_10.txt │ │ ├── B_04_11.txt │ │ ├── B_05_01.txt │ │ ├── B_05_02.txt │ │ ├── B_05_03.txt │ │ ├── B_05_04.txt │ │ ├── B_05_05.txt │ │ ├── B_05_06.txt │ │ ├── B_06_01.txt │ │ ├── B_06_02.txt │ │ ├── B_06_03.txt │ │ ├── B_06_04.txt │ │ ├── B_06_05.txt │ │ ├── B_06_06.txt │ │ ├── B_06_07.txt │ │ ├── B_06_08.txt │ │ ├── B_06_09.txt │ │ ├── B_07_01.txt │ │ ├── B_07_02.txt │ │ ├── B_07_03.txt │ │ ├── B_07_04.txt │ │ ├── B_07_05.txt │ │ ├── B_07_06.txt │ │ ├── B_07_07.txt │ │ ├── B_07_08.txt │ │ ├── B_07_09.txt │ │ ├── B_07_10.txt │ │ ├── B_07_11.txt │ │ ├── B_07_12.txt │ │ ├── B_07_13.txt │ │ ├── B_08_01.txt │ │ ├── B_08_02.txt │ │ ├── B_08_03.txt │ │ ├── B_08_05.txt │ │ ├── B_08_06.txt │ │ ├── B_09_01.txt │ │ ├── B_09_02.txt │ │ ├── B_09_03.txt │ │ ├── B_09_05.txt │ │ ├── B_09_07.txt │ │ ├── B_10_01.txt │ │ ├── B_11_01.txt │ │ ├── B_11_02.txt │ │ ├── B_11_03.txt │ │ ├── B_11_04.txt │ │ ├── B_12_01.txt │ │ ├── B_12_02.txt │ │ ├── B_12_03.txt │ │ ├── B_12_04.txt │ │ ├── B_12_05.txt │ │ ├── B_12_06.txt │ │ ├── B_12_07.txt │ │ ├── B_12_08.txt │ │ ├── B_12_09.txt │ │ ├── C_01_01.txt │ │ ├── C_01_02.txt │ │ ├── C_01_03.txt │ │ ├── C_02_01.txt │ │ ├── C_03_01.txt │ │ ├── C_03_02.txt │ │ ├── C_03_03.txt │ │ ├── C_03_04.txt │ │ ├── C_03_05.txt │ │ ├── C_04_01.txt │ │ ├── C_04_02.txt │ │ ├── C_04_03.txt │ │ ├── C_04_04.txt │ │ ├── C_04_05.txt │ │ ├── C_04_06.txt │ │ ├── C_04_07.txt │ │ ├── C_05_01.txt │ │ ├── C_05_02.txt │ │ ├── C_05_03.txt │ │ ├── C_06_01.txt │ │ ├── C_06_02.txt │ │ ├── C_06_03.txt │ │ ├── C_06_04.txt │ │ ├── C_07_01.txt │ │ ├── D_01_01.txt │ │ ├── D_01_02.txt │ │ ├── D_02.txt │ │ ├── D_02_01.txt │ │ ├── D_02_02.txt │ │ ├── D_02_06.txt │ │ ├── D_02_07.txt │ │ ├── D_02_08.txt │ │ ├── D_03_01.txt │ │ ├── D_04_01.txt │ │ ├── D_04_02.txt │ │ ├── D_04_03.txt │ │ ├── D_04_04.txt │ │ ├── D_04_05.txt │ │ ├── D_05_01.txt │ │ ├── D_05_02.txt │ │ ├── D_05_03.txt │ │ ├── D_05_04.txt │ │ ├── D_05_05.txt │ │ ├── D_06_01.txt │ │ ├── D_06_02.txt │ │ ├── D_06_03.txt │ │ ├── D_06_04.txt │ │ ├── D_06_05.txt │ │ ├── D_06_06.txt │ │ ├── D_06_07.txt │ │ ├── D_07_01.txt │ │ ├── E_01_01.txt │ │ ├── E_01_02.txt │ │ ├── E_01_03.txt │ │ ├── E_01_04.txt │ │ ├── E_02_01.txt │ │ ├── E_03_01.txt │ │ ├── E_04_01.txt │ │ ├── E_04_02.txt │ │ ├── E_04_03.txt │ │ ├── E_04_04.txt │ │ ├── E_04_05.txt │ │ ├── E_05_01.txt │ │ ├── E_06_01.txt │ │ ├── E_06_02.txt │ │ ├── E_06_03.txt │ │ ├── E_06_04.txt │ │ ├── E_06_05.txt │ │ ├── E_06_06.txt │ │ ├── E_06_07.txt │ │ ├── E_06_08.txt │ │ ├── E_07_01.txt │ │ ├── E_07_02.txt │ │ ├── E_07_03.txt │ │ ├── E_07_04.txt │ │ ├── E_08_01.txt │ │ ├── E_09_01.txt │ │ ├── E_09_02.txt │ │ ├── E_09_03.txt │ │ ├── E_10_01.txt │ │ ├── E_10_02.txt │ │ ├── E_10_03.txt │ │ ├── E_10_04.txt │ │ ├── E_11_01.txt │ │ ├── E_11_02.txt │ │ ├── E_11_03.txt │ │ ├── E_11_04.txt │ │ ├── E_11_05.txt │ │ ├── E_11_06.txt │ │ ├── F_01_01.txt │ │ ├── F_01_02.txt │ │ ├── F_01_03.txt │ │ ├── F_01_04.txt │ │ ├── F_01_05.txt │ │ ├── F_01_06.txt │ │ ├── F_01_07.txt │ │ ├── F_01_08.txt │ │ ├── F_01_09.txt │ │ ├── F_01_10.txt │ │ ├── F_01_11.txt │ │ ├── F_01_12.txt │ │ ├── F_02_01.txt │ │ ├── F_02_02.txt │ │ ├── F_02_03.txt │ │ ├── F_03_01.txt │ │ ├── F_03_03.txt │ │ ├── F_03_04.txt │ │ ├── F_03_05.txt │ │ ├── F_03_06.txt │ │ ├── F_04_01.txt │ │ ├── F_04_02.txt │ │ ├── F_05_01.txt │ │ ├── F_06_01.txt │ │ ├── F_06_02.txt │ │ ├── F_06_03.txt │ │ ├── G_01_01.txt │ │ ├── G_02_01.txt │ │ ├── G_02_02.txt │ │ ├── G_02_03.txt │ │ ├── G_03_01.txt │ │ ├── G_03_02.txt │ │ ├── G_03_03.txt │ │ ├── G_03_04.txt │ │ ├── G_04_01.txt │ │ ├── G_05_01.txt │ │ ├── G_05_02.txt │ │ ├── G_05_03.txt │ │ ├── G_05_04.txt │ │ ├── G_05_05.txt │ │ ├── G_05_06.txt │ │ ├── G_05_07.txt │ │ ├── G_06_01.txt │ │ ├── G_07_01.txt │ │ ├── G_07_02.txt │ │ ├── G_07_03.txt │ │ ├── G_08_01.txt │ │ ├── G_08_02.txt │ │ ├── G_08_03.txt │ │ ├── G_08_04.txt │ │ ├── H_01_01.txt │ │ ├── H_02_01.txt │ │ ├── H_03_01.txt │ │ ├── H_03_02.txt │ │ ├── H_03_03.txt │ │ ├── H_03_04.txt │ │ ├── H_03_05.txt │ │ ├── H_03_06.txt │ │ ├── H_04_01.txt │ │ ├── H_04_02.txt │ │ ├── H_04_03.txt │ │ ├── H_04_04.txt │ │ ├── H_05_01.txt │ │ ├── H_05_02.txt │ │ ├── H_05_03.txt │ │ ├── H_06_01.txt │ │ ├── H_06_02.txt │ │ ├── H_06_03.txt │ │ ├── H_07_01.txt │ │ ├── I_01_01.txt │ │ ├── I_01_02.txt │ │ ├── I_02_01.txt │ │ ├── I_02_02.txt │ │ ├── I_02_04.txt │ │ ├── I_02_05.txt │ │ ├── I_02_06.txt │ │ ├── I_03_01.txt │ │ ├── I_03_02.txt │ │ ├── I_04_01.txt │ │ ├── I_05_01.txt │ │ ├── I_05_02.txt │ │ ├── I_06_01.txt │ │ ├── I_07_01.txt │ │ ├── I_07_02.txt │ │ ├── I_07_03.txt │ │ ├── I_08.txt │ │ ├── app_A_1_1_2.txt │ │ ├── app_B_7_9_1.txt │ │ ├── app_D_7_1_1.txt │ │ └── index.txt ├── stopWords.txt └── testDocument.txt └── test └── scala └── search ├── documents └── TestDocument.scala ├── indexing ├── TestSearchRanker.scala └── TestSearchRanker2.scala ├── managers ├── TestSearchManager.scala ├── TestSearchManagerOnLucene.scala └── TestSearchManagerOnMopp.scala └── parser └── TestParser.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # use glob syntax. 2 | syntax: glob 3 | *.ser 4 | *.class 5 | *~ 6 | *.bak 7 | #*.off 8 | *.old 9 | 10 | # eclipse conf file 11 | .settings 12 | .classpath 13 | .project 14 | .manager 15 | .scala_dependencies 16 | 17 | # idea 18 | .idea 19 | *.iml 20 | 21 | # building 22 | target 23 | build 24 | null 25 | tmp* 26 | temp* 27 | dist 28 | test-output 29 | build.log 30 | 31 | # other scm 32 | .svn 33 | .CVS 34 | .hg* 35 | .jpg 36 | 37 | # switch to regexp syntax. 38 | # syntax: regexp 39 | # ^\.pc/ 40 | 41 | #SHITTY output not in target directory 42 | build.log 43 | .DS_Store 44 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: target/start -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.startscript.StartScriptPlugin 2 | 3 | organization := "com.github.dbousamra" 4 | 5 | name := "search-engine-scala" 6 | 7 | version := "0.0.1" 8 | 9 | scalaVersion := "2.9.2" 10 | 11 | seq(webSettings :_*) 12 | 13 | classpathTypes ~= (_ + "orbit") 14 | 15 | seq(StartScriptPlugin.startScriptForClassesSettings: _*) 16 | 17 | libraryDependencies ++= Seq( 18 | "org.apache.lucene" % "lucene-core" % "3.6.1", 19 | "xstream" % "xstream" % "1.2.2", 20 | "net.liftweb" % "lift-json_2.9.1" % "2.4", 21 | "net.sf.opencsv" % "opencsv" % "2.0", 22 | "junit" % "junit" % "4.8.1" % "test", 23 | "org.scalatest" %% "scalatest" % "1.8" % "test", 24 | "org.scalaz" %% "scalaz-core" % "6.0.4", 25 | "org.scalatra" % "scalatra" % "2.1.1", 26 | "org.scalatra" % "scalatra-scalate" % "2.1.1", 27 | "org.scalatra" % "scalatra-specs2" % "2.1.1" % "test", 28 | "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", 29 | "edu.mit" % "jwi" % "2.2.1", 30 | "org.eclipse.jetty" % "jetty-webapp" % "8.1.7.v20120910" % "container;test;provided", 31 | "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")) 32 | ) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.12.0 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.typesafeResolver 2 | 3 | resolvers += "Web plugin repo" at "http://siasia.github.com/maven2" 4 | 5 | libraryDependencies += "com.github.siasia" % "xsbt-web-plugin_2.9.2" % "0.12.0-0.2.11.1" 6 | 7 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0") 8 | 9 | addSbtPlugin("com.typesafe.startscript" % "xsbt-start-script-plugin" % "0.5.3") -------------------------------------------------------------------------------- /src/main/scala/JettyLauncher.scala: -------------------------------------------------------------------------------- 1 | import org.eclipse.jetty.server.Server 2 | import org.eclipse.jetty.servlet.{DefaultServlet, ServletContextHandler} 3 | import net.srirangan.MyScalatraFilter 4 | import org.eclipse.jetty.webapp.WebAppContext 5 | 6 | object JettyLauncher { 7 | def main(args: Array[String]) { 8 | val port = if(System.getenv("PORT") != null) System.getenv("PORT").toInt else 8080 9 | 10 | val server = new Server(port) 11 | val context = new WebAppContext() 12 | context setContextPath "/" 13 | context.setResourceBase("src/main/webapp") 14 | context.addServlet(classOf[MyScalatraFilter], "/*") 15 | context.addServlet(classOf[DefaultServlet], "/") 16 | 17 | server.setHandler(context) 18 | 19 | server.start 20 | server.join 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/scala/demo/MyScalatraFilter.scala: -------------------------------------------------------------------------------- 1 | package net.srirangan 2 | import org.scalatra._ 3 | import java.net.URL 4 | import scalate.ScalateSupport 5 | import search.managers.SearchManager 6 | import java.io.File 7 | import net.liftweb.json.JsonDSL._ 8 | import net.liftweb.json._ 9 | import search.documents.NationalArchiveDocument 10 | import search.documents.NationalArchiveDocumentManager 11 | import search.managers.LuceneSearchManager 12 | 13 | class MyScalatraFilter extends ScalatraServlet with ScalateSupport { 14 | 15 | override implicit val contentType = "text/html" 16 | private val searchManager = new SearchManager[NationalArchiveDocument]() 17 | private val documentManager = new NationalArchiveDocumentManager() 18 | searchManager.addToIndex(documentManager.parse("src/resources/PhotoMetaData10000Replaced.csv")) 19 | 20 | get("/") { 21 | scaml("home") 22 | } 23 | 24 | get("/timeline") { 25 | scaml("timeline") 26 | } 27 | 28 | get("/timeline/data") { 29 | val queryString = "gold coast" 30 | val results = searchManager.query(queryString) 31 | val json = ( 32 | ("timeline") -> 33 | ("headline" -> "National Archives of Australia") 34 | ~ ("text" -> "Search stuff") 35 | ~ ("type" -> "default") 36 | ~ ("date" -> results.map { 37 | p => 38 | ( 39 | ("startDate" -> p.document.year.toString) 40 | ~ ("headline" -> p.document.barcode) 41 | ~ ("text" -> p.document.description) 42 | ~ ("asset" -> ("media" -> p.document.largeImageURL))) 43 | })) 44 | pretty(render(json)) 45 | } 46 | 47 | get("/search") { 48 | val queryString = params("query") 49 | val results = searchManager.query(queryString) 50 | val min = if (results.isEmpty) 0 else results.minBy(_.document.year).document.year 51 | val max = if (results.isEmpty) 0 else results.maxBy(_.document.year).document.year 52 | val json = ( 53 | ("results" -> results.map ( 54 | p => 55 | ( 56 | ("barcode" -> p.document.barcode) 57 | ~ ("description" -> p.document.description) 58 | ~ ("score" -> p.score) 59 | ~ ("year" -> p.document.year) 60 | ~ ("smallImageURL" -> p.document.smallImageURL) 61 | ~ ("largeImageURL" -> p.document.largeImageURL) 62 | ~ ("location" -> p.document.location)) 63 | )) 64 | ~ ("resultsLength" -> results.length) 65 | ~ ("startDate" -> min) 66 | ~ ("endDate" -> max)) 67 | pretty(render(json)) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/scala/download/ImageDownloader.scala: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import search.documents.NationalArchiveDocument 4 | import search.documents.NationalArchiveDocumentManager 5 | import java.io._ 6 | 7 | object ImageDownloader { 8 | 9 | def main(args: Array[String]): Unit = { 10 | val documents = new NationalArchiveDocumentManager().parse("someFile") 11 | // val documents = new NationalArchiveDocumentManager().parse("src/resources/PhotoMetaData10000.csv") 12 | val out = new java.io.FileWriter(new File("someFile2")) 13 | val urls = documents.foreach { x => 14 | // val source = scala.io.Source.fromURL(x.smallImageURL) 15 | // println(x.barcode) 16 | // val writer = new PrintWriter(new File("images", x.barcode + ".jpg")) 17 | // writer.write(source.mkString("")) 18 | // writer.close() 19 | val lo = "images/" + x.barcode+ ".jpg" 20 | out.write(x.barcode + "," + "\"" + x.description.replaceAll("\"", "\"\"") + "\"" + "," + x.year.toString + "," + x.location + "," + lo + "," + lo + "\n") 21 | 22 | } 23 | out.close 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/scala/search/documents/Document.scala: -------------------------------------------------------------------------------- 1 | package search.documents 2 | 3 | abstract class Document(val words: List[String]) { 4 | 5 | def getWordCount(word: String) = counts.getOrElse(word, 0) 6 | 7 | private lazy val counts = words.foldLeft(collection.mutable.HashMap[String, Int]()) { 8 | (map, word) => map += word -> (map.getOrElse(word, 0) + 1) 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/main/scala/search/documents/MockDocument.scala: -------------------------------------------------------------------------------- 1 | package search.documents 2 | 3 | import java.io.File 4 | import search.parsing.Parser 5 | import search.parsing.Parser._ 6 | 7 | class MockDocument(val _name: Option[String], val file: Option[File], words: List[String]) extends Document(words) { 8 | 9 | val name = file match { 10 | case Some(file) => file.getName() 11 | case None => _name.getOrElse("Untitled") 12 | } 13 | 14 | override def toString = name 15 | } 16 | 17 | class MockDocumentManager { 18 | 19 | private val parser = new Parser() 20 | val removeStopWords: Boolean = true 21 | 22 | def parseFolder(folder: File): List[MockDocument] = { 23 | folder.listFiles().toList.map(f => parse(f)) 24 | } 25 | 26 | def parse(file: File): MockDocument = { 27 | val words = parser.parse(file, removeStopWords) 28 | new MockDocument(Some(file.getName()), Some(file), words) 29 | } 30 | 31 | def parseText(input: String, removeStopWords: Boolean = true): MockDocument = { 32 | val words = parser.parse(input, removeStopWords) 33 | new MockDocument(None, None, words) 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/main/scala/search/documents/NationalArchiveDocument.scala: -------------------------------------------------------------------------------- 1 | package search.documents 2 | 3 | import au.com.bytecode.opencsv.CSVReader 4 | import scala.collection.JavaConverters._ 5 | import java.io.FileReader 6 | import search.parsing.Parser 7 | import search.parsing.Parser._ 8 | 9 | class NationalArchiveDocument( 10 | val barcode: String, 11 | title: List[String], 12 | val description: String, 13 | val year: Int, 14 | val location: String, 15 | val largeImageURL: String, 16 | val smallImageURL: String) extends Document(title) 17 | 18 | class NationalArchiveDocumentManager { 19 | 20 | private val parser = new Parser() 21 | 22 | val photos = "src/resources/PhotoMetaData.csv" 23 | 24 | def parse(filename: String): Seq[NationalArchiveDocument] = { 25 | val reader = new CSVReader(new FileReader(filename)); 26 | // reader.readAll().asScala.tail.map(parseRow).toList 27 | 28 | val iterator = Iterator.continually(reader.readNext()).takeWhile(_ != null) 29 | iterator.toSeq.tail.map(parseRow) 30 | } 31 | 32 | def parseRow(row: Array[String]): NationalArchiveDocument = { 33 | new NationalArchiveDocument( 34 | barcode = row(0), 35 | description = row(1), 36 | title = parser.parse(row(1)), 37 | year = row(2).toInt, 38 | location = row(3), 39 | largeImageURL = row(4), 40 | smallImageURL = row(5)) 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/scala/search/documents/QueryDocument.scala: -------------------------------------------------------------------------------- 1 | package search.documents 2 | 3 | import search.parsing.Parser 4 | import search.parsing.Parser._ 5 | 6 | class QueryDocument(words: List[String]) extends Document(words) 7 | 8 | class QueryDocumentManager { 9 | 10 | val parser = new Parser 11 | 12 | def parseText(input: String, removeStopWords: Boolean = true): QueryDocument = { 13 | val words = parser.parse(input, removeStopWords) 14 | new QueryDocument(words) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/scala/search/indexing/InvertedIndex.scala: -------------------------------------------------------------------------------- 1 | package search.indexing 2 | 3 | import scala.collection.mutable.LinkedHashMap 4 | import search.documents.Document 5 | import search.documents.MockDocument 6 | import search.documents.QueryDocument 7 | import scala.collection.mutable.ArrayBuffer 8 | 9 | class InvertedIndex[T <: Document] { 10 | 11 | val index = new LinkedHashMap[String, LinkedHashMap[T, Int]] 12 | val weights = new LinkedHashMap[T, Double] 13 | val names = new ArrayBuffer[T]() 14 | private var _totalDocumentsIndexed = 0 15 | 16 | def addDocumentToIndex(document: T*) = { 17 | document.foreach { d => 18 | d.words.foreach { word => 19 | val x = index.getOrElseUpdate(word, LinkedHashMap(d -> 0)) 20 | x.put(d, x.get(d).getOrElse(0) + 1) 21 | } 22 | incrementTotalDocumentsIndexed() 23 | } 24 | for (doc <- document) { 25 | calculateVectorSpaces(doc) 26 | names += (doc) 27 | } 28 | } 29 | 30 | def calculateVectorSpaces(document: T) = { 31 | weights.put(document, vectorWeights(document)) 32 | } 33 | 34 | def similarity(query: QueryDocument, document: T) = { 35 | dotProduct(query, document) / (vectorWeights(query) * weights.get(document).get) 36 | // dotProduct(query, document) / (vectorWeights(query) * vectorWeights(document)) 37 | } 38 | 39 | def vectorWeights(document: Document) = { 40 | val weights = index.map { word => 41 | math.pow(tfidf(word._1, document), 2) 42 | } 43 | math.sqrt(weights.sum) 44 | } 45 | 46 | /** 47 | * http://c2.com/cgi/wiki?DotProductInManyProgrammingLanguages 48 | */ 49 | private def dp[T <% Double](as: Iterable[T], bs: Iterable[T]) = { 50 | require(as.size == bs.size) 51 | (for ((a, b) <- as zip bs) yield a * b) sum 52 | } 53 | 54 | def dotProduct(query: QueryDocument, document: T) = { 55 | val queryTfidfs = index.map(word => tfidf(word._1, query)) 56 | val documentTfidfs = index.map(word => tfidf(word._1, document)) 57 | dp(queryTfidfs, documentTfidfs) 58 | } 59 | 60 | def normalize(word: String, document: T) = { 61 | math.sqrt(document.words.foldLeft(0D)((accum, w) => accum + math.pow(idf(w), 2))) 62 | } 63 | 64 | def tf(word: String, document: Document) = { 65 | val count = document.getWordCount(word) 66 | if (count > 0) count 67 | else 0.0 68 | } 69 | 70 | def idf(word: String) = { 71 | val occursInAll: Double = index.get(word) match { 72 | case Some(occurrence) => occurrence.size 73 | case None => 0 74 | } 75 | val idf = 1.0 + math.log10(totalDocumentsIndexed / occursInAll) 76 | if (idf.isNaN()) 0.0 else idf 77 | } 78 | 79 | def tfidf(word: String, document: Document) = { 80 | val tfw = tf(word, document) 81 | if (tfw == 0) 0 else tfw * idf(word) 82 | } 83 | 84 | def getAllRelevantDocuments(words: List[String]): List[T] = { 85 | words.map(word => index.get(word).getOrElse(Nil).map(x => x._1).toList).flatten.distinct 86 | } 87 | 88 | def containsDocument(document: T) = names.contains(document) 89 | 90 | def incrementTotalDocumentsIndexed() = _totalDocumentsIndexed += 1 91 | 92 | def totalDocumentsIndexed = _totalDocumentsIndexed 93 | 94 | override def toString = index.mkString("\n") 95 | } -------------------------------------------------------------------------------- /src/main/scala/search/indexing/SearchRanker.scala: -------------------------------------------------------------------------------- 1 | package search.indexing 2 | 3 | import search.documents.Document 4 | import search.result.Result 5 | import search.documents.QueryDocument 6 | 7 | class SearchRanker[T <: Document](val index: InvertedIndex[T]) { 8 | 9 | def query(inputQuery: QueryDocument): List[Result[T]] = { 10 | val documents = index.getAllRelevantDocuments(inputQuery.words) 11 | // println("relevant documents " + documents) 12 | documents.map(doc => query(inputQuery, doc)).sortBy(_.score).reverse 13 | } 14 | 15 | def queryer(input: String) = { 16 | List(1,2,3).reduce(_+_) 17 | } 18 | 19 | def query(query: QueryDocument, document: T): Result[T] = { 20 | val score = index.similarity(query, document) 21 | new Result[T](document, score) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/scala/search/managers/LuceneSearchManager.scala: -------------------------------------------------------------------------------- 1 | package search.managers 2 | 3 | import scala.Array.canBuildFrom 4 | import scala.collection.mutable.LinkedHashMap 5 | 6 | import org.apache.lucene.analysis.standard.StandardAnalyzer 7 | import org.apache.lucene.document.Document 8 | import org.apache.lucene.document.Field 9 | import org.apache.lucene.index.IndexWriter 10 | import org.apache.lucene.queryParser.QueryParser 11 | import org.apache.lucene.search.IndexSearcher 12 | import org.apache.lucene.store.RAMDirectory 13 | import org.apache.lucene.util.Version 14 | 15 | import com.thoughtworks.xstream.XStream 16 | 17 | import search.documents.{Document => doc} 18 | import search.result.Result 19 | 20 | class LuceneSearchManager[T <: doc] { 21 | 22 | val analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT) 23 | val directory = new RAMDirectory(); 24 | val writer = new IndexWriter(directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED) 25 | val mapper = new LinkedHashMap[Int, T] 26 | 27 | def addToIndex(documents: Traversable[T]) = { 28 | var id = 0 29 | documents.foreach { d => 30 | val doc = simpleDoc(id, d) 31 | writer.addDocument(doc) 32 | mapper += id -> d 33 | id += 1 34 | } 35 | writer.commit 36 | writer.close 37 | } 38 | 39 | private def simpleDoc(id: Int, d: T) = { 40 | val doc = new Document() 41 | doc.add(new Field("content", d.words.mkString(" "), Field.Store.YES, Field.Index.ANALYZED)) 42 | doc.add(new Field("id", id.toString, Field.Store.YES, Field.Index.NO)) 43 | doc 44 | } 45 | 46 | def query(input: String) = { 47 | val searcher = new IndexSearcher(directory) 48 | val q = new QueryParser(Version.LUCENE_36, "content", analyzer).parse(input); 49 | val docs = searcher.search(q, 100) 50 | val xstream = new XStream() 51 | 52 | val results = docs.scoreDocs map { docId => 53 | val d = searcher.doc(docId.doc) 54 | val backToDocument = mapper.get(d.get("id").toInt).get 55 | new Result(backToDocument, docId.score) 56 | } 57 | 58 | searcher.close 59 | results.toList 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/scala/search/managers/SearchManager.scala: -------------------------------------------------------------------------------- 1 | package search.managers 2 | 3 | import scala.Array.canBuildFrom 4 | import search.documents.Document 5 | import search.documents.QueryDocumentManager 6 | import search.indexing.InvertedIndex 7 | import search.indexing.SearchRanker 8 | import search.parsing.Parser 9 | import search.parsing.Parser.string2Iterator 10 | 11 | class SearchManager[T <: Document] { 12 | 13 | private val _index = new InvertedIndex[T]() 14 | private val ranker = new SearchRanker[T](index) 15 | private val parser: Parser = new Parser() 16 | 17 | def addToIndex(documents: Traversable[T]): List[T] = { 18 | documents.map(addToIndex).toList 19 | } 20 | 21 | def addToIndex(document: T): T = { 22 | if (index.containsDocument(document)) { 23 | document 24 | } else { 25 | _index.addDocumentToIndex(document) 26 | document 27 | } 28 | } 29 | 30 | def query(input: String) = { 31 | val queryable = new QueryDocumentManager().parseText(input) 32 | ranker.query(queryable).filter(d => d.score > 0.0).take(100) 33 | } 34 | 35 | def queryMatch(input: String) = { 36 | val queryable = new QueryDocumentManager().parseText(input) 37 | // _index.index.keys.filter(_.startsWith(input).) 38 | val x = _index.index.filter(_._1.startsWith(input)) 39 | } 40 | 41 | def index = _index 42 | 43 | } -------------------------------------------------------------------------------- /src/main/scala/search/parsing/Parser.scala: -------------------------------------------------------------------------------- 1 | package search.parsing 2 | 3 | import scala.io.Source 4 | import java.io.File 5 | import java.util.Locale 6 | import java.io.InputStream 7 | 8 | object Parser { 9 | implicit def file2Iterator(file: File) = { 10 | Source.fromFile(file, "latin1").getLines() 11 | } 12 | implicit def string2Iterator(input: String) = { 13 | List(input).toIterator 14 | } 15 | } 16 | 17 | class Parser { 18 | 19 | private val STOP_WORDS = "search/parsing/stopWords.txt"; 20 | private val stopWords: Set[String] = parseStopWords(getClass.getClassLoader.getResourceAsStream(STOP_WORDS)) 21 | 22 | def parseStopWords(stream: InputStream) = Source.fromInputStream(stream).getLines().toSet 23 | 24 | def parse(input: Iterator[String], removeStopWords: Boolean = true): List[String] = { 25 | input.map { x => 26 | val words = getWordsFromLine(x) 27 | if (removeStopWords) 28 | filterStopWords(words) 29 | else 30 | words 31 | }.toList.flatten 32 | } 33 | 34 | private val getWordsFromLine = (line: String) => { 35 | line.split(" ") 36 | .map(_.toLowerCase()) 37 | .map(word => word.filter(Character.isLetter(_))) 38 | // .filter(_.length() > 1) 39 | .toList 40 | } 41 | 42 | private val filterStopWords = (words: List[String]) => { 43 | words.filterNot(word => stopWords.contains(word)) 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/scala/search/result/Result.scala: -------------------------------------------------------------------------------- 1 | package search.result 2 | 3 | import search.documents.Document 4 | import scala.collection.mutable.Map 5 | 6 | case class Result[T <: Document](document: T, var score: Double, snippet: Snippet) { 7 | 8 | 9 | def this(document: T, score: Double) = { 10 | this(document, score, new Snippet(document)) 11 | } 12 | 13 | override def toString = "\n" + document + ": Score=" + score 14 | 15 | } -------------------------------------------------------------------------------- /src/main/scala/search/result/Snippet.scala: -------------------------------------------------------------------------------- 1 | package search.result 2 | 3 | import search.documents.Document 4 | import scala.io.Source 5 | 6 | class Snippet(document: Document) { 7 | 8 | val sentences = { 9 | //Source.fromFile(document.file.get).getLines.toList.flatten 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/scalate/layouts/default.scaml: -------------------------------------------------------------------------------- 1 | -@ val title: String = "National Archives of Australia Search" 2 | -@ val headline: String = title 3 | -@ val body: String 4 | 5 | !!! 6 | %html 7 | %head 8 | %title= title 9 | %link(rel="stylesheet" type="text/css" href="/static/css/bootstrap2.css") 10 | %link(rel="stylesheet" type="text/css" href="/static/css/main.css") 11 | 12 | %script(type="text/javascript" src="/static/js/jquery-1.7.2.min.js") 13 | %script(type="text/javascript" src="/static/js/bootstrap.min.js") 14 | %body 15 | 16 | %div.navbar 17 | %div.navbar-inner 18 | %a.brand{:href => "#"} 19 | National Archives Search 20 | 21 | != body -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 18 | 19 | 20 | scalatra 21 | net.srirangan.MyScalatraFilter 22 | 23 | 24 | 25 | scalatra 26 | /* 27 | 28 | 29 | 30 | default 31 | /img/* 32 | /css/* 33 | /js/* 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/webapp/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | /*background-color: #E4E4E4;*/ 3 | background-color: #FBFBFC; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .container { 8 | width: 1200px; 9 | } 10 | 11 | .modal-body { 12 | max-height: 800px; 13 | } 14 | 15 | .modal-big{ 16 | max-width:940px; 17 | width: auto; 18 | } 19 | 20 | 21 | .box img { 22 | width: 198px; 23 | } 24 | 25 | .box { 26 | background-color: white; 27 | padding: 16px; 28 | margin: 4px; 29 | border: 1px solid #DEDEDE; 30 | max-width: 198px; 31 | } 32 | 33 | .caption { 34 | border-top: 1px solid #DEDEDE; 35 | margin: 16px -16px 0px -16px; 36 | /* margin-right: -16px; 37 | margin-left: -16px;*/ 38 | /*padding-left: 16px;*/ 39 | padding: 16px; 40 | padding-bottom: 0px; 41 | } 42 | 43 | .centered { 44 | margin: 0 auto; 45 | } 46 | 47 | .home-tile { 48 | width: 198px; 49 | /*display: none;*/ 50 | } 51 | 52 | .input-mysize { 53 | width: 120px 54 | } 55 | -------------------------------------------------------------------------------- /src/main/webapp/static/css/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbousamra/search-engine-scala/379f9c4386e5e600a4d5d93b5b2ff294c7a74dfc/src/main/webapp/static/css/timeline.png -------------------------------------------------------------------------------- /src/main/webapp/static/js/custom/masonryloader.js: -------------------------------------------------------------------------------- 1 | // jQuery.expr[':'].between = function(a, b, c) { 2 | // var args = c[3].split(','); 3 | // var val = parseInt(jQuery(a).attr("id")); 4 | // return val >= parseInt(args[0]) && val <= parseInt(args[1]); 5 | // }; 6 | 7 | var $container = $('#result_content'); 8 | $container.imagesLoaded( function(){ 9 | $container.isotope({ 10 | itemSelector : '.box', 11 | // animationOptions: { 12 | // duration: 750, 13 | // easing: 'linear', 14 | // queue: false 15 | // }, 16 | animated : true, 17 | resizable : true, 18 | }); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /src/main/webapp/static/js/custom/search.js: -------------------------------------------------------------------------------- 1 | var Result = Backbone.Model.extend({}); 2 | 3 | _.templateSettings = { 4 | evaluate : /\{\[([\s\S]+?)\]\}/g, 5 | interpolate : /\{\{([\s\S]+?)\}\}/g 6 | }; 7 | 8 | var Results = Backbone.Collection.extend({ 9 | model : Result, 10 | initialize : function(models, options) { 11 | this.query = options.query; 12 | }, 13 | url : function() { 14 | return "/search?query=" + this.query; 15 | }, 16 | parse : function(data) { 17 | this.resultsLength = data.resultsLength 18 | this.startDate = data.startDate 19 | this.endDate = data.endDate 20 | return data.results; 21 | } 22 | }); 23 | 24 | jQuery.expr[':'].between = function(a, b, c) { 25 | var args = c[3].split(','); 26 | var val = parseInt(jQuery(a).attr("id")); 27 | return val >= parseInt(args[0]) && val <= parseInt(args[1]); 28 | }; 29 | 30 | 31 | var ResultsView = Backbone.View.extend({ 32 | template : _.template($("#result_template").html()), 33 | render : function() { 34 | $('#length').html("

" + this.collection.resultsLength + " results found

") 35 | 36 | $('#slider').slider({ 37 | range: true, 38 | min: this.collection.startDate, 39 | max: this.collection.endDate, 40 | values: [1857, 2012], 41 | slide: function (event, ui) { 42 | console.log(); 43 | var selector = $(".result:between(" + ui.values[0] + "," + + ui.values[1] + "), .home-tile") 44 | $container.isotope({ filter: selector }); 45 | $(".year0").html(ui.values[0]) 46 | $(".year1").html(ui.values[1]) 47 | } 48 | }); 49 | 50 | this.collection.each(function(result) { 51 | var $output = $(this.template(result.toJSON())); 52 | var $container = $('#result_content'); 53 | $container.append( $output ).isotope( 'reloadItems' ).isotope({ sortBy: 'original-order' }); 54 | $output.imagesLoaded( function(){ 55 | $container.isotope( 'reloadItems' ).isotope({ sortBy: 'original-order' }); 56 | }); 57 | }, this); 58 | $('.modal').on('show', function (e) { 59 | }) 60 | return this; 61 | } 62 | }); 63 | 64 | var CreateSearchView = Backbone.View.extend({ 65 | initialize: function() { 66 | _.bindAll(this, 'updateQuery', 'submitSearchClicked'); 67 | }, 68 | events: { 69 | "click #submit-search": "submitSearchClicked", 70 | "keypress #search-query" : "queryKeyPress" 71 | }, 72 | submitSearchClicked: function() { 73 | this.updateQuery(); 74 | }, 75 | queryKeyPress: function(event) { 76 | if(event.keyCode == 13) { 77 | this.updateQuery(); 78 | } 79 | }, 80 | updateQuery: function() { 81 | 82 | //get the search query from input and create collection 83 | var results = new Results([], { 84 | query: this.$('#search-query').val() 85 | }); 86 | 87 | $('#result_content .result').remove() 88 | 89 | // create a view that will contain our results 90 | var resultsView = new ResultsView({ 91 | el : "#result_content", 92 | collection: results 93 | }); 94 | 95 | var targetSpinner = document.getElementById('loading'); 96 | 97 | var spinner = new Spinner({ top: '10px' }).spin(targetSpinner); 98 | // on a successful fetch, update the collection. 99 | results.fetch({ 100 | success: function(collection) { 101 | resultsView.render(); 102 | spinner.stop(); 103 | var $container = $('#result_content'); 104 | } 105 | }) 106 | } 107 | }); 108 | 109 | var createSearchView = new CreateSearchView({ 110 | el: $("#create-search"), 111 | model: Result 112 | }); -------------------------------------------------------------------------------- /src/main/webapp/static/js/custom/timelineloader.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | createStoryJS({ 3 | type: 'timeline', 4 | width: '100%', 5 | height: '600', 6 | source: 'timeline/data', 7 | embed_id: 'my-timeline' 8 | }); 9 | }); -------------------------------------------------------------------------------- /src/main/webapp/static/js/jquery.imagesloaded.min.js: -------------------------------------------------------------------------------- 1 | (function(c,n){var l="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";c.fn.imagesLoaded=function(f){function m(){var b=c(i),a=c(h);d&&(h.length?d.reject(e,b,a):d.resolve(e));c.isFunction(f)&&f.call(g,e,b,a)}function j(b,a){b.src===l||-1!==c.inArray(b,k)||(k.push(b),a?h.push(b):i.push(b),c.data(b,"imagesLoaded",{isBroken:a,src:b.src}),o&&d.notifyWith(c(b),[a,e,c(i),c(h)]),e.length===k.length&&(setTimeout(m),e.unbind(".imagesLoaded")))}var g=this,d=c.isFunction(c.Deferred)?c.Deferred(): 2 | 0,o=c.isFunction(d.notify),e=g.find("img").add(g.filter("img")),k=[],i=[],h=[];c.isPlainObject(f)&&c.each(f,function(b,a){if("callback"===b)f=a;else if(d)d[b](a)});e.length?e.bind("load.imagesLoaded error.imagesLoaded",function(b){j(b.target,"error"===b.type)}).each(function(b,a){var d=a.src,e=c.data(a,"imagesLoaded");if(e&&e.src===d)j(a,e.isBroken);else if(a.complete&&a.naturalWidth!==n)j(a,0===a.naturalWidth||0===a.naturalHeight);else if(a.readyState||a.complete)a.src=l,a.src=d}):m();return d?d.promise(g): 3 | g}})(jQuery); 4 | -------------------------------------------------------------------------------- /src/main/webapp/static/js/jquery.lazyload.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Lazy Load - jQuery plugin for lazy loading images 3 | * 4 | * Copyright (c) 2007-2012 Mika Tuupola 5 | * 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Project home: 10 | * http://www.appelsiini.net/projects/lazyload 11 | * 12 | * Version: 1.8.1-dev 13 | * 14 | */ 15 | (function(a,b){var c=a(b);a.fn.lazyload=function(d){function h(){var b=0;e.each(function(){var c=a(this);if(g.skip_invisible&&!c.is(":visible"))return;if(!a.abovethetop(this,g)&&!a.leftofbegin(this,g))if(!a.belowthefold(this,g)&&!a.rightoffold(this,g))c.trigger("appear"),b=0;else if(++b>g.failure_limit)return!1})}var e=this,f,g={threshold:0,failure_limit:0,event:"scroll",effect:"show",container:b,data_attribute:"original",skip_invisible:!0,appear:null,load:null};return d&&(undefined!==d.failurelimit&&(d.failure_limit=d.failurelimit,delete d.failurelimit),undefined!==d.effectspeed&&(d.effect_speed=d.effectspeed,delete d.effectspeed),a.extend(g,d)),f=g.container===undefined||g.container===b?c:a(g.container),0===g.event.indexOf("scroll")&&f.bind(g.event,function(a){return h()}),this.each(function(){var b=this,c=a(b);b.loaded=!1,c.one("appear",function(){if(!this.loaded){if(g.appear){var d=e.length;g.appear.call(b,d,g)}a("").bind("load",function(){c.hide().attr("src",c.data(g.data_attribute))[g.effect](g.effect_speed),b.loaded=!0;var d=a.grep(e,function(a){return!a.loaded});e=a(d);if(g.load){var f=e.length;g.load.call(b,f,g)}}).attr("src",c.data(g.data_attribute))}}),0!==g.event.indexOf("scroll")&&c.bind(g.event,function(a){b.loaded||c.trigger("appear")})}),c.bind("resize",function(a){h()}),a(document).ready(function(){h()}),this},a.belowthefold=function(d,e){var f;return e.container===undefined||e.container===b?f=c.height()+c.scrollTop():f=a(e.container).offset().top+a(e.container).height(),f<=a(d).offset().top-e.threshold},a.rightoffold=function(d,e){var f;return e.container===undefined||e.container===b?f=c.width()+c.scrollLeft():f=a(e.container).offset().left+a(e.container).width(),f<=a(d).offset().left-e.threshold},a.abovethetop=function(d,e){var f;return e.container===undefined||e.container===b?f=c.scrollTop():f=a(e.container).offset().top,f>=a(d).offset().top+e.threshold+a(d).height()},a.leftofbegin=function(d,e){var f;return e.container===undefined||e.container===b?f=c.scrollLeft():f=a(e.container).offset().left,f>=a(d).offset().left+e.threshold+a(d).width()},a.inviewport=function(b,c){return!a.rightofscreen(b,c)&&!a.leftofscreen(b,c)&&!a.belowthefold(b,c)&&!a.abovethetop(b,c)},a.extend(a.expr[":"],{"below-the-fold":function(b){return a.belowthefold(b,{threshold:0})},"above-the-top":function(b){return!a.belowthefold(b,{threshold:0})},"right-of-screen":function(b){return a.rightoffold(b,{threshold:0})},"left-of-screen":function(b){return!a.rightoffold(b,{threshold:0})},"in-viewport":function(b){return!a.inviewport(b,{threshold:0})},"above-the-fold":function(b){return!a.belowthefold(b,{threshold:0})},"right-of-fold":function(b){return a.rightoffold(b,{threshold:0})},"left-of-fold":function(b){return!a.rightoffold(b,{threshold:0})}})})(jQuery,window) 16 | -------------------------------------------------------------------------------- /src/main/webapp/static/js/spin.min.js: -------------------------------------------------------------------------------- 1 | //fgnass.github.com/spin.js#v1.2.7 2 | !function(e,t,n){function o(e,n){var r=t.createElement(e||"div"),i;for(i in n)r[i]=n[i];return r}function u(e){for(var t=1,n=arguments.length;t>1):parseInt(n.left,10)+i)+"px",top:(n.top=="auto"?a.y-u.y+(e.offsetHeight>>1):parseInt(n.top,10)+i)+"px"})),r.setAttribute("aria-role","progressbar"),t.lines(r,t.opts);if(!s){var f=0,l=n.fps,h=l/n.speed,d=(1-n.opacity)/(h*n.trail/100),v=h/n.lines;(function m(){f++;for(var e=n.lines;e;e--){var i=Math.max(1-(f+e*v)%h*d,n.opacity);t.opacity(r,n.lines-e,i,n)}t.timeout=t.el&&setTimeout(m,~~(1e3/l))})()}return t},stop:function(){var e=this.el;return e&&(clearTimeout(this.timeout),e.parentNode&&e.parentNode.removeChild(e),this.el=n),this},lines:function(e,t){function i(e,r){return c(o(),{position:"absolute",width:t.length+t.width+"px",height:t.width+"px",background:e,boxShadow:r,transformOrigin:"left",transform:"rotate("+~~(360/t.lines*n+t.rotate)+"deg) translate("+t.radius+"px"+",0)",borderRadius:(t.corners*t.width>>1)+"px"})}var n=0,r;for(;n',t)}var t=c(o("group"),{behavior:"url(#default#VML)"});!l(t,"transform")&&t.adj?(a.addRule(".spin-vml","behavior:url(#default#VML)"),v.prototype.lines=function(t,n){function s(){return c(e("group",{coordsize:i+" "+i,coordorigin:-r+" "+ -r}),{width:i,height:i})}function l(t,i,o){u(a,u(c(s(),{rotation:360/n.lines*t+"deg",left:~~i}),u(c(e("roundrect",{arcsize:n.corners}),{width:r,height:n.width,left:n.radius,top:-n.width>>1,filter:o}),e("fill",{color:n.color,opacity:n.opacity}),e("stroke",{opacity:0}))))}var r=n.length+n.width,i=2*r,o=-(n.width+n.length)*2+"px",a=c(s(),{position:"absolute",top:o,left:o}),f;if(n.shadow)for(f=1;f<=n.lines;f++)l(f,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(f=1;f<=n.lines;f++)l(f);return u(t,a)},v.prototype.opacity=function(e,t,n,r){var i=e.firstChild;r=r.shadow&&r.lines||0,i&&t+r