├── .gitignore ├── LICENSE.txt ├── README.md ├── app ├── assets │ ├── javascripts │ │ ├── admin-utils.js │ │ ├── build.js │ │ ├── common │ │ │ ├── autocomplete.js │ │ │ ├── densityGrid.js │ │ │ ├── draggable.js │ │ │ ├── formatting.js │ │ │ ├── hasEvents.js │ │ │ └── heatmapLayer.js │ │ ├── heatmap.js │ │ ├── map.js │ │ ├── page-utils.js │ │ ├── peripleo-ui.js │ │ ├── peripleo-ui │ │ │ ├── api.js │ │ │ ├── controls │ │ │ │ ├── autoSuggest.js │ │ │ │ ├── filter │ │ │ │ │ ├── facetChart.js │ │ │ │ │ ├── facetFilterParser.js │ │ │ │ │ ├── filterEditor.js │ │ │ │ │ ├── filterPanel.js │ │ │ │ │ └── timeHistogram.js │ │ │ │ ├── imageControl.js │ │ │ │ ├── resultList.js │ │ │ │ ├── searchAtButton.js │ │ │ │ ├── searchPanel.js │ │ │ │ ├── selection │ │ │ │ │ ├── selectedItem.js │ │ │ │ │ ├── selectedPlace.js │ │ │ │ │ └── selectionInfo.js │ │ │ │ ├── settings │ │ │ │ │ └── settingsEditor.js │ │ │ │ └── toolbar.js │ │ │ ├── events │ │ │ │ ├── eventBroker.js │ │ │ │ ├── events.js │ │ │ │ └── lifecycleWatcher.js │ │ │ ├── map │ │ │ │ ├── densityGrid.js │ │ │ │ ├── map.js │ │ │ │ └── objectLayer.js │ │ │ └── urlBar.js │ │ ├── place-map.js │ │ ├── place-network.js │ │ ├── placeAdjacencyNetwork.js │ │ ├── temporal-profile.js │ │ └── time-histogram.js │ └── stylesheets │ │ ├── admin │ │ └── main.less │ │ ├── base-layout.less │ │ ├── common │ │ ├── autocomplete.less │ │ ├── paginate.less │ │ ├── pagination.less │ │ └── table.less │ │ ├── datasets │ │ ├── dataset-details.less │ │ └── dataset-list.less │ │ ├── gazetteer-uri.less │ │ ├── gazetteer │ │ └── main.less │ │ ├── globals-new.less │ │ ├── globals.less │ │ ├── home │ │ └── index.less │ │ ├── items │ │ └── item-details.less │ │ ├── map-new │ │ ├── _autoComplete.less │ │ ├── _facetChart.less │ │ ├── _filterEditor.less │ │ ├── _filterPanel.less │ │ ├── _gazetteerUri.less │ │ ├── _gazetteerUriEARK.less │ │ ├── _imageControl.less │ │ ├── _modalEditor.less │ │ ├── _searchPanel.less │ │ ├── _searchResults.less │ │ ├── _selectionInfo.less │ │ ├── _settingsEditor.less │ │ ├── _timeHistogram.less │ │ ├── _toolbar.less │ │ └── main.less │ │ ├── map │ │ ├── _autocomplete.less │ │ ├── _facet-graph.less │ │ └── main.less │ │ ├── places │ │ └── place-details.less │ │ └── search │ │ ├── _filterpanel.less │ │ ├── _map.less │ │ ├── _pagination.less │ │ ├── _searchresults.less │ │ └── main.less ├── controllers │ ├── AbstractController.scala │ ├── AnnotatedThingController.scala │ ├── AnnotationController.scala │ ├── DatasetController.scala │ ├── PlaceController.scala │ ├── SearchController.scala │ ├── admin │ │ ├── AnalyticsController.scala │ │ ├── AuthController.scala │ │ ├── BaseUploadController.scala │ │ ├── DatasetAdminController.scala │ │ └── GazetteerAdminController.scala │ ├── common │ │ └── JSONWrites.scala │ ├── experimental │ │ └── ExperimentalPagesController.scala │ └── pages │ │ ├── AnnotatedThingPagesController.scala │ │ ├── DatasetPagesController.scala │ │ ├── LandingPageController.scala │ │ └── PlacePagesController.scala ├── global │ ├── Global.scala │ └── housekeeping │ │ └── AcessLogArchiver.scala ├── index │ ├── FacetTree.scala │ ├── Heatmap.scala │ ├── Index.scala │ ├── IndexFields.scala │ ├── NGramAnalyzer.scala │ ├── NumberRangePrefixTreeStrategy.java │ ├── SearchParameters.scala │ ├── TimeHistogram.scala │ ├── annotations │ │ ├── AnnotationReader.scala │ │ ├── AnnotationWriter.scala │ │ └── IndexedAnnotation.scala │ ├── objects │ │ ├── IndexedObject.scala │ │ ├── ObjectReader.scala │ │ └── ObjectWriter.scala │ ├── places │ │ ├── IndexedPlace.scala │ │ ├── IndexedPlaceNetwork.scala │ │ ├── PlaceReader.scala │ │ └── PlaceWriter.scala │ └── suggest │ │ └── SuggestIndex.scala ├── ingest │ ├── AbstractImporter.scala │ ├── CSVImporter.scala │ ├── PelagiosOAImporter.scala │ ├── TEImporter.scala │ ├── VoIDImporter.scala │ └── harvest │ │ ├── DataHarvestWorker.scala │ │ ├── DataHarvester.scala │ │ ├── GazetteerImporter.scala │ │ └── Messages.scala ├── models │ ├── AccessLog.scala │ ├── Associations.scala │ ├── HarvestLog.scala │ ├── ImportStatus.scala │ ├── Page.scala │ ├── Taxonomy.scala │ ├── adjacency │ │ ├── PlaceAdjacency.scala │ │ └── PlaceAdjacencyGraph.scala │ ├── core │ │ ├── AnnotatedThing.scala │ │ ├── Annotation.scala │ │ ├── Dataset.scala │ │ ├── Image.scala │ │ ├── Tag.scala │ │ └── TemporalProfile.scala │ └── geo │ │ ├── BoundingBox.scala │ │ ├── Gazetteer.scala │ │ ├── GazetteerReference.scala │ │ └── Hull.scala └── views │ ├── admin │ ├── accessLog.scala.html │ ├── datasets.scala.html │ ├── gazetteers.scala.html │ └── login.scala.html │ ├── annotatedThingDetails.scala.html │ ├── datasetDetails.scala.html │ ├── datasetList.scala.html │ ├── home.scala.html │ ├── landingPage.scala.html │ ├── map.scala.html │ ├── placeAdjacencyHack.scala.html │ ├── placeDetails.scala.html │ ├── showGazetteer.scala.html │ └── tags │ ├── gazetteerURI.scala.html │ └── timespan.scala.html ├── build.sbt ├── conf ├── .gitignore ├── application.conf.template └── routes ├── db └── .gitignore ├── gazetteer ├── .gitignore ├── dare-2015-1014.ttl.gz └── pleiades-201207-migrated.ttl.gz ├── lib └── concave_hull.jar ├── project ├── build.properties └── plugins.sbt ├── public ├── images │ ├── cc-by-nc.png │ ├── cc-zero.png │ ├── favicon.png │ ├── open-data-generic.png │ ├── wait-anim-fb.gif │ └── wait-circle.gif ├── javascripts │ └── lib │ │ ├── chart │ │ └── chart.min.js │ │ ├── d3.v3.min.js │ │ ├── heatmap │ │ ├── heatmap.min.js │ │ └── leaflet-heatmap.js │ │ ├── jquery-1.9.0.min.js │ │ ├── jquery-ui.min.js │ │ ├── jquery.twbsPagination.min.js │ │ ├── jquery.ui.touch-punch.min.js │ │ ├── leaflet │ │ ├── images │ │ │ ├── layers-2x.png │ │ │ ├── layers.png │ │ │ ├── marker-icon-2x.png │ │ │ ├── marker-icon-blue-2x.png │ │ │ ├── marker-icon-blue.png │ │ │ ├── marker-icon.png │ │ │ └── marker-shadow.png │ │ ├── leaflet-canvasOverlay.js │ │ ├── leaflet-heat.js │ │ ├── leaflet.css │ │ └── leaflet.js │ │ ├── moment.min.js │ │ ├── numeral.min.js │ │ ├── tinycolor.js │ │ ├── typeahead │ │ └── typeahead.jquery.min.js │ │ └── velocity.min.js └── stylesheets │ ├── Cinzel-Regular.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-Regular.ttf │ ├── fontawesome-webfont.ttf │ ├── fontello-icons.ttf │ └── peripleo.ttf ├── test-scenarios.md ├── test ├── ApplicationSpec.scala ├── IntegrationSpec.scala ├── index │ └── places │ │ └── IndexedPlaceNetworkSpec.scala ├── ingest │ └── TEIImporterSpec.scala ├── models │ └── TaxonomySpec.scala └── resources │ ├── sample-short.tei.xml │ └── sample.tei.xml └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | /.target 17 | /bin 18 | /.cache 19 | /lib/scalagios-* 20 | /index 21 | /rebuild-scalagios.sh 22 | .cache-* 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/admin-utils.js: -------------------------------------------------------------------------------- 1 | var util = util || {}; 2 | 3 | /** Enables the file upload buttons **/ 4 | util.enableUploads = function() { 5 | // Wire CSS-styled upload button to hidden file input 6 | $(document).on('click', '.upload', function(e) { 7 | var inputId = e.target.getAttribute('data-input'); 8 | $('#' + inputId).click(); 9 | }); 10 | 11 | // Set the hidden inputs to auto-submit 12 | $(document).on('change', 'input:file', function(e) { 13 | e.target.parentNode.submit(); 14 | }); 15 | } -------------------------------------------------------------------------------- /app/assets/javascripts/build.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: "/assets/javascripts", 3 | fileExclusionRegExp: /^lib$/ 4 | }); 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/common/autocomplete.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | var AutoComplete = function(form, input) { 4 | input.typeahead({ 5 | hint: true, 6 | highlight: true, 7 | minLength: 1 8 | },{ 9 | displayKey: 'key', 10 | source: function(query, callback) { 11 | jQuery.getJSON('/api-v3/new/autosuggest?q=' + query, function(results) { 12 | callback(results); 13 | }); 14 | } 15 | }); 16 | 17 | input.on('typeahead:selected', function(e) { 18 | form.submit(); 19 | }); 20 | }; 21 | 22 | return AutoComplete; 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/common/densityGrid.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | var densityGridLayer = function() { 4 | 5 | var isHidden = false, 6 | 7 | render = function(canvasOverlay, params) { 8 | if (!params.options.heatmap) 9 | return; 10 | 11 | var ctx = params.canvas.getContext('2d'); 12 | ctx.clearRect(0, 0, params.canvas.width, params.canvas.height); 13 | ctx.fillStyle = '#5254a3'; 14 | ctx.strokeStyle = '#5254a3'; 15 | 16 | // Hack! 17 | var heatmap = params.options.heatmap, 18 | weights = jQuery.map(heatmap.cells, function(val) { return val.weight; }); 19 | mean = weights.reduce(function(a, b) { return a + b; }, 0) / weights.length, 20 | maxWeight = heatmap.max_value, 21 | 22 | xyOrigin = canvasOverlay._map.latLngToContainerPoint([0, 0]), 23 | xyOneCell = canvasOverlay._map.latLngToContainerPoint([1.40625, 1.40625]), // TODO grab info from heatmap JSON 24 | cellHalfDimensions = { x: (xyOneCell.x - xyOrigin.x) / 2, y: (xyOrigin.y - xyOneCell.y) / 2 }; 25 | 26 | var classified = jQuery.map(heatmap.cells, function(val) { 27 | return { c: val, is_outlier: val.weight > 5 * mean }; 28 | }); 29 | 30 | jQuery.each(classified, function(idx, tuple) { 31 | var x = tuple.c.x, y = tuple.c.y, 32 | delta = Math.sqrt(tuple.c.weight / (5 * mean), 2), 33 | bottomLeft = canvasOverlay._map.latLngToContainerPoint([y + heatmap.cell_height / 2, x - heatmap.cell_width / 2]); 34 | topRight = canvasOverlay._map.latLngToContainerPoint([y - heatmap.cell_height / 2, x + heatmap.cell_width / 2]); 35 | width = topRight.x - bottomLeft.x, 36 | height = topRight.y - bottomLeft.y; 37 | 38 | if (tuple.is_outlier) 39 | ctx.globalAlpha = 0.9; 40 | else 41 | ctx.globalAlpha = 0.1 + delta * 0.7; 42 | ctx.fillRect(bottomLeft.x, bottomLeft.y, width, height); 43 | ctx.globalAlpha = 0.2; 44 | ctx.strokeRect(bottomLeft.x, bottomLeft.y, width, height); 45 | }); 46 | }, 47 | 48 | canvasOverlay = L.canvasOverlay().drawing(render); 49 | 50 | /** Privileged methods **/ 51 | this.update = function(heatmap) { 52 | canvasOverlay.params({ heatmap: heatmap }); 53 | canvasOverlay.redraw(); 54 | }; 55 | 56 | this.hideasdf = function() { 57 | if (!isHidden) { 58 | map.removeLayer(canvasOverlay); 59 | isHidden = true; 60 | } 61 | }; 62 | 63 | this.showasdf = function() { 64 | if (isHidden) { 65 | map.addLayer(canvasOverlay); 66 | isHidden = false; 67 | } 68 | }; 69 | 70 | this.adsaddTo = function(map) { 71 | canvasOverlay.addTo(map); 72 | return this; // Just to mimick with Leaflet's API 73 | }; 74 | 75 | }; 76 | 77 | return densityGridLayer; 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /app/assets/javascripts/common/draggable.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | /** Flag that indicates wether the device supports touch events **/ 4 | var hasTouch = ('ontouchstart' in window) || (navigator.MaxTouchPoints > 0), 5 | 6 | makeTouchXDraggable = function(element, onDrag, onStop, opt_containment) { 7 | element.bind('touchmove', function(e) { 8 | console.log(e); 9 | }); 10 | }; 11 | 12 | return { 13 | 14 | makeXDraggable: function(element, onDrag, onStop, opt_containment) { 15 | if (hasTouch) 16 | makeTouchXDraggable(element, onDrag, onStop, opt_containment); 17 | else // TODO remove jQuery dependency once we have implemented our own touch code 18 | element.draggable({ 19 | axis: 'x', 20 | containment: opt_containment, 21 | drag: onDrag, 22 | stop: onStop 23 | }); 24 | } 25 | 26 | }; 27 | 28 | /* 29 | * TODO mimic jQuery's draggable method: 30 | * 31 | * We need to constrain to X axis, provide onDrag/onStop callbacks and - possibly - a parent containment 32 | * 33 | * 34 | element.draggable({ 35 | axis: 'x', 36 | containment: opt_containment, 37 | drag: onDrag, 38 | stop: onStop 39 | }); 40 | */ 41 | 42 | /* 43 | element.bind('touchmove', function(e) { 44 | console.log(e); 45 | }); 46 | */ 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /app/assets/javascripts/common/hasEvents.js: -------------------------------------------------------------------------------- 1 | /** A generic pub-sub trait **/ 2 | define(function() { 3 | 4 | /** 5 | * A simple base class that takes care of event subcription. 6 | * @constructor 7 | */ 8 | var HasEvents = function() { 9 | this.handlers = {} 10 | } 11 | 12 | /** 13 | * Adds an event handler to this component. Refer to the docs of the components 14 | * for information about supported events. 15 | * @param {String} event the event name 16 | * @param {Function} handler the handler function 17 | */ 18 | HasEvents.prototype.on = function(event, handler) { 19 | this.handlers[event] = handler; 20 | } 21 | 22 | /** 23 | * Fires an event. 24 | * @param {String} event the event name 25 | * @param {Object} e the event object 26 | * @param {Object} args the event arguments 27 | */ 28 | HasEvents.prototype.fireEvent = function(event, e, args) { 29 | if (this.handlers[event]) 30 | this.handlers[event](e, args); 31 | } 32 | 33 | return HasEvents; 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /app/assets/javascripts/common/heatmapLayer.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | var HeatmapLayer = function() { 4 | var heatmapLayer = L.heatLayer([], { radius: 30 }); 5 | 6 | 7 | this.layer = heatmapLayer; 8 | 9 | this.update = function(heatmap) { 10 | var weights = jQuery.map(heatmap, function(val) { return val.weight; }), 11 | maxWeight = Math.max.apply(Math, weights), 12 | points = []; 13 | 14 | jQuery.each(heatmap, function(idx, val) { 15 | points.push([ val.y, val.x, val.weight ]); 16 | }); 17 | 18 | heatmapLayer.setLatLngs(points); 19 | }; 20 | 21 | }; 22 | 23 | return HeatmapLayer; 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/heatmap.js: -------------------------------------------------------------------------------- 1 | window.Heatmap = function(mapId, places) { 2 | var awmcLayer = L.tileLayer('http://a.tiles.mapbox.com/v3/isawnyu.map-knmctlkh/{z}/{x}/{y}.png', { 3 | attribution: 'Tiles and Data © 2013 AWMC ' + 4 | 'CC-BY-NC 3.0'}); 5 | 6 | this.map = new L.Map(mapId, { 7 | center: new L.LatLng(41.893588, 12.488022), 8 | zoom: 3, 9 | layers: [awmcLayer] 10 | }); 11 | 12 | var latlngs = []; 13 | $.each(places, function(idx, place) { 14 | if (place.centroid) 15 | latlngs.push([place.centroid.lat, place.centroid.lon]); 16 | }); 17 | L.heatLayer(latlngs, { max: 1.5, maxZoom: 0, radius: 5, blur: 6 }).addTo(this.map); 18 | }; 19 | 20 | window.Heatmap.prototype.refresh = function() { 21 | this.map.invalidateSize(); 22 | }; 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/map.js: -------------------------------------------------------------------------------- 1 | require(['common/autocomplete', 'common/densityGrid', 'common/timeHistogram'], function(AutoComplete, DensityGrid, TimeHistogram) { 2 | 3 | jQuery(document).ready(function() { 4 | var timeHistogram = new TimeHistogram('time-histogram', function(interval) { 5 | queryFilters.timespan = interval; 6 | update(); 7 | refreshHeatmap(); 8 | }), 9 | 10 | autoComplete = new AutoComplete(searchForm, searchInput); 11 | 12 | /** Helper to parse the source facet label **/ 13 | parseSourceFacetLabel = function(labelAndId) { 14 | var separatorIdx = labelAndId.indexOf('#'), 15 | label = labelAndId.substring(0, separatorIdx), 16 | id = labelAndId.substring(separatorIdx); 17 | 18 | return { label: label, id: id }; 19 | }, 20 | 21 | search = function() { 22 | queryFilters.query = searchInput.val(); 23 | searchInput.blur(); 24 | update(); 25 | refreshHeatmap(); 26 | return false; // preventDefault + stopPropagation 27 | }; 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/page-utils.js: -------------------------------------------------------------------------------- 1 | var util = util || {}; 2 | 3 | /** Loops through all elements with CSS class .number and formats using numeral.js **/ 4 | util.formatNumbers = function(opt_parent) { 5 | var elements = (opt_parent) ? $(opt_parent).find('.number') : $('.number'); 6 | $.each(elements, function(idx, el) { 7 | var formatted = numeral($(el).text()).format('0,0'); 8 | $(el).html(formatted); 9 | }); 10 | }; 11 | 12 | /** Renders an image icon corresponding to a specific license URL **/ 13 | util.licenseIcon = function(url) { 14 | if (url.indexOf('http://opendatacommons.org/licenses/odbl') == 0) { 15 | return ''; 16 | } else if (url.indexOf('http://creativecommons.org/publicdomain/zero/1.0') == 0) { 17 | return ''; 18 | } else if (url.indexOf('http://creativecommons.org/licenses/by-nc') == 0) { 19 | return ''; 20 | } else { 21 | return '' + url + ''; 22 | } 23 | }; 24 | 25 | util.formatGazetteerURI = function(uri) { 26 | if (uri.indexOf('http://pleiades.stoa.org/places/') > -1) { 27 | return 'pleiades:' + uri.substr(32); 28 | } else if (uri.indexOf('http://dare.ht.lu.se/places/') > -1) { 29 | return 'dare:' + uri.substr(28); 30 | } else if (uri.indexOf('http://gazetteer.dainst.org/place/') > -1) { 31 | return 'dai:' + uri.substr(34); 32 | } else if (uri.indexOf('http://sws.geonames.org/') > -1) { 33 | return 'geonames:' + uri.substr(24); 34 | } else if (uri.indexOf('http://vici.org/vici') > -1) { 35 | return 'vici:' + uri.substr(21); 36 | } else if (uri.indexOf('http://www.trismegistos.org/place/') > -1) { 37 | return 'trismegistos:' + uri.substr(34); 38 | } else if (uri.indexOf('http://nomisma.org/') > -1) { 39 | return 'nomisma:' + uri.substr(22); 40 | } else if (uri.indexOf('http://data.pastplace.org') > -1) { 41 | return 'pastplace:' + uri.substr(35); 42 | } else if (uri.indexOf('http://www.wikidata.org/entity') > -1) { 43 | return 'wikidata:' + uri.substr(32); 44 | } else { 45 | return uri; 46 | } 47 | }; 48 | 49 | /** From http://www.samaxes.com/2011/09/change-url-parameters-with-jquery/ **/ 50 | util.buildPageRequestURL = function(offset, limit) { 51 | var queryParameters = {}, 52 | queryString = location.search.substring(1).replace(/\+/g, ' '), 53 | re = /([^&=]+)=([^&]*)/g, 54 | m; 55 | 56 | while (m = re.exec(queryString)) { 57 | queryParameters[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); 58 | } 59 | 60 | queryParameters['offset'] = offset; 61 | queryParameters['limit'] = limit; 62 | 63 | return $.param(queryParameters); 64 | }; 65 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui.js: -------------------------------------------------------------------------------- 1 | require(['peripleo-ui/controls/settings/settingsEditor', 2 | 'peripleo-ui/controls/resultList', 3 | 'peripleo-ui/controls/searchPanel', 4 | 'peripleo-ui/controls/toolbar', 5 | 'peripleo-ui/events/events', 6 | 'peripleo-ui/events/eventBroker', 7 | 'peripleo-ui/events/lifecycleWatcher', 8 | 'peripleo-ui/map/map', 9 | 'peripleo-ui/api', 10 | 'peripleo-ui/urlBar'], function(SettingsEditor, ResultList, SearchPanel, Toolbar, Events, EventBroker, LifeCycleWatcher, Map, API, URLBar) { 11 | 12 | jQuery(document).ready(function() { 13 | /** DOM element shorthands **/ 14 | var mapDIV = document.getElementById('map'), 15 | controlsDIV = jQuery('#controls'), 16 | toolbarDIV = jQuery('#toolbar'), 17 | 18 | /** Top-level components **/ 19 | eventBroker = new EventBroker(), 20 | lifeCycleWatcher = new LifeCycleWatcher(eventBroker), 21 | urlBar = new URLBar(eventBroker), 22 | api = new API(eventBroker), 23 | map = new Map(mapDIV, eventBroker), 24 | toolbar = new Toolbar(toolbarDIV, eventBroker), 25 | searchPanel = new SearchPanel(controlsDIV, eventBroker), 26 | resultList = new ResultList(controlsDIV, eventBroker), 27 | settingsEditor = new SettingsEditor(eventBroker), 28 | 29 | /** Resolve the URL bar **/ 30 | initialSettings = urlBar.parseURLHash(window.location.hash), 31 | 32 | /** Is there an initially selected place? Fetch it now! **/ 33 | fetchInitiallySelectedPlace = function(encodedURL, zoomTo) { 34 | jQuery.getJSON('/peripleo/places/' + encodedURL, function(response) { 35 | eventBroker.fireEvent(Events.SELECT_RESULT, [ response ]); 36 | if (zoomTo) 37 | map.zoomTo(response.geo_bounds); 38 | }); 39 | }, 40 | 41 | /** Is there an initial query phrase? Fetch the results now **/ 42 | runInitialQuery = function(query) { 43 | eventBroker.fireEvent(Events.SEARCH_CHANGED, { query: query }); 44 | }, 45 | 46 | /** Initializes map center and zoom **/ 47 | initializeMap = function(at) { 48 | map.setView([ at.lat, at.lng ], at.zoom); 49 | }; 50 | 51 | eventBroker.fireEvent(Events.LOAD, initialSettings); 52 | 53 | if (initialSettings.at) 54 | initializeMap(initialSettings.at); 55 | 56 | if (initialSettings.query) 57 | runInitialQuery(initialSettings.query); 58 | 59 | if (initialSettings.places) 60 | fetchInitiallySelectedPlace(initialSettings.places, !initialSettings.hasOwnProperty('at')); 61 | 62 | if (initialSettings.ex) 63 | eventBroker.fireEvent(Events.START_EXPLORATION); 64 | 65 | delete initialSettings.at; 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/controls/autoSuggest.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | var AutoSuggest = function(form, input) { 4 | input.typeahead({ 5 | hint: false, 6 | highlight: true, 7 | minLength: 1 8 | },{ 9 | displayKey: 'val', 10 | source: function(query, callback) { 11 | jQuery.getJSON('/peripleo/autocomplete?q=' + query, function(results) { 12 | callback(results); 13 | }); 14 | } 15 | }); 16 | 17 | input.on('typeahead:selected', function(e) { 18 | form.submit(); 19 | }); 20 | 21 | 22 | this.clear = function() { 23 | input.typeahead('val',''); 24 | }; 25 | }; 26 | 27 | return AutoSuggest; 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/controls/selection/selectedItem.js: -------------------------------------------------------------------------------- 1 | define(['common/formatting', 2 | 'peripleo-ui/controls/selection/selectionInfo', 3 | 'peripleo-ui/events/events'], function(Formatting, SelectionInfo, Events) { 4 | 5 | var SLIDE_DURATION = 180; 6 | 7 | var SelectedItem = function(container, eventBroker) { 8 | var self = this, 9 | 10 | content = jQuery( 11 | '
' + 12 | '

' + 13 | '

' + 14 | ' ' + 15 | ' ' + 16 | '

' + 17 | '

' + 18 | '

' + 19 | '

' + 20 | '
'), 21 | 22 | /** DOM element shorthands **/ 23 | heading = content.find('h3'), 24 | tempBounds = content.find('.temp-bounds'), 25 | names = content.find('.names'), 26 | description = content.find('.description'), 27 | homepage = content.find('.homepage'), 28 | snippets = content.find('.snippets'), 29 | 30 | /** Clears the contents **/ 31 | clearContent = function() { 32 | heading.empty(); 33 | tempBounds.empty(); 34 | tempBounds.hide(); 35 | names.empty(); 36 | description.empty(); 37 | homepage.empty(); 38 | snippets.empty(); 39 | }, 40 | 41 | /** Fills the content **/ 42 | fill = function(obj) { 43 | heading.html(obj.title); 44 | 45 | if (obj.temporal_bounds) { 46 | if (obj.temporal_bounds.start === obj.temporal_bounds.end) 47 | tempBounds.html(Formatting.formatYear(obj.temporal_bounds.start)); 48 | else 49 | tempBounds.html(Formatting.formatYear(obj.temporal_bounds.start) + ' - ' + Formatting.formatYear(obj.temporal_bounds.end)); 50 | tempBounds.show(); 51 | } 52 | 53 | if (obj.description) 54 | description.html(obj.description); 55 | 56 | if (obj.homepage) 57 | homepage.append(Formatting.formatSourceURL(obj.homepage)); 58 | 59 | if (obj.snippet) 60 | snippets.append(obj.snippet); 61 | }; 62 | 63 | container.append(content); 64 | SelectionInfo.apply(this, [ container, eventBroker, fill, clearContent ]); 65 | 66 | eventBroker.addHandler(Events.SELECTION, function(results) { 67 | var firstResultType = (results) ? results[0].object_type : false; 68 | if (firstResultType === 'Item') 69 | self.show(results[0]); 70 | else 71 | self.hide(); 72 | }); 73 | }; 74 | 75 | SelectedItem.prototype = Object.create(SelectionInfo.prototype); 76 | 77 | return SelectedItem; 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/controls/selection/selectedPlace.js: -------------------------------------------------------------------------------- 1 | define(['common/formatting', 2 | 'peripleo-ui/controls/selection/selectionInfo', 3 | 'peripleo-ui/events/events'], function(Formatting, SelectionInfo, Events) { 4 | 5 | var SLIDE_DURATION = 180; 6 | 7 | var SelectedPlace = function(container, eventBroker) { 8 | var self = this, 9 | 10 | content = jQuery( 11 | '
' + 12 | '

' + 13 | '

' + 14 | ' ' + 15 | ' ' + 16 | '

' + 17 | '

' + 18 | '

' + 19 | ' ' + 20 | '
'), 21 | 22 | /** DOM element shorthands **/ 23 | heading = content.find('h3'), 24 | tempBounds = content.find('.temp-bounds'), 25 | names = content.find('.names'), 26 | description = content.find('.description'), 27 | uris = content.find('.uris'), 28 | 29 | /** Clears the contents **/ 30 | clearContent = function() { 31 | heading.empty(); 32 | tempBounds.empty(); 33 | tempBounds.hide(); 34 | names.empty(); 35 | description.empty(); 36 | uris.empty(); 37 | }, 38 | 39 | /** Fills the content **/ 40 | fill = function(obj) { 41 | heading.html(obj.title); 42 | 43 | if (obj.temporal_bounds) { 44 | if (obj.temporal_bounds.start === obj.temporal_bounds.end) 45 | tempBounds.html(Formatting.formatYear(obj.temporal_bounds.start)); 46 | else 47 | tempBounds.html(Formatting.formatYear(obj.temporal_bounds.start) + ' - ' + Formatting.formatYear(obj.temporal_bounds.end)); 48 | tempBounds.show(); 49 | } 50 | 51 | if (obj.names) 52 | names.html(obj.names.slice(0, 8).join(', ')); 53 | 54 | if (obj.description) 55 | description.html(obj.description); 56 | 57 | uris.append(jQuery('
  • ' + Formatting.formatGazetteerURI(obj.identifier) + '
  • ')); 58 | if (obj.matches) 59 | jQuery.each(obj.matches, function(idx, uri) { 60 | uris.append(jQuery('
  • ' + Formatting.formatGazetteerURI(uri) + '
  • ')); 61 | }); 62 | }; 63 | 64 | container.append(content); 65 | SelectionInfo.apply(this, [ container, eventBroker, fill, clearContent ]); 66 | 67 | eventBroker.addHandler(Events.SELECT_MARKER, self.show); 68 | eventBroker.addHandler(Events.SELECT_RESULT, function(results) { 69 | var firstResultType = (results) ? results[0].object_type : false; 70 | if (firstResultType === 'Place') 71 | self.show(results[0]); 72 | }); 73 | }; 74 | 75 | SelectedPlace.prototype = Object.create(SelectionInfo.prototype); 76 | 77 | return SelectedPlace; 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/controls/selection/selectionInfo.js: -------------------------------------------------------------------------------- 1 | /** Common code for SelectedPlace and SelectedItem boxes **/ 2 | define(function() { 3 | 4 | var SLIDE_DURATION = 180; 5 | 6 | var SelectionInfo = function(container, eventBroker, fill, clearContent) { 7 | 8 | var currentObject = false, 9 | 10 | slideDown = function() { 11 | container.velocity('slideDown', { duration: SLIDE_DURATION }); 12 | }, 13 | 14 | slideUp = function(opt_complete) { 15 | container.velocity('slideUp', { 16 | duration: SLIDE_DURATION, 17 | complete: function() { 18 | clearContent(); 19 | if (opt_complete) 20 | opt_complete(); 21 | } 22 | }); 23 | }, 24 | 25 | hide = function() { 26 | currentObject = false; 27 | slideUp(); 28 | }, 29 | 30 | show = function(objects) { 31 | // TODO support display of lists of objects, rather than just single one 32 | var obj = (jQuery.isArray(objects)) ? objects[0] : objects, 33 | currentType = (currentObject) ? currentObject.object_type : false; 34 | 35 | if (currentObject) { // Box is currently open 36 | if (!obj) { // Close it 37 | hide(); 38 | } else { 39 | if (currentObject.identifier !== obj.identifier) { // New object - change 40 | currentObject = obj; 41 | slideUp(function() { 42 | fill(obj); 43 | slideDown(); 44 | }); 45 | } 46 | } 47 | } else { // Currently closed 48 | if (obj) { // Open it 49 | currentObject = obj; 50 | fill(obj); 51 | slideDown(); 52 | } 53 | } 54 | }; 55 | 56 | this.show = show; 57 | this.hide = hide; 58 | container.hide(); 59 | }; 60 | 61 | return SelectionInfo; 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/controls/toolbar.js: -------------------------------------------------------------------------------- 1 | define(['peripleo-ui/events/events'], function(Events) { 2 | 3 | var Toolbar = function(container, eventBroker) { 4 | var btnHelp = 5 | jQuery('
    '), 6 | 7 | btnSettings = 8 | jQuery('
    '), 9 | 10 | btnZoom = jQuery( 11 | '
    ' + 12 | '
    ' + 13 | '
    ' + 14 | '
    '), 15 | 16 | btnZoomIn = btnZoom.find('#toolbar-zoom-in'), 17 | btnZoomOut = btnZoom.find('#toolbar-zoom-out'); 18 | 19 | btnSettings.click(function() { eventBroker.fireEvent(Events.EDIT_MAP_SETTINGS); }); 20 | btnZoomIn.click(function() { eventBroker.fireEvent(Events.ZOOM_IN); }); 21 | btnZoomOut.click(function() { eventBroker.fireEvent(Events.ZOOM_OUT); }); 22 | 23 | container.append(btnHelp); 24 | container.append(btnSettings); 25 | container.append(btnZoom); 26 | 27 | }; 28 | 29 | return Toolbar; 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/events/eventBroker.js: -------------------------------------------------------------------------------- 1 | /** A generic event broker implementation **/ 2 | define(function() { 3 | 4 | var _handlers = []; 5 | 6 | /** A central event broker for communication between UI components **/ 7 | var EventBroker = function(events) { 8 | 9 | this.events = events; 10 | 11 | }; 12 | 13 | /** Adds an event handler **/ 14 | EventBroker.prototype.addHandler = function(type, handler) { 15 | if (type) { 16 | if (!_handlers[type]) 17 | _handlers[type] = []; 18 | 19 | _handlers[type].push(handler); 20 | } else { 21 | throw('Event type is undefined'); 22 | } 23 | }; 24 | 25 | /** Removes an event handler **/ 26 | EventBroker.prototype.removeHandler = function(type, handler) { 27 | var handlers = _handlers[type]; 28 | if (handlers) { 29 | var idx = handlers.indexOf(handler); 30 | handlers.splice(idx, 1); 31 | } 32 | }; 33 | 34 | /** Fires an event **/ 35 | EventBroker.prototype.fireEvent = function(type, opt_event) { 36 | if (!type) 37 | throw('Event type is undefined'); 38 | 39 | var handlers = _handlers[type]; 40 | if (handlers) { 41 | jQuery.each(handlers, function(idx, handler) { 42 | handler(opt_event); 43 | }); 44 | } 45 | } 46 | 47 | return EventBroker; 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/events/lifecycleWatcher.js: -------------------------------------------------------------------------------- 1 | /** A helper that manages 'derivative events' in the Peripleo UI lifecycle **/ 2 | define(['peripleo-ui/events/events'], function(Events) { 3 | 4 | var LifecycleWatcher = function(eventBroker) { 5 | 6 | /** Flag recording current search mode state **/ 7 | var isStateSubsearch = false; 8 | 9 | /** SELECT_MARKER & SELECT_RESULT trigger parent SELECTION **/ 10 | eventBroker.addHandler(Events.SELECT_MARKER, function(selection) { 11 | eventBroker.fireEvent(Events.SELECTION, selection); 12 | }); 13 | 14 | eventBroker.addHandler(Events.SELECT_RESULT, function(results) { 15 | eventBroker.fireEvent(Events.SELECTION, results); 16 | }); 17 | 18 | /** 19 | * When in subsearch state, selection of a new place or place de-selection 20 | * triggers TO_STATE_SEARCH 21 | */ 22 | eventBroker.addHandler(Events.TO_STATE_SUB_SEARCH, function() { 23 | isStateSubsearch = true; 24 | }); 25 | 26 | eventBroker.addHandler(Events.SELECT_MARKER, function(places) { 27 | eventBroker.fireEvent(Events.TO_STATE_SEARCH); 28 | }); 29 | 30 | }; 31 | 32 | return LifecycleWatcher; 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /app/assets/javascripts/peripleo-ui/map/densityGrid.js: -------------------------------------------------------------------------------- 1 | /** Pelagios' 'Density Grid' layer for Leaflet **/ 2 | define(['peripleo-ui/events/events'], function(Events) { 3 | 4 | var DensityGrid = function(map, eventBroker) { 5 | 6 | var render = function(canvasOverlay, params) { 7 | var ctx = params.canvas.getContext('2d'); 8 | ctx.clearRect(0, 0, params.canvas.width, params.canvas.height); 9 | 10 | if (!params.options.heatmap) 11 | return; 12 | 13 | ctx.fillStyle = '#5254a3'; 14 | ctx.strokeStyle = '#5254a3'; 15 | 16 | // Hack! 17 | var heatmap = params.options.heatmap, 18 | weights = jQuery.map(heatmap.cells, function(val) { return val.weight; }); 19 | mean = weights.reduce(function(a, b) { return a + b; }, 0) / weights.length, 20 | maxWeight = heatmap.max_value, 21 | 22 | xyOrigin = canvasOverlay._map.latLngToContainerPoint([0, 0]), 23 | xyOneCell = canvasOverlay._map.latLngToContainerPoint([1.40625, 1.40625]), // TODO grab info from heatmap JSON 24 | cellHalfDimensions = { x: (xyOneCell.x - xyOrigin.x) / 2, y: (xyOrigin.y - xyOneCell.y) / 2 }; 25 | 26 | var classified = jQuery.map(heatmap.cells, function(val) { 27 | return { c: val, is_outlier: val.weight > 5 * mean }; 28 | }); 29 | 30 | jQuery.each(classified, function(idx, tuple) { 31 | var x = tuple.c.x, y = tuple.c.y, 32 | delta = Math.sqrt(tuple.c.weight / (5 * mean), 2), 33 | bottomLeft = canvasOverlay._map.latLngToContainerPoint([y + heatmap.cell_height / 2, x - heatmap.cell_width / 2]), 34 | topRight = canvasOverlay._map.latLngToContainerPoint([y - heatmap.cell_height / 2, x + heatmap.cell_width / 2]), 35 | width = topRight.x - bottomLeft.x, 36 | height = topRight.y - bottomLeft.y; 37 | 38 | if (tuple.is_outlier) { 39 | ctx.globalAlpha = 1; 40 | } else { 41 | ctx.globalAlpha = 0.3 + delta * 0.5; 42 | } 43 | 44 | ctx.fillRect(bottomLeft.x, bottomLeft.y, width, height); 45 | ctx.globalAlpha = 0.2; 46 | ctx.strokeRect(bottomLeft.x, bottomLeft.y, width, height); 47 | }); 48 | }, 49 | 50 | canvasOverlay = L.canvasOverlay().drawing(render); 51 | 52 | eventBroker.addHandler(Events.TOGGLE_HEATMAP, function(args) { 53 | if (args.enabled) { 54 | canvasOverlay.addTo(map); 55 | } else { 56 | canvasOverlay.params({ heatmap: false }); 57 | canvasOverlay.redraw(); 58 | map.removeLayer(canvasOverlay); 59 | } 60 | }); 61 | 62 | eventBroker.addHandler(Events.API_VIEW_UPDATE, function(response) { 63 | if (response.heatmap) { 64 | canvasOverlay.params({ heatmap: response.heatmap }); 65 | window.setTimeout(function() { 66 | canvasOverlay.redraw(); 67 | }, 1); 68 | } 69 | }); 70 | }; 71 | 72 | return DensityGrid; 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /app/assets/javascripts/place-map.js: -------------------------------------------------------------------------------- 1 | window.PlaceMap = function(mapId, coord) { 2 | var awmcLayer = L.tileLayer('http://a.tiles.mapbox.com/v3/isawnyu.map-knmctlkh/{z}/{x}/{y}.png', { 3 | attribution: 'Tiles and Data © 2013 AWMC ' + 4 | 'CC-BY-NC 3.0'}); 5 | 6 | var map = new L.Map(mapId, { 7 | center: coord, 8 | zoom: 7, 9 | layers: [awmcLayer] 10 | }); 11 | 12 | L.marker(coord).addTo(map); 13 | }; 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/place-network.js: -------------------------------------------------------------------------------- 1 | window.PlaceNetwork = function(divId, nodes, edges) { 2 | var div = $('#' + divId), 3 | width = div.width(), 4 | height = div.height(), 5 | innerNodes = $.grep(nodes, function(n) { 6 | return n.is_inner_node; 7 | }), 8 | innerEdges = $.grep(edges, function(e) { 9 | return e.is_inner_edge; 10 | }); 11 | 12 | var force = d3.layout.force() 13 | .charge(-300) 14 | .linkDistance(80) 15 | .size([width, height]) 16 | .nodes(nodes) 17 | .links(edges) 18 | .on('tick', function() { 19 | link 20 | .attr('x1', function(d) { return d.source.x; }) 21 | .attr('y1', function(d) { return d.source.y; }) 22 | .attr('x2', function(d) { return d.target.x; }) 23 | .attr('y2', function(d) { return d.target.y; }); 24 | 25 | node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')' }); 26 | }); 27 | 28 | var svg = d3.select('#' + divId).append('svg') 29 | .attr('width', width) 30 | .attr('height', height); 31 | 32 | svg.append('defs').selectAll('marker') 33 | .data(['end']) 34 | .enter().append('marker') 35 | .attr('id', String) 36 | .attr('viewBox', '0 -5 10 10') 37 | .attr('refX', 16) 38 | .attr('refY', 0) 39 | .attr('markerWidth', 7) 40 | .attr('markerHeight', 9) 41 | .attr('orient', 'auto') 42 | .append('path') 43 | .attr('d', 'M0,-5L10,0L0,5'); 44 | 45 | var link = svg.selectAll('.link') 46 | .data(innerEdges) 47 | .enter().append('line') 48 | .attr('class', function(d) { 49 | var t = nodes[d.target]; 50 | if (t.label) 51 | return 'link' 52 | else 53 | return 'link virtual'; 54 | }) 55 | .attr('marker-end', 'url(#end)') 56 | 57 | var node = svg.selectAll('.node') 58 | .data(innerNodes) 59 | .enter().append('g') 60 | .attr('class', 'node') 61 | .call(force.drag); 62 | 63 | node.append('circle') 64 | .attr('r', 6) 65 | .attr('class', function(d) { 66 | if (d.source_gazetteer) 67 | return d.source_gazetteer.toLowerCase(); 68 | else 69 | return 'virtual'; 70 | }); 71 | 72 | node.append('title') 73 | .text(function(d) { return (d.title) ? d.title : d.uri; }); 74 | 75 | node.append('text') 76 | .attr('x', 12) 77 | .attr('dy', '.35em') 78 | .text(function(d) { return util.formatGazetteerURI(d.uri); }); 79 | 80 | force.start(); 81 | } 82 | -------------------------------------------------------------------------------- /app/assets/javascripts/placeAdjacencyNetwork.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | jQuery(document).ready(function() { 4 | var div = $('#graph'), 5 | width = div.width(), 6 | height = div.height(), 7 | 8 | relatedLinks = jQuery.grep($('link'), function(element) { return $(element).attr('rel') == 'related'; }), 9 | dataURL = $(relatedLinks[0]).attr('href'), 10 | 11 | force = d3.layout.force() 12 | .charge(-100) 13 | .linkDistance(80) 14 | .size([width, height]), 15 | 16 | zoom = d3.behavior.zoom() 17 | .scaleExtent([1, 10]) 18 | .on('zoom', function() { 19 | svg.attr('transform', 'translate(' + d3.event.translate + ') scale(' + d3.event.scale + ')'); }), 20 | 21 | svg = d3.select('#graph').append('svg') 22 | .attr('width', width) 23 | .attr('height', height) 24 | .call(zoom) 25 | .append('g'), 26 | 27 | renderGraph = function(nodes, edges) { 28 | var edge, node; 29 | 30 | force 31 | .nodes(nodes) 32 | .links(edges) 33 | .start(); 34 | 35 | edge = svg.selectAll('.link') 36 | .data(edges) 37 | .enter().append('line') 38 | .attr('class', 'link') 39 | .style('stroke-width', function(d) { return 1.5 * Math.sqrt(d.weight); }); 40 | 41 | node = svg.selectAll('.node') 42 | .data(nodes) 43 | .enter().append('g') 44 | .attr('class', 'node') 45 | 46 | .call(force.drag); 47 | 48 | node.append('circle') 49 | .attr('r', function(d) { 50 | return Math.max(6, 3 * Math.sqrt(d.weight)); 51 | }); 52 | 53 | node.append('title').text(function(d) { return d.title; }); 54 | node.append('text') 55 | .attr('x', 12) 56 | .attr('dy', '.35em') 57 | .text(function(d) { return d.title }); 58 | 59 | force.on('tick', function() { 60 | edge 61 | .attr('x1', function(d) { return d.source.x; }) 62 | .attr('y1', function(d) { return d.source.y; }) 63 | .attr('x2', function(d) { return d.target.x; }) 64 | .attr('y2', function(d) { return d.target.y; }); 65 | 66 | node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')' }); 67 | }); 68 | 69 | }; 70 | 71 | loadGraphData = function(url, callback) { 72 | jQuery.getJSON(url, function(data) { 73 | callback(data.nodes, data.links); 74 | }); 75 | }; 76 | 77 | loadGraphData(dataURL, renderGraph); 78 | }); 79 | 80 | 81 | 82 | 83 | 84 | /* 85 | svg.append('defs').selectAll('marker') 86 | .data(['end']) 87 | .enter().append('marker') 88 | .attr('id', String) 89 | .attr('viewBox', '0 -5 10 10') 90 | .attr('refX', 16) 91 | .attr('refY', 0) 92 | .attr('markerWidth', 7) 93 | .attr('markerHeight', 9) 94 | .attr('orient', 'auto') 95 | .append('path') 96 | .attr('d', 'M0,-5L10,0L0,5'); 97 | 98 | 99 | 100 | node.append('circle') 101 | .attr('r', 6) 102 | .attr('class', function(d) { 103 | if (d.source_gazetteer) 104 | return d.source_gazetteer.toLowerCase(); 105 | else 106 | return 'virtual'; 107 | }); 108 | 109 | node.append('title') 110 | .text(function(d) { return (d.title) ? d.title : d.uri; }); 111 | 112 | node.append('text') 113 | .attr('x', 12) 114 | .attr('dy', '.35em') 115 | .text(function(d) { return util.formatGazetteerURI(d.uri); }); 116 | 117 | force.start(); 118 | */ 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /app/assets/javascripts/temporal-profile.js: -------------------------------------------------------------------------------- 1 | window.TemporalProfile = function(divId, data) { 2 | var div = $('#' + divId), 3 | width = div.width(), 4 | height = div.height(), 5 | canvas = $(''), 6 | ctx = canvas[0].getContext('2d'), 7 | intervalStart = data.bounds_start, 8 | intervalEnd = data.bounds_end, 9 | intervalSize = intervalEnd - intervalStart, 10 | bucketSize = Math.ceil(intervalSize / width); 11 | 12 | div.append(canvas); 13 | ctx.fillStyle = '#5483bd'; 14 | 15 | var currentYear = data.bounds_start, 16 | buckets = [], 17 | maxValue = 0; 18 | 19 | while (currentYear <= intervalEnd) { 20 | // Aggregate multiple years into buckets, according to histogram screensize 21 | var bucketValue = 0; 22 | 23 | for (var i = 0; i < bucketSize; i++) { 24 | var currentValue = data.histogram[currentYear] 25 | if (currentValue) 26 | bucketValue += currentValue; 27 | 28 | currentYear++; 29 | } 30 | 31 | if (bucketValue > maxValue) 32 | maxValue = bucketValue; 33 | buckets.push(bucketValue); 34 | } 35 | 36 | $.each(buckets, function(offset, value) { 37 | var h = value * height * 0.9 / maxValue; 38 | ctx.fillRect(offset, height - h, 1, h); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /app/assets/javascripts/time-histogram.js: -------------------------------------------------------------------------------- 1 | window.TimeHistogram = (function() { 2 | 3 | var PADDING = 10, 4 | BAR_SPACING = 2, 5 | BASELINE_COLOR = '#ccc'; 6 | BAR_COLOR = '#99ccff'; 7 | 8 | var TimeHistogram = function(div, data, steps) { 9 | var minYear = 9007199254740992, 10 | maxYear = -9007199254740992, 11 | 12 | bars, 13 | maxBarValue = 0, 14 | 15 | el = jQuery(div), 16 | canvas, 17 | 18 | init = function() { 19 | var year, value; 20 | for (key in data) { 21 | year = parseInt(key); 22 | 23 | if (year < minYear) 24 | minYear = year; 25 | 26 | if (year > maxYear) 27 | maxYear = year; 28 | } 29 | }, 30 | 31 | toLabel = function(year) { 32 | if (year < 0) { 33 | return Math.abs(year) + 'BC'; 34 | } else { 35 | return year + 'AD'; 36 | } 37 | }, 38 | 39 | getSum = function(from, to) { 40 | var i, value, sum = 0; 41 | for (i = from; i <= to; i++) { 42 | value = data[i]; 43 | if (value) 44 | sum += value; 45 | } 46 | return sum 47 | }, 48 | 49 | computeBars = function(from, to, steps) { 50 | var stepWidth = (steps) ? (to - from) / steps : (to - from) / 25, 51 | sum, year = from, _bars = []; 52 | 53 | while (year < to) { 54 | sum = getSum(Math.floor(year), Math.floor(year + stepWidth)) 55 | if (sum > maxBarValue) 56 | maxBarValue = sum; 57 | 58 | _bars.push(sum); 59 | year += stepWidth; 60 | } 61 | 62 | return _bars; 63 | }, 64 | 65 | draw = function(canvas) { 66 | var idx, barHeight, xOffset, yOffset, 67 | ctx = canvas[0].getContext('2d'); 68 | width = canvas.width(), 69 | height = canvas.height(), 70 | maxHeight = height / 2 - PADDING, 71 | yIncrement = maxHeight / maxBarValue, 72 | barWidth = ((width - 2 * PADDING) / bars.length) - BAR_SPACING; 73 | 74 | // Bars 75 | xOffset = PADDING + BAR_SPACING / 2; 76 | for (idx in bars) { 77 | barHeight = bars[idx] * yIncrement; 78 | yOffset = height / 2 - barHeight; 79 | 80 | ctx.fillStyle = BAR_COLOR; 81 | ctx.beginPath(); 82 | ctx.rect(xOffset, yOffset, barWidth, barHeight * 2); 83 | ctx.fill(); 84 | 85 | xOffset += barWidth + BAR_SPACING; 86 | } 87 | 88 | // Baseline 89 | ctx.strokeStyle = BASELINE_COLOR; 90 | ctx.lineWidth = 1; 91 | ctx.beginPath(); 92 | ctx.moveTo(PADDING, Math.round(height / 2) - 0.5); 93 | ctx.lineTo(width - PADDING, Math.round(height / 2) - 0.5); 94 | ctx.stroke(); 95 | }; 96 | 97 | init(); 98 | 99 | if (minYear < 9007199254740992 && maxYear > -9007199254740992) { 100 | bars = computeBars(minYear, maxYear, steps); 101 | 102 | canvas = jQuery('') 103 | .attr('width', el.width()) 104 | .attr('height', el.height()); 105 | 106 | el.append(canvas); 107 | draw(canvas); 108 | 109 | this.minYear = toLabel(minYear); 110 | this.maxYear = toLabel(maxYear); 111 | } 112 | }; 113 | 114 | return TimeHistogram; 115 | 116 | })(); 117 | -------------------------------------------------------------------------------- /app/assets/stylesheets/admin/main.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../common/table.less"; 3 | 4 | input[type=file] { 5 | display:none; 6 | } 7 | 8 | #content { 9 | padding:20px 30px; 10 | 11 | .error { 12 | background-color: #f2dede; 13 | color: #a94442; 14 | padding:15px; 15 | border-left:3px solid #a94442; 16 | 17 | p { 18 | padding:8px 0 0 0; 19 | margin:0; 20 | } 21 | 22 | } 23 | 24 | .success { 25 | background-color: #dff0d8; 26 | color: #3c763d; 27 | padding:15px; 28 | border-left:3px solid #3c763d; 29 | } 30 | 31 | #admin-actions { 32 | margin:20px 0; 33 | padding:20px; 34 | background-color:#fcf8f2; 35 | border-left:3px solid #f0ad4e; 36 | 37 | h4 { 38 | font-size:18px; 39 | display:inline; 40 | color:#f0ad4e; 41 | } 42 | 43 | ul { 44 | position:relative; 45 | bottom:1px; 46 | display:inline; 47 | list-style:none; 48 | margin-left:20px; 49 | padding-left:0; 50 | } 51 | 52 | li { 53 | display:inline; 54 | padding:0 8px 8px 0; 55 | text-indent:0; 56 | color:#f0ad4e; 57 | } 58 | 59 | li a { 60 | color:#f0ad4e; 61 | font-size:13px; 62 | } 63 | 64 | li:before { 65 | font-family:"Font Awesome"; 66 | content:'\f0fe'; 67 | padding:0 10px 0 5px; 68 | } 69 | 70 | button { 71 | margin-right:20px; 72 | } 73 | 74 | } /** #admin-actions **/ 75 | 76 | table.list { 77 | width:100%; 78 | 79 | td { 80 | 81 | form { display:none; } 82 | 83 | } /** td **/ 84 | 85 | td.center, th.center { 86 | text-align:center; 87 | } 88 | 89 | .icon.plus { cursor: pointer; } 90 | 91 | .icon.plus:after { content:'\f196'; } 92 | 93 | .icon.plus.less:after { content: '\f147'; } 94 | 95 | span.status:after { 96 | font-family: "Font Awesome"; 97 | width:16px; 98 | text-align:center; 99 | } 100 | 101 | span.status.ok:after { 102 | content:'\f00c'; 103 | color:#01a109; 104 | } 105 | 106 | span.status.failed:after { 107 | content:'\f00d'; 108 | color:#c90d0d; 109 | } 110 | 111 | span.status.pending:after { 112 | content:url('/peripleo/static/images/wait-anim-fb.gif'); 113 | height:16px; 114 | } 115 | 116 | .meter { 117 | width:180px; 118 | white-space:nowrap; 119 | position:relative; 120 | .rounded-corners(3px); 121 | .gradient(#ccc, #eee); 122 | 123 | .bar { 124 | .rounded-corners(3px); 125 | .gradient(#83c783, #008a00); 126 | display:inline-block; 127 | height:10px; 128 | background-color:#2a4c77; 129 | } 130 | 131 | .label { 132 | position:absolute; 133 | width:50px; 134 | text-align:center; 135 | right:-50px; 136 | margin-top:-3px; 137 | } 138 | 139 | } /* .meter */ 140 | 141 | button { 142 | margin:0 2px; 143 | } 144 | 145 | } /** table.list **/ 146 | 147 | } 148 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base-layout.less: -------------------------------------------------------------------------------- 1 | .row { 2 | clear:both; 3 | position:relative; 4 | width:900px; 5 | margin:0 auto; 6 | } 7 | 8 | a { 9 | color:#4f7eb4; 10 | text-decoration:none; 11 | } 12 | 13 | #header { 14 | color:#fff; 15 | 16 | a { 17 | color:#fff; 18 | text-decoration:none; 19 | } 20 | 21 | #header-menu { 22 | background-color:#2a4c77; 23 | padding:0 20px; 24 | border-bottom:1px solid #1e3858; 25 | text-shadow:1px 1px rgba(0,0,0,0.4); 26 | 27 | .pelagios-logo { 28 | font-family:Cinzel; 29 | font-size:22px; 30 | position:relative; 31 | top:3px; 32 | margin-right:10px; 33 | } 34 | 35 | ul { 36 | margin:0; 37 | padding:0; 38 | list-style-type:none; 39 | display:inline-block; 40 | font-size:14px; 41 | 42 | li { 43 | display:inline-block; 44 | border-style:solid; 45 | border-color:transparent; 46 | border-width:2px 0; 47 | 48 | a { 49 | padding:12px 20px; 50 | display:block; 51 | } 52 | 53 | } /* li */ 54 | 55 | li:hover { 56 | background-color:#3e5d83; 57 | } 58 | 59 | li.current { 60 | border-bottom-color:#fff; 61 | } 62 | 63 | } /* ul */ 64 | 65 | } /* #header-menu */ 66 | 67 | #header-body { 68 | color:#fff; 69 | background-color:#325693; 70 | border-style:solid; 71 | border-width:1px 0; 72 | border-top-color:#4e78ac; 73 | border-bottom-color:#5478a4; 74 | background:linear-gradient(#3d6ba3, #6493c8); 75 | padding:20px 0; 76 | text-shadow:1px 1px rgba(0,0,0,0.4); 77 | 78 | h1 { 79 | margin:10px 0 0 0; 80 | font-size:36px; 81 | font-weight:bold; 82 | } 83 | 84 | h2 { 85 | margin:8px 0; 86 | font-size:14px; 87 | font-weight:normal; 88 | } /* h2 */ 89 | 90 | } /* #header-body */ 91 | 92 | p { 93 | font-size:14px; 94 | line-height:160%; 95 | } 96 | 97 | p.description { 98 | margin:25px 0 0 0; 99 | max-width:70%; 100 | } 101 | 102 | p.stats { 103 | margin:12px 0; 104 | 105 | span { 106 | padding-right:6px; 107 | } 108 | 109 | } 110 | 111 | em { 112 | font-size:22px; 113 | font-weight:bold; 114 | font-style:normal; 115 | } 116 | 117 | } /* #header */ 118 | 119 | #content { 120 | 121 | padding:30px 0 60px 0; 122 | 123 | h2 { 124 | margin:20px 0 20px 0; 125 | font-size:22px; 126 | font-weight:bold; 127 | color:#323232; 128 | } 129 | 130 | } /* #content */ 131 | -------------------------------------------------------------------------------- /app/assets/stylesheets/common/autocomplete.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | 3 | .tt-hint { 4 | color:#a2a2a2; 5 | } 6 | 7 | .tt-dropdown-menu { 8 | width:260px; 9 | background-color: #fff; 10 | border:1px solid #ccc; 11 | text-align:left; 12 | .box-shadow(1px, 5px, 10px, 0, rgba(0,0,0,0.2)); 13 | .rounded-corners(5px); 14 | } 15 | 16 | .tt-suggestion { 17 | color:#5f5f5f; 18 | font-size:14px; 19 | text-shadow:none; 20 | padding: 4px 8px; 21 | line-height: 18px; 22 | 23 | p { margin:0; } 24 | 25 | strong { color:#333; } 26 | 27 | } 28 | 29 | .tt-suggestion:hover { 30 | background-color:#e2e2e2; 31 | } 32 | 33 | .tt-suggestion.tt-cursor { 34 | background-color: #e2e2e2; 35 | } 36 | -------------------------------------------------------------------------------- /app/assets/stylesheets/common/paginate.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | 3 | .pagination { 4 | color:#4372a9; 5 | margin-bottom:30px; 6 | 7 | .label { 8 | display:inline-block; 9 | margin-right:15px; 10 | } 11 | 12 | ul { 13 | float:right; 14 | display:inline; 15 | margin:0; 16 | padding:0; 17 | list-style-type:none; 18 | font-size:13px; 19 | 20 | li { 21 | display:inline-block; 22 | text-align:center; 23 | margin:0 5px 0 0; 24 | 25 | a { 26 | padding:6px; 27 | display:block; 28 | text-decoration:none; 29 | } 30 | 31 | } /* li */ 32 | 33 | li.page { 34 | background-color:#4372a9; 35 | border:1px solid #4372a9; 36 | min-width:26px; 37 | 38 | a { color:#fff; } 39 | 40 | } 41 | 42 | li.page:hover { 43 | background-color:#5d8bbf; 44 | } 45 | 46 | li.page.active { 47 | background-color:#fff; 48 | color:#4372a9; 49 | border:1px solid #4372a9; 50 | 51 | a { 52 | cursor:default; 53 | color:#4372a9; 54 | } 55 | } 56 | 57 | li.first, li.prev, li.next, li.last { 58 | font-family:"Font Awesome"; 59 | } 60 | 61 | li.disabled { 62 | 63 | a { 64 | cursor:default; 65 | color:#ccc; 66 | } 67 | 68 | } 69 | 70 | 71 | } /* ul */ 72 | 73 | } /* .pagination */ 74 | -------------------------------------------------------------------------------- /app/assets/stylesheets/common/pagination.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | 3 | .pagination { 4 | 5 | ul { 6 | margin:0; 7 | padding:0; 8 | list-style-type:none; 9 | font-size:13px; 10 | 11 | li:first-child { 12 | padding-left:4px; 13 | .rounded-corners-left(3px); 14 | } 15 | 16 | li:last-child { 17 | padding-right:4px; 18 | .rounded-corners-right(3px); 19 | border-right-width:1px; 20 | } 21 | 22 | li { 23 | float:left; 24 | background-color:#4372a9; 25 | .gradient(#4b83c3, #4372a9); 26 | color:#fff; 27 | min-width:26px; 28 | text-align:center; 29 | margin:0; 30 | border-width:1px 0 1px 1px; 31 | border-style:solid; 32 | border-color:#4372a9 rgba(255, 255, 255, 0.2) #4372a9 #4372a9; 33 | 34 | a, span { 35 | padding:6px; 36 | display:block; 37 | color:#fff; 38 | text-decoration:none; 39 | } 40 | 41 | } /* li */ 42 | 43 | li.active { 44 | background-image:none; 45 | background-color:#fff; 46 | color:#4372a9; 47 | border-color:#4372a9; 48 | 49 | a, span { color:#4372a9; } 50 | 51 | } 52 | 53 | li:not(.active):hover { 54 | background-image:none; 55 | background-color:#5d8bbf; 56 | } 57 | 58 | } /* ul */ 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/assets/stylesheets/common/table.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | 3 | table.list { 4 | border-collapse:collapse; 5 | 6 | tr { 7 | border-color:#e2e2e2; 8 | border-style:solid; 9 | border-width:1px 0; 10 | } 11 | 12 | tr:hover { background-color:#f2f2f2; } 13 | 14 | th, td { 15 | font-size:13px; 16 | padding:6px 10px 6px 10px; 17 | text-align:left; 18 | 19 | a { 20 | color:#333; 21 | text-decoration:none; 22 | } 23 | 24 | } 25 | 26 | td.center, th.center { text-align:center; } 27 | td.right, th.right { text-align:right; } 28 | 29 | .lang { 30 | .rounded-corners(3px); 31 | padding:2px 4px; 32 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 33 | font-size:75%; 34 | font-weight:bold; 35 | color:#fff; 36 | text-align:center; 37 | background-color: #999; 38 | width:14px; 39 | display:inline-block; 40 | cursor:default; 41 | } 42 | 43 | .lang.en { background-color:#1f77b4; } 44 | .lang.es { background-color:#9467bd; } 45 | .lang.la { background-color:#bcbd22; } 46 | 47 | } /* table */ 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/datasets/dataset-details.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../base-layout.less"; 3 | @import "../common/table.less"; 4 | @import "../common/paginate.less"; 5 | 6 | #header { 7 | 8 | #header-body { 9 | 10 | .subset-count { 11 | display:block; 12 | font-size:18px; 13 | font-weight:normal; 14 | } 15 | 16 | p.time { 17 | padding:5px 0 20px 0; 18 | margin:0; 19 | } 20 | 21 | } /* header-body */ 22 | 23 | } /* #header */ 24 | 25 | #content { 26 | 27 | .float-right { 28 | float:right; 29 | 30 | #map { 31 | width:360px; 32 | height:240px; 33 | border:1px solid #999; 34 | } 35 | 36 | #map-toggle-fullscreen { 37 | padding-top:3px; 38 | text-align:right; 39 | } 40 | 41 | #temporal-profile { 42 | width:360px; 43 | height:40px; 44 | margin:10px 0 30px 0; 45 | border-style:solid; 46 | border-color:#ccc; 47 | border-width:0 0 1px 0; 48 | } 49 | 50 | } /* .floatright */ 51 | 52 | #top-n-places { 53 | padding-bottom:30px; 54 | 55 | .place-title { 56 | text-align:right; 57 | padding:5px 8px; 58 | max-width:205px; 59 | overflow:hidden; 60 | white-space:nowrap; 61 | text-overflow:ellipsis; 62 | } 63 | 64 | .place-count { 65 | width:320px; 66 | 67 | .meter { 68 | width:240px; 69 | white-space:nowrap; 70 | margin-bottom:1px; 71 | 72 | .bar { 73 | .rounded-corners(3px); 74 | .gradient(#5483bd, #4a6b93); 75 | .box-shadow(2px, 2px, 4px, 0, rgba(0, 0, 0, 0.4)); 76 | display:inline-block; 77 | height:10px; 78 | background-color:#2a4c77; 79 | } 80 | 81 | span { 82 | display:inline-block; 83 | height:10px; 84 | margin-left:8px; 85 | } 86 | 87 | } /* .meter */ 88 | 89 | } /* .place-count */ 90 | 91 | } 92 | 93 | #items-list { 94 | clear:both; 95 | padding-top:20px; 96 | 97 | h2, .pagination { 98 | margin:20px 0 10px 0; 99 | } 100 | 101 | .from-to { 102 | padding-left:20px; 103 | } 104 | 105 | table { 106 | width:100%; 107 | border-collapse:collapse; 108 | margin-bottom:80px; 109 | 110 | tr { 111 | border-color:#e2e2e2; 112 | border-style:solid; 113 | border-width:1px 0; 114 | } /* tr */ 115 | 116 | th, td { 117 | font-size:13px; 118 | padding:6px 10px 6px 10px; 119 | text-align:left; 120 | } /* th, td */ 121 | 122 | } /* table */ 123 | 124 | } /* #items-list */ 125 | 126 | } /* #content */ 127 | -------------------------------------------------------------------------------- /app/assets/stylesheets/datasets/dataset-list.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../base-layout.less"; 3 | @import "../common/pagination.less"; 4 | 5 | #content { 6 | 7 | #dataset-list { 8 | border-collapse:collapse; 9 | 10 | tr { 11 | border-bottom:1px solid #e2e2e2; 12 | color:#323232; 13 | 14 | td { 15 | vertical-align:top; 16 | line-height:140%; 17 | padding:20px 0; 18 | 19 | h3 { 20 | margin:0; 21 | padding:0; 22 | 23 | a { 24 | font-size:18px; 25 | } 26 | 27 | } /* h3 */ 28 | 29 | h4 { 30 | margin:0; 31 | padding:0; 32 | font-size:13px; 33 | font-weight:normal; 34 | } /* h4 */ 35 | 36 | .description { color:#525252; } 37 | 38 | } /* td */ 39 | 40 | td.stats { 41 | color:#323232; 42 | white-space: nowrap; 43 | padding-left:15px; 44 | font-size:11px; 45 | text-align:right; 46 | 47 | em { 48 | font-size:18px; 49 | font-weight:bold; 50 | font-style:normal; 51 | text-shadow:1px 1px rgba(0,0,0,0.2); 52 | } 53 | 54 | } /* td.stats */ 55 | 56 | } /* tr */ 57 | 58 | } /* #dataset-list */ 59 | 60 | } /* #content */ 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/assets/stylesheets/gazetteer-uri.less: -------------------------------------------------------------------------------- 1 | @import "globals.less"; 2 | 3 | .gazetteer-uri { 4 | color:#fff; 5 | .rounded-corners(4px); 6 | font-size:13px; 7 | padding:4px 8px 5px 8px; 8 | background-color:#ccc; 9 | text-shadow:1px 1px rgba(0,0,0,0.4); 10 | border:1px solid #eee; 11 | } 12 | 13 | .gazetteer-uri.inline { 14 | padding:2px 5px 3px 5px; 15 | margin-left:5px; 16 | font-size:11px; 17 | position:relative; 18 | bottom:2px; 19 | } 20 | 21 | .gazetteer-uri.pleiades { 22 | background-color:#1f77b4; 23 | border-color:darken(color('#1f77b4'), 5%); 24 | } 25 | 26 | .gazetteer-uri.dare { 27 | background-color:#f58929; 28 | border-color:darken(color('#f58929'), 14%); 29 | } 30 | 31 | .gazetteer-uri.idai { 32 | background-color:#2ca02c; 33 | border-color:darken(color('#2ca02c'), 14%); 34 | } 35 | 36 | .gazetteer-uri.vici { 37 | background-color:#d33d3d; 38 | border-color:darken(color('#d33d3d'), 14%); 39 | } 40 | 41 | .gazetteer-uri.trismegistos { 42 | background-color:#bcbd22; 43 | border-color:darken(color('#bcbd22'), 7%); 44 | } 45 | 46 | .gazetteer-uri.nomisma { 47 | background-color:#17becf; 48 | border-color:darken(color('#bcbd22'), 10%); 49 | } 50 | -------------------------------------------------------------------------------- /app/assets/stylesheets/gazetteer/main.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | width:100%; 3 | height:100%; 4 | padding:0; 5 | margin:0; 6 | } 7 | 8 | #map { 9 | width:100%; 10 | height:100%; 11 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/globals-new.less: -------------------------------------------------------------------------------- 1 | /** Custom fonts **/ 2 | 3 | @font-face { 4 | font-family: "Font Awesome"; 5 | src: url("/peripleo/static/stylesheets/fontawesome-webfont.ttf"); 6 | } 7 | 8 | @font-face { 9 | font-family: "Fontello"; 10 | src: url("/peripleo/static/stylesheets/fontello-icons.ttf"); 11 | } 12 | 13 | @font-face { 14 | font-family: "Peripleo"; 15 | src: url("/peripleo/static/stylesheets/peripleo.ttf"); 16 | } 17 | 18 | @font-face { 19 | font-family: "Roboto"; 20 | src: url("/peripleo/static/stylesheets/Roboto-Regular.ttf"); 21 | } 22 | 23 | @font-face { 24 | font-family: "Roboto"; 25 | src: url("/peripleo/static/stylesheets/Roboto-Light.ttf"); 26 | font-weight:300; 27 | } 28 | 29 | /** Mixins **/ 30 | 31 | .gradient (@start, @stop) { 32 | background-color: @start; 33 | background:-webkit-gradient(linear, left top, left bottom, from(@start), to(@stop)); 34 | background:-moz-linear-gradient(top, @start, @stop); 35 | background:-webkit-linear-gradient(top, @start 0%, @stop 100%); 36 | background:-o-linear-gradient(top, @start 0%, @stop 100%); 37 | background:-ms-linear-gradient(top, @start 0%, @stop 100%); 38 | background:linear-gradient(@start, @stop); 39 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=@start,endColorstr=@stop); 40 | } 41 | 42 | .rounded-corners (@radius) { 43 | -moz-border-radius: @radius; 44 | -webkit-border-radius: @radius; 45 | -khtml-border-radius: @radius; 46 | border-radius: @radius; 47 | } 48 | 49 | .rounded-corners-bottom (@radius) { 50 | -moz-border-radius-bottomleft: @radius; 51 | -moz-border-radius-bottomright: @radius; 52 | -webkit-border-bottom-left-radius: @radius; 53 | -webkit-border-bottom-right-radius: @radius; 54 | -khtml-border-radius-bottomleft: @radius; 55 | -khtml-border-radius-bottomright: @radius; 56 | border-bottom-left-radius: @radius; 57 | border-bottom-right-radius: @radius; 58 | } 59 | 60 | .rounded-corners-top (@radius) { 61 | -moz-border-radius-topleft: @radius; 62 | -moz-border-radius-topright: @radius; 63 | -webkit-border-top-left-radius: @radius; 64 | -webkit-border-top-right-radius: @radius; 65 | -khtml-border-radius-topleft: @radius; 66 | -khtml-border-radius-topright: @radius; 67 | border-top-left-radius: @radius; 68 | border-top-right-radius: @radius; 69 | } 70 | 71 | .rounded-corners-left (@radius) { 72 | -moz-border-radius-topleft: @radius; 73 | -moz-border-radius-bottomleft: @radius; 74 | -webkit-border-top-left-radius: @radius; 75 | -webkit-border-bottom-left-radius: @radius; 76 | -khtml-border-radius-topleft: @radius; 77 | -khtml-border-radius-bottomleft: @radius; 78 | border-top-left-radius: @radius; 79 | border-bottom-left-radius: @radius; 80 | } 81 | 82 | .rounded-corners-right (@radius) { 83 | -moz-border-radius-topright: @radius; 84 | -moz-border-radius-bottomright: @radius; 85 | -webkit-border-top-right-radius: @radius; 86 | -webkit-border-bottom-right-radius: @radius; 87 | -khtml-border-radius-topright: @radius; 88 | -khtml-border-radius-bottomright: @radius; 89 | border-top-right-radius: @radius; 90 | border-bottom-right-radius: @radius; 91 | } 92 | 93 | .box-shadow (@h, @v, @blur, @spread, @color) { 94 | -moz-box-shadow:@h @v @blur @spread @color; 95 | -webkit-box-shadow:@h @v @blur @spread @color; 96 | box-shadow: @h @v @blur @spread @color; 97 | } 98 | 99 | .box-shadow-inset (@h, @v, @blur, @spread, @color) { 100 | box-shadow: inset @h @v @blur @spread @color; 101 | -webkit-box-shadow: inset @h @v @blur @spread #ABABAB; 102 | -moz-box-shadow: inset @h @v @blur @spread #ABABAB; 103 | -o-box-shadow: inset @h @v @blur @spread #ABABAB; 104 | } 105 | 106 | .no-select() { 107 | -webkit-user-select: none; 108 | -moz-user-select: none; 109 | user-select: none; 110 | -webkit-user-drag: none; 111 | } 112 | 113 | /** Global styles **/ 114 | 115 | .icon { 116 | font-family:'Font Awesome' !important; 117 | text-decoration:none; 118 | font-weight:normal; 119 | } 120 | -------------------------------------------------------------------------------- /app/assets/stylesheets/home/index.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../base-layout.less"; 3 | @import "../common/autocomplete"; 4 | 5 | #header { 6 | 7 | #header-body { 8 | text-align:center; 9 | padding-top:60px; 10 | padding-bottom:60px; 11 | 12 | form { 13 | margin:30px 0 40px 0; 14 | 15 | .form-container { 16 | margin:0 auto; 17 | display:inline-block; 18 | 19 | input[type="text"] { 20 | font-size:15px; 21 | display:block; 22 | width:340px; 23 | margin:0; 24 | border-width:0; 25 | padding:8px; 26 | outline:none; 27 | height:31px; 28 | border:1px solid #436691; 29 | } 30 | 31 | input[type="submit"] { 32 | display:block; 33 | text-align:center; 34 | width:60px; 35 | float:left; 36 | height:31px; 37 | margin:1px 0 0 0; 38 | background-color:#799fca; 39 | color:#fff; 40 | border-width:1px 1px 1px 0; 41 | border-style:solid; 42 | border-color:#98b4d4 #4a6079 #4a6079 #98b4d4; 43 | cursor:pointer; 44 | .rounded-corners-right(5px); 45 | } 46 | 47 | } /* .form-container */ 48 | 49 | .form-container:after { 50 | content:" "; 51 | display:block; 52 | height:0; 53 | clear:both; 54 | } 55 | 56 | } /* form */ 57 | 58 | em.number { 59 | padding:0 5px; 60 | } 61 | 62 | } /* #header-body */ 63 | 64 | } /* #header */ 65 | -------------------------------------------------------------------------------- /app/assets/stylesheets/items/item-details.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../base-layout.less"; 3 | 4 | #header { 5 | 6 | #header-body { 7 | 8 | h1 a { 9 | padding-left:3px; 10 | font-size:18px; 11 | .icon { font-size:15px; } 12 | } 13 | 14 | p.time { 15 | padding:5px 0 20px 0; 16 | margin:0; 17 | } 18 | 19 | } /* header-body */ 20 | 21 | } /* #header */ 22 | 23 | #content { 24 | 25 | } /* #content */ 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_autoComplete.less: -------------------------------------------------------------------------------- 1 | .tt-hint { 2 | color:#a2a2a2; 3 | } 4 | 5 | .tt-dropdown-menu { 6 | position:relative !important; 7 | background-color: #fff; 8 | margin-top:1px; 9 | } 10 | 11 | .tt-suggestion { 12 | color:#5f5f5f; 13 | font-size:13px; 14 | font-weight:300; 15 | text-shadow:none; 16 | padding: 8px 10px; 17 | line-height: 18px; 18 | border-width:0 0 1px 0; 19 | border-style:solid; 20 | border-color:#e6e6e6; 21 | 22 | p { margin:0; } 23 | 24 | strong { 25 | font-weight:normal; 26 | color:#333; 27 | } 28 | 29 | } 30 | 31 | .tt-suggestion:last-child { 32 | border:none; 33 | } 34 | 35 | .tt-suggestion:hover { 36 | background-color:#fafafa; 37 | } 38 | 39 | .tt-suggestion.tt-cursor { 40 | background-color: #fafafa; 41 | } 42 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_facetChart.less: -------------------------------------------------------------------------------- 1 | .section.facet { 2 | 3 | .facet-header { 4 | position:relative; 5 | padding-bottom:4px; 6 | 7 | h3 { 8 | display:inline-block; 9 | color:#686868; 10 | font-weight:normal; 11 | font-weight:normal; 12 | font-size:13px; 13 | padding:0; 14 | margin:0 0 2px 0; 15 | } 16 | 17 | .filter-buttons { 18 | position:absolute; 19 | right:0; 20 | padding:3px 4px; 21 | font-family:Roboto, Arial, sans-serif; 22 | font-size:12px; 23 | 24 | .btn { 25 | margin-left:5px; 26 | color:#4e78ac; 27 | text-decoration:none; 28 | 29 | .icon { 30 | position:relative; 31 | top:1px; 32 | font-size:11px; 33 | vertical-align:top; 34 | margin-right:1px; 35 | } 36 | 37 | } 38 | 39 | .btn.clear .icon { 40 | top:0; 41 | font-size:13px; 42 | } 43 | 44 | .btn:hover .label { 45 | text-decoration:underline; 46 | } 47 | 48 | } 49 | 50 | } /* .facet-header */ 51 | 52 | } 53 | 54 | ul.chart { 55 | list-style-type:none; 56 | margin:0 0 0 30px; 57 | padding:0; 58 | 59 | li { padding: 2px 0; } 60 | 61 | .meter { 62 | width:140px; 63 | background:none; 64 | white-space:nowrap; 65 | 66 | .bar { 67 | display:inline-block; 68 | height:6px; 69 | } 70 | 71 | } /* .meter */ 72 | 73 | .label { 74 | width:150px; 75 | max-width:150px; 76 | display:inline-block; 77 | vertical-align:middle; 78 | color:#323232; 79 | font-size:11px; 80 | overflow:hidden; 81 | white-space:nowrap; 82 | text-overflow: ellipsis; 83 | padding:0 5px; 84 | } 85 | 86 | } /* ul.chart */ 87 | 88 | ul.chart.type .bar { 89 | border:1px solid #756bb1; 90 | background-color:#9e9ac8; 91 | } 92 | 93 | ul.chart.source_dataset .bar { 94 | border:1px solid #31a354; 95 | background-color:#74c476; 96 | } 97 | 98 | ul.chart.lang .bar { 99 | border:1px solid #bcbd22; 100 | background-color:#dbdb8d; 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_filterEditor.less: -------------------------------------------------------------------------------- 1 | #filter-editor { 2 | 3 | .buttons { 4 | padding:12px 16px; 5 | 6 | .btn { 7 | display:inline-block; 8 | padding-right:10px; 9 | color:#4e78ac; 10 | font-size:13.5px; 11 | cursor:pointer; 12 | } 13 | 14 | .btn:hover .label { 15 | text-decoration:underline; 16 | } 17 | 18 | } 19 | 20 | } 21 | 22 | ul.chart.large { 23 | margin:15px 0 0 50px; 24 | padding:0; 25 | 26 | li { 27 | padding: 6px 0; 28 | cursor:pointer; 29 | } 30 | 31 | .selection-toggle { 32 | color:#5a5a5a; 33 | float:left; 34 | margin:-1px 10px 0 0; 35 | width:20px; 36 | font-size:16.5px; 37 | } 38 | 39 | .meter { 40 | width:300px; 41 | 42 | .bar { 43 | height:12px; 44 | .rounded-corners(2px); 45 | .box-shadow(1px, 1px, 2px, 0, rgba(0,0,0,0.2)); 46 | } 47 | 48 | } /* .meter */ 49 | 50 | .label { 51 | width:300px; 52 | max-width:300px; 53 | color:#323232; 54 | font-size:12px; 55 | padding:0 0 3px 8px; 56 | } 57 | 58 | .label-other { 59 | color:#323232; 60 | font-size:12px; 61 | padding:0 0 3px 8px; 62 | } 63 | 64 | } /* ul.chart */ 65 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_filterPanel.less: -------------------------------------------------------------------------------- 1 | @import "_timeHistogram.less"; 2 | @import "_facetChart.less"; 3 | 4 | #filterpanel { 5 | margin-top:-2px; 6 | color:#333; 7 | background-color:#fff; 8 | .box-shadow(0, 0, 6px, 0, rgba(0,0,0,0.3)); 9 | .rounded-corners-bottom(2px); 10 | 11 | .section { 12 | padding:6px; 13 | background-color:#fafafa; 14 | border-bottom:1px solid #e6e6e6; 15 | } 16 | 17 | .section.histogram { padding-top:9px; } 18 | 19 | .footer { 20 | padding:14px 12px 11px 10px; 21 | color:#686868; 22 | font-size:13px; 23 | 24 | .advanced { 25 | float:right; 26 | color:#9f9f9f; 27 | cursor:pointer; 28 | } 29 | 30 | .advanced:hover { 31 | color:#686868; 32 | } 33 | 34 | .advanced:after { 35 | font-family:"Font Awesome"; 36 | content:'\f078'; 37 | padding-left:3px; 38 | } 39 | 40 | .advanced.open:after { 41 | content:'\f077'; 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_gazetteerUri.less: -------------------------------------------------------------------------------- 1 | @import "_gazetteerUriEARK.less"; 2 | 3 | .gazetteer-uri { 4 | color:#fff; 5 | .rounded-corners(3px); 6 | font-size:11px; 7 | padding:2px 4px 2px 4px; 8 | background-color:#ccc; 9 | text-shadow:1px 1px rgba(0,0,0,0.2); 10 | border:1px solid #eee; 11 | margin-right:5px; 12 | text-decoration:none; 13 | } 14 | 15 | .gazetteer-uri.pleiades, .gazetteer-uri.atlantides{ 16 | background-color:#1f77b4; 17 | border-color:darken(color('#1f77b4'), 5%); 18 | } 19 | 20 | .gazetteer-uri.dare { 21 | background-color:#f58929; 22 | border-color:darken(color('#f58929'), 14%); 23 | } 24 | 25 | .gazetteer-uri.idai { 26 | background-color:#2ca02c; 27 | border-color:darken(color('#2ca02c'), 14%); 28 | } 29 | 30 | .gazetteer-uri.vici { 31 | background-color:#d33d3d; 32 | border-color:darken(color('#d33d3d'), 14%); 33 | } 34 | 35 | .gazetteer-uri.chgis { 36 | background-color:#9467bd; 37 | border-color:darken(color('#9467bd'), 14%); 38 | } 39 | 40 | .gazetteer-uri.trismegistos { 41 | background-color:#bcbd22; 42 | border-color:darken(color('#bcbd22'), 7%); 43 | } 44 | 45 | .gazetteer-uri.nomisma { 46 | background-color:#17becf; 47 | border-color:darken(color('#17becf'), 10%); 48 | } 49 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_gazetteerUriEARK.less: -------------------------------------------------------------------------------- 1 | .gazetteer-uri.obcine1994, .gazetteer-uri.earkdev1994 { 2 | background-color:#1f77b4; 3 | border-color:darken(color('#1f77b4'), 5%); 4 | } 5 | 6 | .gazetteer-uri.obcine1995, .gazetteer-uri.earkdev1995 { 7 | background-color:#f58929; 8 | border-color:darken(color('#f58929'), 14%); 9 | } 10 | 11 | .gazetteer-uri.obcine1998, .gazetteer-uri.earkdev1998 { 12 | background-color:#2ca02c; 13 | border-color:darken(color('#2ca02c'), 14%); 14 | } 15 | 16 | .gazetteer-uri.obcine2002, .gazetteer-uri.earkdev2002 { 17 | background-color:#d33d3d; 18 | border-color:darken(color('#d33d3d'), 14%); 19 | } 20 | 21 | .gazetteer-uri.obcine2006, .gazetteer-uri.earkdev2006 { 22 | background-color:#9467bd; 23 | border-color:darken(color('#9467bd'), 14%); 24 | } 25 | 26 | .gazetteer-uri.obcine2010, .gazetteer-uri.earkdev2010 { 27 | background-color:#bcbd22; 28 | border-color:darken(color('#bcbd22'), 7%); 29 | } 30 | 31 | .gazetteer-uri.obcine2015, .gazetteer-uri.earkdev2015 { 32 | background-color:#17becf; 33 | border-color:darken(color('#17becf'), 10%); 34 | } 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_imageControl.less: -------------------------------------------------------------------------------- 1 | #image-control { 2 | margin-top:4px; 3 | background-color:#fff; 4 | position:relative; 5 | .box-shadow(0, 2px, 6px, 0, rgba(0,0,0,0.3)); 6 | .rounded-corners(2px); 7 | padding:2px; 8 | height:98px; 9 | 10 | .caption { 11 | position:absolute; 12 | color:#fff; 13 | text-shadow:rgba(0,0,0,0.7) 0px 1px; 14 | bottom:8px; 15 | left:8px; 16 | } 17 | 18 | } 19 | 20 | #thumbnail-container { 21 | position:relative; 22 | width:100%; 23 | height:100%; 24 | overflow:hidden; 25 | 26 | img { cursor: pointer; } 27 | 28 | } 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_modalEditor.less: -------------------------------------------------------------------------------- 1 | .clicktrap { 2 | position:absolute; 3 | top:0; 4 | left:0; 5 | width:100%; 6 | height:100%; 7 | background-color:rgba(0,0,0,0.8); 8 | z-index:9999; 9 | } 10 | 11 | .modal-editor { 12 | position:absolute; 13 | background-color:#fff; 14 | width:640px; 15 | height:470px; 16 | margin:auto; 17 | top:0; 18 | bottom:0; 19 | left:0; 20 | right:0; 21 | .rounded-corners(2px); 22 | 23 | .close { 24 | float:right; 25 | padding:8px 10px; 26 | font-size:28px; 27 | color:#3f3f3f; 28 | cursor:pointer; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_searchPanel.less: -------------------------------------------------------------------------------- 1 | @import "../globals-new.less"; 2 | @import "_autoComplete.less"; 3 | 4 | #searchbox { 5 | position:relative; 6 | width:100%; 7 | margin:0; 8 | .box-shadow(0, 3px, 7px, 0, rgba(0,0,0,0.3)); 9 | 10 | form { 11 | margin:0; 12 | padding:0; 13 | } 14 | 15 | span { width: 100%; } 16 | 17 | input[type='text'] { 18 | -webkit-appearance: none; 19 | width:100%; 20 | border:none; 21 | outline:none; 22 | padding:8px 32px 8px 10px; 23 | font-family:Roboto, Arial, sans-serif; 24 | font-size:15px; 25 | font-weight:300; 26 | .rounded-corners-top(2px); 27 | } 28 | 29 | input.search-at { 30 | padding-left:36px; 31 | } 32 | 33 | input[type='text']:focus { 34 | box-shadow:0 0 0 1px #4e78ac; 35 | -webkit-box-shadow:0 0 0 1px #4e78ac; 36 | -moz-box-shadow:0 0 0 1px #4e78ac; 37 | -o-box-shadow:0 0 0 1px #4e78ac; 38 | } 39 | 40 | #subsearch-indicator { 41 | position:absolute; 42 | font-size:13px; 43 | top:6px; 44 | left:8px; 45 | border-radius:50%; 46 | text-align:center; 47 | padding-top:3px; 48 | color:#fff; 49 | background-color:#4e78ac; 50 | border:1px solid #4e78ac; 51 | width:19px; 52 | height:16px; 53 | } 54 | 55 | #search-icon { 56 | position:absolute; 57 | top:9px; 58 | height:15px; 59 | width:15px; 60 | font-size:15px; 61 | left:374px; 62 | color:#9f9f9f; 63 | z-index:1; 64 | } 65 | 66 | #search-icon.clear { 67 | top:8px; 68 | cursor:pointer; 69 | } 70 | 71 | } 72 | 73 | .list-all { 74 | color:#4e78ac; 75 | cursor:pointer; 76 | 77 | .icon { 78 | position:relative; 79 | font-size:14px; 80 | top:1px; 81 | margin-right:2px; 82 | } 83 | 84 | } /* .list-all */ 85 | 86 | .list-all:hover { 87 | .label { text-decoration:underline; } 88 | } 89 | 90 | #button-explore { 91 | position:absolute; 92 | top:0; 93 | left:406px; 94 | box-sizing:border-box; 95 | width:34px; 96 | height:34px; 97 | padding:5px 0; 98 | background-color:#fff; 99 | font-family:Peripleo; 100 | font-size:28px 101 | } 102 | 103 | #button-explore.enabled { 104 | color:#fff; 105 | background-color:#4e78ac; 106 | } 107 | 108 | #button-listall { 109 | background-color:#fff; 110 | border-top:1px solid #e6e6e6; 111 | .rounded-corners-bottom(2px); 112 | padding:11px 10px; 113 | color:#686868; 114 | } 115 | 116 | #button-search-at { 117 | color:#4e78ac; 118 | background-color:#fff; 119 | position:relative; 120 | margin-top:4px; 121 | padding:10px; 122 | .rounded-corners(2px); 123 | .box-shadow(0, 2px, 6px, 0, rgba(0,0,0,0.3)); 124 | color:#4e78ac; 125 | 126 | .icon { 127 | font-size: 14px; 128 | margin-right:3px; 129 | } 130 | 131 | .all, .query { 132 | cursor:pointer; 133 | } 134 | 135 | .no-query:hover .underline, .underline:hover { 136 | text-decoration:underline; 137 | } 138 | 139 | .totals { color:#686868; } 140 | 141 | } 142 | 143 | #image-workshop { 144 | position:absolute; 145 | } 146 | 147 | 148 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_searchResults.less: -------------------------------------------------------------------------------- 1 | #search-results { 2 | margin-top:6px; 3 | background-color:#fff; 4 | position:relative; 5 | .box-shadow(0, 2px, 6px, 0, rgba(0,0,0,0.3)); 6 | .rounded-corners(2px); 7 | padding:0; 8 | overflow-y:auto; 9 | max-height:280px; 10 | 11 | ul { 12 | list-style-type:none; 13 | padding:0; 14 | margin:0; 15 | } 16 | 17 | li { 18 | padding:8px 10px; 19 | border-bottom:1px solid #e6e6e6; 20 | cursor:pointer; 21 | color:#4f4f4f; 22 | 23 | h3 { 24 | font-size:13px; 25 | font-weight:normal; 26 | margin:0; 27 | padding:0; 28 | line-height:140%; 29 | 30 | .icon { padding-right:5px; } 31 | } 32 | 33 | p { 34 | margin: 0; 35 | padding:2px 0; 36 | } 37 | 38 | .temp-bounds, .names, .description { 39 | color:#999; 40 | } 41 | 42 | .description { 43 | font-style:italic; 44 | } 45 | 46 | ul.uris { 47 | list-style-type:none; 48 | padding:4px 1px 0 1px; 49 | margin:0; 50 | 51 | line-height: 26px; 52 | 53 | li { display:inline-block; } 54 | 55 | .gazetteer-uri { font-size:11px; } 56 | 57 | } 58 | 59 | .source { 60 | font-size:11px; 61 | span { color:#4e78ac; } 62 | } 63 | 64 | .snippet { 65 | font-style:italic; 66 | font-family:"Times New Roman", Times, serif; 67 | font-size:14px; 68 | padding:5px 0; 69 | 70 | strong { color:#3f3f3f; } 71 | 72 | } 73 | 74 | } 75 | 76 | li:hover { background-color:#fafafa; } 77 | 78 | li:first-child { 79 | padding-top:10px; 80 | } 81 | 82 | li:last-child { 83 | border-width: 0px; 84 | padding-bottom:10px; 85 | } 86 | 87 | } 88 | 89 | #wait-for-next { 90 | width:100%; 91 | background-color:#fafafa; 92 | border-top:1px solid #e6e6e6; 93 | text-align:center; 94 | padding:10px 0; 95 | } 96 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_selectionInfo.less: -------------------------------------------------------------------------------- 1 | @import "_gazetteerUri.less"; 2 | 3 | .selection-info { 4 | margin-top:4px; 5 | background-color:#fff; 6 | position:relative; 7 | .box-shadow(0, 2px, 6px, 0, rgba(0,0,0,0.3)); 8 | .rounded-corners(2px); 9 | padding:8px 10px; 10 | 11 | .content { 12 | 13 | h3 { 14 | color:#323232; 15 | font-size:16px; 16 | line-height:22px; 17 | font-weight:normal; 18 | margin:0; 19 | padding:0 0 3px 0; 20 | } 21 | 22 | p { 23 | margin:0; 24 | padding:0 1px; 25 | color:#999; 26 | line-height:17px; 27 | } 28 | 29 | .temp-bounds { margin-right:8px; } 30 | 31 | .top-places { 32 | color:#4e78ac; 33 | cursor:pointer; 34 | 35 | .top { padding-left: 5px; } 36 | 37 | } 38 | 39 | .top-places:hover .top { text-decoration:underline; } 40 | 41 | .description { font-style:italic; } 42 | 43 | ul.uris { 44 | list-style-type:none; 45 | padding:4px 1px 2px 1px; 46 | margin:0; 47 | line-height: 26px; 48 | 49 | li { 50 | display:inline-block; 51 | } 52 | 53 | } 54 | 55 | .homepage { 56 | color:#4e78ac; 57 | 58 | a { 59 | color:#4e78ac; 60 | text-decoration:none; 61 | } 62 | 63 | a:hover { text-decoration:underline; } 64 | 65 | } 66 | 67 | .homepage:before { 68 | content:'\f14c'; 69 | font-family:'Font Awesome'; 70 | padding-right:5px; 71 | } 72 | 73 | .snippets { 74 | padding:10px 2px; 75 | } 76 | 77 | .snippet { 78 | padding:5px 0; 79 | } 80 | 81 | .related { 82 | color:#4e78ac; 83 | cursor:pointer; 84 | display:inline-block; 85 | } 86 | 87 | .related:hover { 88 | text-decoration:underline; 89 | } 90 | 91 | } /* .content */ 92 | 93 | .content.with-thumb { 94 | margin-right:110px; 95 | } 96 | 97 | .thumbnail { 98 | position:absolute; 99 | top:0; 100 | right:0; 101 | 102 | img, .img-404 { 103 | margin:0; 104 | width:110px; 105 | height:110px; 106 | .rounded-corners-right (2px); 107 | } 108 | 109 | .img-404 { background-color: #ccc; } 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_settingsEditor.less: -------------------------------------------------------------------------------- 1 | #settings-editor { 2 | overflow:hidden; 3 | 4 | ul, h2, p { 5 | padding:0; 6 | margin:0; 7 | } 8 | 9 | ul { 10 | list-style-type:none; 11 | margin-top:10px; 12 | } 13 | 14 | li { padding:10px 20px; } 15 | 16 | li.baselayer { 17 | cursor:pointer; 18 | min-height:66px; 19 | 20 | .map-thumb-container { 21 | overflow:hidden; 22 | margin-right:10px; 23 | .rounded-corners(2px); 24 | width:160px; 25 | height:66px; 26 | float:left; 27 | 28 | img { margin: -110px -35px -80px -25px; } 29 | } 30 | 31 | h2 { 32 | color:#323232; 33 | font-size:16px; 34 | font-weight:normal; 35 | line-height:22px; 36 | margin:0; 37 | padding:0; 38 | } 39 | 40 | p { 41 | font-size:13px; 42 | color:#999; 43 | line-height:15px; 44 | } 45 | 46 | a { 47 | color:#4e78ac; 48 | text-decoration:none; 49 | } 50 | 51 | a:hover { text-decoration:underline; } 52 | 53 | } /* li */ 54 | 55 | li.baselayer:hover { background-color:#fafafa; } 56 | 57 | #misc-settings { 58 | position:absolute; 59 | bottom:0; 60 | width:100%; 61 | border-top:1px solid #e6e6e6; 62 | background-color:#fafafa; 63 | padding:10px 4px; 64 | font-size:15px; 65 | color:#5a5a5a; 66 | .no-select(); 67 | 68 | li { padding:5px 20px; } 69 | 70 | em { 71 | font-style:normal; 72 | color:#929292; 73 | padding-left:2px; 74 | } 75 | 76 | .icon { 77 | width:17px; 78 | display:inline-block; 79 | cursor:pointer; 80 | } 81 | 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_timeHistogram.less: -------------------------------------------------------------------------------- 1 | #time-histogram { 2 | width:100%; 3 | position:relative; 4 | padding-top:8px; 5 | 6 | canvas { 7 | width:320px; 8 | height:40px; 9 | margin:0 34px 24px 34px; 10 | } 11 | 12 | .handle, .axislabel, .selection { 13 | position:absolute; 14 | } 15 | 16 | .axislabel { 17 | bottom:0; 18 | display:block; 19 | width:70px; 20 | text-align:center; 21 | font-size:11px; 22 | } 23 | 24 | .axislabel.from { 25 | left:2px; 26 | } 27 | 28 | .axislabel.to { 29 | right:2px; 30 | } 31 | 32 | .selection { 33 | top:3px; 34 | bottom:19px; 35 | left:31px; 36 | right:31px; 37 | .box-shadow-inset(0, 0, 2px, 0, rgba(0,0,0,0.3)); 38 | border:1px solid rgba(0,0,0,0.3); 39 | background-color:rgba(255,255,255,0.3); 40 | } 41 | 42 | .selection:before, .selection:after { 43 | position:absolute; 44 | top:6px; 45 | content:'\e801'; 46 | width:6px; 47 | padding:10px 3px 10px 4px; 48 | font-family:Fontello; 49 | font-size:11px; 50 | color:rgba(0,0,0,0.4); 51 | background-color:#bcbcbc; 52 | border-color:rgba(0,0,0,0.2); 53 | border-style:solid; 54 | .box-shadow(0, 0, 2px, 0, rgba(0,0,0,0.3)); 55 | } 56 | 57 | .selection:before { 58 | left:-15px; 59 | border-width:1px 0 1px 1px; 60 | .rounded-corners-left(5px); 61 | } 62 | 63 | .selection:after { 64 | right:-15px; 65 | border-width:1px 1px 1px 0; 66 | .rounded-corners-right(5px); 67 | } 68 | 69 | .handle { 70 | top:10px; 71 | bottom:28px; 72 | width:15px; 73 | // background-color:rgba(255, 0, 0, 0.3); 74 | cursor:pointer; 75 | 76 | .label { 77 | color:#efefef; 78 | width:54px; 79 | white-space:nowrap; 80 | position:absolute; 81 | top:-36px; 82 | padding:4px 2px; 83 | background-color:#323232; 84 | .rounded-corners(4px); 85 | text-align:center; 86 | } 87 | 88 | .label:after { 89 | position:absolute; 90 | top:98.1%; 91 | left:70%; 92 | margin-left:-25%; 93 | content: ''; 94 | width:0; 95 | height:0; 96 | border-top:solid 5px #323232; 97 | border-left:solid 5px transparent; 98 | border-right:solid 5px transparent; 99 | } 100 | 101 | } 102 | 103 | .handle.from { 104 | left:17px; 105 | .label { left:-16px; } 106 | } 107 | 108 | .handle.to { 109 | right:17px; 110 | .label { left:-32px; } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/_toolbar.less: -------------------------------------------------------------------------------- 1 | @import "../globals-new.less"; 2 | 3 | #toolbar { 4 | position:absolute; 5 | top:15px; 6 | right:30px; 7 | font-size:18px; 8 | 9 | .button { 10 | box-sizing:border-box; 11 | width:34px; 12 | height:34px; 13 | padding:8px 0; 14 | background-color:#fff; 15 | } 16 | 17 | .button:hover { 18 | background-color:#f2f2f2; 19 | } 20 | 21 | #toolbar-zoom .button { 22 | padding:10px 0; 23 | font-size:15px; 24 | } 25 | 26 | #toolbar-zoom-in { 27 | border-bottom: 1px solid #e6e6e6; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map-new/main.less: -------------------------------------------------------------------------------- 1 | @import "_imageControl.less"; 2 | @import "_searchPanel.less"; 3 | @import "_searchResults.less"; 4 | @import "_selectionInfo.less"; 5 | @import "_toolbar.less"; 6 | @import "_filterPanel.less"; 7 | @import "_modalEditor.less"; 8 | @import "_filterEditor.less"; 9 | @import "_settingsEditor.less"; 10 | 11 | html, body, #map { 12 | padding:0; 13 | margin:0; 14 | width:100%; 15 | height:100%; 16 | font-family:Roboto, Arial, sans-serif; 17 | } 18 | 19 | #controls { 20 | position:absolute; 21 | top:15px; 22 | bottom:15px; 23 | left:30px; 24 | font-size:13px; 25 | width:400px; 26 | pointer-events:none; 27 | } 28 | 29 | #controls * { 30 | pointer-events:all; 31 | } 32 | 33 | .tool { 34 | margin-bottom:6px; 35 | text-align:center; 36 | .box-shadow(0, 2px, 6px, 0, rgba(0,0,0,0.3)); 37 | color:#9f9f9f; 38 | cursor:pointer; 39 | .rounded-corners(2px); 40 | .no-select(); 41 | overflow:hidden; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map/_autocomplete.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/app/assets/stylesheets/map/_autocomplete.less -------------------------------------------------------------------------------- /app/assets/stylesheets/map/_facet-graph.less: -------------------------------------------------------------------------------- 1 | .leaflet-marker-icon, 2 | .leaflet-marker-shadow { 3 | -webkit-animation: fadein 2s; /* Safari, Chrome and Opera > 12.1 */ 4 | -moz-animation: fadein 2s; /* Firefox < 16 */ 5 | -ms-animation: fadein 2s; /* Internet Explorer */ 6 | -o-animation: fadein 2s; /* Opera < 12.1 */ 7 | animation: fadein 2s; 8 | } 9 | 10 | @keyframes fadein { 11 | from { opacity: 0; } 12 | to { opacity: 1; } 13 | } 14 | 15 | /* Firefox < 16 */ 16 | @-moz-keyframes fadein { 17 | from { opacity: 0; } 18 | to { opacity: 1; } 19 | } 20 | 21 | /* Safari, Chrome and Opera > 12.1 */ 22 | @-webkit-keyframes fadein { 23 | from { opacity: 0; } 24 | to { opacity: 1; } 25 | } 26 | 27 | /* Internet Explorer */ 28 | @-ms-keyframes fadein { 29 | from { opacity: 0; } 30 | to { opacity: 1; } 31 | } 32 | 33 | /* Opera < 12.1 */ 34 | @-o-keyframes fadein { 35 | from { opacity: 0; } 36 | to { opacity: 1; } 37 | } 38 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map/main.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "_facet-graph.less"; 3 | @import "../common/autocomplete.less"; 4 | 5 | html, body, #map { 6 | width:100%; 7 | height:100%; 8 | } 9 | 10 | form { 11 | margin:0; 12 | padding:0; 13 | } 14 | 15 | #controls { 16 | position:fixed; 17 | top:10px; 18 | right:10px; 19 | padding:0; 20 | .gradient(#4c5562, #444c58); 21 | .box-shadow(1px, 1px, 12px, 0, rgba(0, 0, 0, 0.4)); 22 | border:1px solid #3e4550; 23 | } 24 | 25 | #query-container { 26 | padding:6px; 27 | color:#fff; 28 | position:relative; 29 | 30 | .icon { 31 | position:absolute; 32 | top:14px; 33 | right:18px; 34 | color:#ccc; 35 | } 36 | 37 | input[type="text"] { 38 | background-color:#6a717a; 39 | border:1px solid #3e4550; 40 | padding:6px; 41 | width:260px; 42 | outline:none; 43 | color:#fff; 44 | } 45 | 46 | } 47 | 48 | #filter-panel { 49 | 50 | .header { 51 | padding:6px; 52 | font-weight:bold; 53 | font-size:14px; 54 | color:#fff; 55 | } 56 | 57 | .section.histogram { 58 | padding:6px; 59 | color:#fff; 60 | position:relative; 61 | 62 | canvas { width:300px; } 63 | 64 | span.label { 65 | position:relative; 66 | top:-10px; 67 | font-size:11px; 68 | } 69 | 70 | div.selection { 71 | position:absolute; 72 | top:0; 73 | bottom:0; 74 | border:1px solid #fff; 75 | } 76 | 77 | div.handle { 78 | position:absolute; 79 | width:20px; 80 | height:20px; 81 | bottom:40px; 82 | cursor:pointer; 83 | 84 | .label { 85 | position:absolute; 86 | top:-20px; 87 | width:50px; 88 | } 89 | 90 | } 91 | 92 | div.handle.from { 93 | background-color:#ff0000; 94 | left:5px; 95 | } 96 | 97 | div.handle.to { 98 | background-color:#00ffff; 99 | right:5px; 100 | } 101 | 102 | } /* .section.histogram */ 103 | 104 | .section.facets { 105 | padding:6px; 106 | background-color:#929292; 107 | border-top:1px solid #3e4550; 108 | color:#fff; 109 | text-align:center; 110 | 111 | table { border-collapse:collapse; } 112 | 113 | td { 114 | padding:2px; 115 | color:#323232; 116 | font-size:12px; 117 | } 118 | 119 | h3 { 120 | font-weight:normal; 121 | color:#dfdfdf; 122 | font-weight:normal; 123 | text-shadow:0 1px 0 rgba(255,255,255,0.5), 0 -1px 1px rgba(0,0,0,0.4); 124 | font-size:13px; 125 | padding:0; 126 | margin:0 0 5px 0; 127 | } 128 | 129 | td.label { 130 | min-width:60px; 131 | max-width:100px; 132 | overflow:hidden; 133 | white-space:nowrap; 134 | text-align:right; 135 | text-overflow: ellipsis; 136 | padding:0 5px 0 10px; 137 | } 138 | 139 | .meter { 140 | width:80px; 141 | background:none; 142 | white-space:nowrap; 143 | 144 | .count-number { 145 | padding-left:5px; 146 | font-size:12px; 147 | } 148 | 149 | .bar { 150 | display:inline-block; 151 | height:6px; 152 | } 153 | 154 | } /* .meter */ 155 | 156 | .chart.type .bar { 157 | border:1px solid #756bb1; 158 | background-color:#9e9ac8; 159 | } 160 | 161 | .chart.source .bar { 162 | border:1px solid #31a354; 163 | background-color:#74c476; 164 | } 165 | 166 | } /* .section.facets */ 167 | 168 | } 169 | 170 | #result-list { 171 | padding:6px; 172 | background-color:#929292; 173 | border-top:1px solid #3e4550; 174 | color:#fff; 175 | width:260px; 176 | list-style-type:none; 177 | } 178 | -------------------------------------------------------------------------------- /app/assets/stylesheets/places/place-details.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | @import "../common/table.less"; 3 | @import "../base-layout.less"; 4 | @import "../gazetteer-uri.less"; 5 | 6 | #header { 7 | 8 | #header-body { 9 | padding-top:20px; 10 | padding-bottom:10px; 11 | 12 | h3 { 13 | font-size:16px; 14 | margin:0; 15 | padding:0; 16 | } 17 | 18 | h1 { 19 | padding:10px 0 !important; 20 | } 21 | 22 | p.description { 23 | padding:0 0 10px 0; 24 | margin:0; 25 | line-height:180%; 26 | 27 | a.source-link { 28 | display:block; 29 | font-weight:bold; 30 | } 31 | } 32 | 33 | } /* #header-boddy */ 34 | 35 | } /* #header */ 36 | 37 | #content { 38 | 39 | .number { font-weight:bold; } 40 | 41 | #map { 42 | width:440px; 43 | height:440px; 44 | border:1px solid #ccc; 45 | margin-bottom: 30px; 46 | } 47 | 48 | #network { 49 | position:absolute; 50 | top:0; 51 | right:0; 52 | width:440px; 53 | height:440px; 54 | border:1px solid #ccc; 55 | } 56 | 57 | .close-matches { 58 | padding-bottom:30px; 59 | line-height:35px; 60 | 61 | a { 62 | padding-right:10px; 63 | } 64 | 65 | } /* .close-matches */ 66 | 67 | .references table { 68 | width:100%; 69 | border-collapse:collapse; 70 | margin-bottom:80px; 71 | 72 | tr { 73 | border-color:#e2e2e2; 74 | border-style:solid; 75 | border-width:1px 0; 76 | } /* tr */ 77 | 78 | th, td { 79 | font-size:13px; 80 | padding:6px 10px 6px 10px; 81 | text-align:left; 82 | } /* th, td */ 83 | 84 | } /* .references table */ 85 | 86 | } /* #content */ 87 | 88 | svg { 89 | 90 | circle { 91 | fill:#ccc; 92 | stroke:#fff; 93 | stroke-width:1.5px; 94 | } 95 | 96 | circle.pleiades { fill:#1f77b4; } 97 | circle.dare { fill:#f58929; } 98 | circle.idai { fill:#2ca02c; } 99 | circle.vici { fill:#d33d3d; } 100 | circle.pastplace { fill:#9467bd; } 101 | circle.trismegistos { fill:#bcbd22; } 102 | circle.nomisma { fill:#17becf; } 103 | circle.virtual { stroke:#a2a2a2; fill:#fff; } 104 | 105 | .link { 106 | stroke-width:1.5px; 107 | stroke:#a2a2a2; 108 | } 109 | 110 | .virtual { 111 | stroke-dasharray: 0,2 1; 112 | } 113 | 114 | marker { 115 | fill:#a2a2a2; 116 | } 117 | 118 | text { 119 | fill:#3f3f3f; 120 | font:12px sans-serif; 121 | pointer-events: none; 122 | text-shadow:0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search/_filterpanel.less: -------------------------------------------------------------------------------- 1 | #filter-panel { 2 | position:absolute; 3 | bottom:0; 4 | left:420px; 5 | right:0; 6 | height:140px; 7 | background-color:rgba(255,255,255,0.6); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search/_map.less: -------------------------------------------------------------------------------- 1 | #map { 2 | position:absolute; 3 | top:44px; 4 | bottom:0; 5 | left:420px; 6 | right:0; 7 | } 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search/_pagination.less: -------------------------------------------------------------------------------- 1 | @import "../globals.less"; 2 | 3 | .pagination { 4 | color:#4372a9; 5 | margin-bottom:30px; 6 | 7 | ul { 8 | display:inline; 9 | margin:0; 10 | padding:0; 11 | list-style-type:none; 12 | font-size:13px; 13 | 14 | li { 15 | display:inline-block; 16 | text-align:center; 17 | margin:0 5px 0 0; 18 | 19 | a { 20 | padding:6px; 21 | display:block; 22 | text-decoration:none; 23 | color:#4372a9; 24 | } 25 | 26 | } /* li */ 27 | 28 | li.page { 29 | background-color:#4372a9; 30 | border:1px solid #4372a9; 31 | min-width:26px; 32 | 33 | a { color:#fff; } 34 | 35 | } 36 | 37 | li.page:hover { 38 | background-color:#5d8bbf; 39 | } 40 | 41 | li.page.active { 42 | background-color:#fff; 43 | color:#4372a9; 44 | border:1px solid #4372a9; 45 | 46 | a { 47 | cursor:default; 48 | color:#4372a9; 49 | } 50 | } 51 | 52 | li.first, li.prev, li.next, li.last { 53 | font-family:"Font Awesome"; 54 | } 55 | 56 | li.disabled { 57 | 58 | a { 59 | cursor:default; 60 | color:#ccc; 61 | } 62 | 63 | } 64 | 65 | 66 | } /* ul */ 67 | 68 | } /* .pagination */ 69 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search/_searchresults.less: -------------------------------------------------------------------------------- 1 | #results-list { 2 | position:absolute; 3 | top:44px; 4 | bottom:0; 5 | left:0; 6 | width:420px; 7 | background-color:#cfcfcf; 8 | padding:0; 9 | box-sizing:border-box; 10 | .box-shadow-inset(-1px, 2px, 1px, rgba(0,0,0,0.25)); 11 | overflow-y:scroll; 12 | 13 | li { 14 | margin:0 1px 1px 0; 15 | line-height:150%; 16 | padding:6px; 17 | height:100px; 18 | background-color:#f2f2f2; 19 | .box-shadow(1px, 1px, 1px, 0, rgba(0,0,0,0.25)); 20 | .rounded-corners(1px); 21 | 22 | p { 23 | margin:0; 24 | padding:0; 25 | } 26 | 27 | .title { 28 | font-weight:bold; 29 | font-size:14px; 30 | 31 | .category { 32 | font-weight:normal; 33 | padding-left:2px; 34 | color:#8f8f8f; 35 | } 36 | } 37 | 38 | } /** li **/ 39 | 40 | } 41 | 42 | /* 43 | 44 | list-style-type:none; 45 | padding:0; 46 | margin:0; 47 | margin-right:330px; 48 | */ 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/controllers/AnnotatedThingController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import controllers.common.JSONWrites._ 4 | import global.Global 5 | import models.Associations 6 | import models.adjacency.PlaceAdjacencys 7 | import models.core.{ Annotations, AnnotatedThings } 8 | import play.api.db.slick._ 9 | import play.api.libs.json.{ Json, JsObject, JsString, Writes } 10 | 11 | object AnnotatedThingController extends AbstractController { 12 | 13 | def listAll(limit: Int, offset: Int) = loggingAction { implicit session => 14 | jsonOk(Json.toJson(AnnotatedThings.listAll(false, offset, limit)), session.request) 15 | } 16 | 17 | def listPlaceVectors(limit: Int, offset: Int) = DBAction { implicit session => 18 | val things = AnnotatedThings.listAll(true, offset, limit) 19 | val vectors = Associations.findPlaceVectorsForThings(things.items.map(_.id)) 20 | 21 | val response = things.items.map(thing => { 22 | thing.id + ";" + 23 | thing.title + ";" + 24 | thing.dataset + ";" + 25 | Annotations.countByAnnotatedThing(thing.id, true) + ";" + 26 | vectors.get(thing.id).map(_.mkString(",")).getOrElse("") 27 | }).mkString("\n") 28 | 29 | Ok(response) 30 | } 31 | 32 | def getAnnotatedThing(id: String) = DBAction { implicit session => 33 | val thing = AnnotatedThings.findById(id) 34 | if (thing.isDefined) { 35 | // Hack 36 | val fulltext = Global.index.findById(id).flatMap(_.fulltext) 37 | val json = 38 | if (fulltext.isDefined) 39 | Json.toJson(thing.get).as[JsObject] ++ Json.obj("fulltext" -> fulltext.get) 40 | else 41 | Json.toJson(thing.get) 42 | 43 | jsonOk(json, session.request) 44 | } else { 45 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 46 | } 47 | } 48 | 49 | def listSubThings(id: String, limit: Int, offset: Int) = loggingAction { implicit session => 50 | val subItems = AnnotatedThings.listChildren(id) 51 | jsonOk(Json.toJson(subItems), session.request) 52 | } 53 | 54 | def listPlaces(id: String, limit: Int, offset: Int) = loggingAction { implicit session => 55 | val places = Associations.findPlacesForThing(id) 56 | jsonOk(Json.toJson(places), session.request) 57 | } 58 | 59 | def listAnnotations(id: String, limit: Int, offset: Int) = loggingAction { implicit session => 60 | val annotatedThing = AnnotatedThings.findById(id) 61 | if (annotatedThing.isDefined) 62 | jsonOk(Json.toJson(Annotations.findByAnnotatedThing(id)), session.request) 63 | else 64 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 65 | } 66 | 67 | def getAdjacencyGraph(id: String) = loggingAction { implicit session => 68 | val annotatedThing = AnnotatedThings.findById(id) 69 | if (annotatedThing.isDefined) { 70 | jsonOk(Json.toJson(PlaceAdjacencys.findByAnnotatedThingRecursive(id)), session.request) 71 | } else { 72 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/controllers/AnnotationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import controllers.common.JSONWrites._ 4 | import java.util.UUID 5 | import models.core.Annotations 6 | import play.api.db.slick._ 7 | import play.api.libs.json.Json 8 | 9 | object AnnotationController extends AbstractController { 10 | 11 | def listAll(limit: Int, offset: Int) = loggingAction { implicit session => 12 | jsonOk(Json.toJson(Annotations.listAll()), session.request) 13 | } 14 | 15 | def getAnnotation(id: UUID) = loggingAction { implicit session => 16 | val annotation = Annotations.findByUUID(id) 17 | if (annotation.isDefined) 18 | jsonOk(Json.toJson(annotation.get), session.request) 19 | else 20 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/controllers/DatasetController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import controllers.common.JSONWrites._ 4 | import models.Associations 5 | import models.core.{ AnnotatedThings, Datasets } 6 | import play.api.db.slick._ 7 | import play.api.libs.json.{ Json, JsValue } 8 | 9 | object DatasetController extends AbstractController { 10 | 11 | def listAll(limit: Int, offset: Int) = loggingAction { implicit session => 12 | jsonOk(Json.toJson(Datasets.listAll(true, offset, limit)), session.request) 13 | } 14 | 15 | def getDataset(id: String) = loggingAction { implicit session => 16 | val dataset = Datasets.findById(id) 17 | if (dataset.isDefined) 18 | jsonOk(Json.toJson(dataset.get), session.request) 19 | else 20 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 21 | } 22 | 23 | def getTemporalProfile(id: String) = loggingAction { implicit session => 24 | val dataset = Datasets.findById(id) 25 | if (dataset.isDefined) 26 | jsonOk(Json.parse(dataset.get.temporalProfile.getOrElse("{}")), session.request) 27 | else 28 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 29 | } 30 | 31 | def listAnnotatedThings(id: String, limit: Int, offset: Int) = loggingAction { implicit session => 32 | val dataset = Datasets.findById(id) 33 | if (dataset.isDefined) 34 | jsonOk(Json.toJson(AnnotatedThings.findByDataset(id, true, true, offset, limit)), session.request) 35 | else 36 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 37 | } 38 | 39 | def listPlaces(id: String, limit: Int, offset: Int) = loggingAction { implicit session => 40 | val places = Associations.findPlacesInDataset(id, offset, limit) 41 | 42 | implicit val verbose = session.request.queryString 43 | .filter(_._1.toLowerCase.equals("verbose")) 44 | .headOption.flatMap(_._2.headOption.map(_.toBoolean)).getOrElse(true) 45 | 46 | jsonOk(Json.toJson(places), session.request) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/controllers/PlaceController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import controllers.common.JSONWrites._ 4 | import global.Global 5 | import index.{ Index, SearchParameters } 6 | import index.places.IndexedPlace 7 | import models.Associations 8 | import models.geo.Gazetteers 9 | import models.core.Dataset 10 | import play.api.mvc.Action 11 | import play.api.db.slick._ 12 | import play.api.libs.json.Json 13 | import play.api.Logger 14 | 15 | object PlaceController extends AbstractController { 16 | 17 | private val SOURCE_DATASET = "source_dataset" 18 | 19 | def listGazetteers(limit: Int, offset: Int) = loggingAction { implicit session => 20 | jsonOk(Json.toJson(Gazetteers.listAll(offset, limit)), session.request) 21 | } 22 | 23 | def getGazetteer(gazetteerName: String) = loggingAction { implicit session => 24 | Gazetteers.findByNameWithPrefixes(gazetteerName) match { 25 | case Some(tuple) => jsonOk(Json.toJson(tuple), session.request) 26 | case _ => NotFound(Json.parse("{ \"message\": \"Gazetteer not found.\" }")) 27 | } 28 | } 29 | 30 | /** 31 | * TODO revise! 32 | */ 33 | def listPlaces(gazetteerName:String, bbox: Option[String], limit: Option[Int], offset: Option[Int]) = loggingAction { implicit session => 34 | // Map BBox coordinates 35 | val bboxTupled = bbox.flatMap(str => { 36 | val coords = str.split(",").map(_.trim) 37 | try { 38 | Some((coords(0).toDouble, coords(1).toDouble, coords(2).toDouble, coords(3).toDouble)) 39 | } catch { 40 | case _: Throwable => None 41 | } 42 | }) 43 | 44 | val gazetteer = Gazetteers.findByName(gazetteerName) 45 | if (gazetteer.isDefined) { 46 | val allPlaces = Global.index.listAllPlaces(gazetteer.get.name.toLowerCase, bboxTupled, offset.getOrElse(0), limit.getOrElse(20)) 47 | jsonOk(Json.toJson(allPlaces.map(_.asJson)), session.request) 48 | } else { 49 | NotFound(Json.parse("{ \"message\": \"Place not found.\" }")) 50 | } 51 | } 52 | 53 | /** Detail information about a place. 54 | * 55 | * Includes the cross-gazetteer network graph, and an overview of the data linked 56 | * to the place. 57 | */ 58 | def getPlace(uri: String, datasetLimit: Int) = loggingAction { implicit session => 59 | val placeNetwork = Global.index.findNetworkByPlaceURI(uri) 60 | if (placeNetwork.isDefined) { 61 | val params = SearchParameters.forPlace(uri, 1, 0) 62 | val (_, facetTree, _, _, _) = Global.index.search( 63 | params, 64 | true, // facets 65 | false, // snippets 66 | false, // time histogram 67 | 0, // top places 68 | false, // heatmap 69 | false) // Only with images 70 | 71 | val sourceFacetValues = facetTree.get.getTopChildren(SOURCE_DATASET, datasetLimit) 72 | val topDatasets = sourceFacetValues.map { case (labelAndId, count) => { 73 | val split = labelAndId.split('#') 74 | (split(0), split(1), count) 75 | }} 76 | 77 | jsonOk(Json.toJson((placeNetwork.get, topDatasets)), session.request) 78 | } else { 79 | NotFound(Json.parse("{ \"message\": \"Not found\" }")) 80 | } 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /app/controllers/admin/AnalyticsController.scala: -------------------------------------------------------------------------------- 1 | package controllers.admin 2 | 3 | import models.AccessLog 4 | import play.api.db.slick._ 5 | import play.api.mvc.Controller 6 | import models.AccessLogAnalytics 7 | 8 | object AnalyticsController extends Controller with Secured { 9 | 10 | def index() = adminAction { username => implicit requestWithSession => 11 | // TODO only a temporary hack! 12 | val allLogRecords = AccessLog.listAll() 13 | Ok(views.html.admin.accessLog(new AccessLogAnalytics(allLogRecords), allLogRecords.take(30))) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/controllers/admin/AuthController.scala: -------------------------------------------------------------------------------- 1 | package controllers.admin 2 | 3 | import play.api.Play 4 | import play.api.Play.current 5 | import play.api.data.Form 6 | import play.api.data.Forms._ 7 | import play.api.mvc.{ Session => PlaySession, _ } 8 | import play.api.db.slick._ 9 | 10 | object AuthController extends Controller { 11 | 12 | val adminUser = Play.current.configuration.getString("admin.user").getOrElse("admin") 13 | val adminPassword = Play.current.configuration.getString("admin.password").getOrElse("admin") 14 | 15 | val loginForm = Form( 16 | tuple( 17 | "username" -> text, 18 | "password" -> text 19 | ) verifying ("Invalid email or password", result => result match { 20 | case (username, password) => { 21 | username.equals(adminUser) & password.equals(adminPassword) 22 | } 23 | }) 24 | ) 25 | 26 | def login = Action { implicit request => 27 | Ok(views.html.admin.login(loginForm)) 28 | } 29 | 30 | def authenticate = Action { implicit request => 31 | loginForm.bindFromRequest.fold( 32 | formWithErrors => BadRequest(views.html.admin.login(formWithErrors)), 33 | user => Redirect(controllers.admin.routes.DatasetAdminController.index()).withSession("username" -> user._1) 34 | ) 35 | } 36 | 37 | def logout = Action { 38 | Redirect(routes.AuthController.login).withNewSession.flashing( 39 | "success" -> "You've been logged out" 40 | ) 41 | } 42 | 43 | } 44 | 45 | trait Secured { 46 | 47 | private def username(request: RequestHeader) = request.session.get(Security.username) 48 | 49 | private def onUnauthorized(request: RequestHeader) = Results.Forbidden("Not Authorized.") 50 | 51 | def adminAction(f: => String => DBSessionRequest[AnyContent] => SimpleResult) = { 52 | Security.Authenticated(username, onUnauthorized) { username => 53 | DBAction(BodyParsers.parse.anyContent)(rs => f(username)(rs)) 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /app/controllers/admin/BaseUploadController.scala: -------------------------------------------------------------------------------- 1 | package controllers.admin 2 | 3 | import play.api.Logger 4 | import play.api.mvc.{ AnyContent, Controller, SimpleResult } 5 | import play.api.mvc.MultipartFormData.FilePart 6 | import play.api.db.slick.DBSessionRequest 7 | import play.api.libs.json.Json 8 | import play.api.libs.Files.TemporaryFile 9 | 10 | class BaseUploadController extends Controller { 11 | 12 | /** Generic boiler plate code needed for file upload **/ 13 | protected def processUpload(formFieldName: String, requestWithSession: DBSessionRequest[AnyContent], action: FilePart[TemporaryFile] => SimpleResult) = { 14 | val formData = requestWithSession.request.body.asMultipartFormData 15 | if (formData.isDefined) { 16 | try { 17 | Logger.info("Processing upload...") 18 | val f = formData.get.file(formFieldName) 19 | if (f.isDefined) 20 | action(f.get) // This is where we execute the handler - the rest is sanity checking boilerplate 21 | else 22 | BadRequest(Json.parse("{\"message\": \"Invalid form data - missing file\"}")) 23 | } catch { 24 | case t: Throwable => { 25 | t.printStackTrace() 26 | Redirect(routes.DatasetAdminController.index).flashing("error" -> { "There is something wrong with the upload: " + t.getMessage }) 27 | } 28 | } 29 | } else { 30 | BadRequest(Json.parse("{\"message\": \"Invalid form data\"}")) 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/controllers/admin/GazetteerAdminController.scala: -------------------------------------------------------------------------------- 1 | package controllers.admin 2 | 3 | import java.io.File 4 | import global.Global 5 | import ingest.harvest.GazetteerImporter 6 | import models.geo.{ Gazetteers, Gazetteer } 7 | import play.api.db.slick._ 8 | import play.api.mvc.Controller 9 | import play.api.Logger 10 | import play.api.libs.Files 11 | import play.api.libs.json.Json 12 | import play.api.libs.concurrent.Execution.Implicits._ 13 | 14 | object GazetteerAdminController extends BaseUploadController with Secured { 15 | 16 | def index = adminAction { username => implicit requestWithSession => 17 | Ok(views.html.admin.gazetteers()) 18 | } 19 | 20 | def deleteGazetteer(name: String) = adminAction { username => implicit requestWithSession => 21 | val gazetteer = Gazetteers.findByName(name) 22 | if (gazetteer.isDefined) { 23 | Logger.info("Deleting gazetteer: " + name) 24 | 25 | Gazetteers.delete(gazetteer.get.name) 26 | 27 | Global.index.deleteGazetter(gazetteer.get.name.toLowerCase) 28 | Logger.info("Done.") 29 | Status(200) 30 | } else { 31 | NotFound 32 | } 33 | } 34 | 35 | def uploadGazetteerDump = adminAction { username => implicit requestWithSession => 36 | val json = requestWithSession.request.body.asJson 37 | if (json.isDefined) { 38 | val url = (json.get \ "url").as[String] 39 | Logger.info("Importing from " + url + " not implemented yet") 40 | 41 | // TODO implement! 42 | 43 | Ok(Json.parse("{ \"message\": \"Not implemented yet.\" }")) 44 | } else { 45 | processUpload("rdf", requestWithSession, { filepart => { 46 | // Original name of the uploaded file 47 | val filename = filepart.filename 48 | val gazetteerName = filename.substring(0, filename.indexOf(".")) 49 | 50 | // Play apparently removes the file after first read... But ingest will 51 | // need to read the file twice (once to count the places, second to import 52 | // them) so we create a copy here 53 | val tempFile = filepart.ref.file 54 | val copy = new File(tempFile.getAbsolutePath + "_cp") 55 | Files.copyFile(tempFile, copy, true, true) 56 | 57 | val importer = new GazetteerImporter(Global.index) 58 | val future = importer.importDataFileAsync(copy.getAbsolutePath, gazetteerName, Some(filename)) 59 | future.onComplete(_ => { 60 | Logger.info("Deleting file " + copy.getAbsolutePath) 61 | copy.delete() 62 | }) 63 | 64 | Redirect(routes.GazetteerAdminController.index).flashing("success" -> { "Import in progress." }) 65 | }}) 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /app/controllers/experimental/ExperimentalPagesController.scala: -------------------------------------------------------------------------------- 1 | package controllers.experimental 2 | 3 | import controllers.AbstractController 4 | import global.Global 5 | import models.Associations 6 | import play.api.db.slick._ 7 | 8 | object ExperimentalPagesController extends AbstractController { 9 | 10 | def getAdjacencyGraph(id: String) = loggingAction { implicit session => 11 | Ok(views.html.placeAdjacencyHack(id)) 12 | } 13 | 14 | def listItemVectors(limit: Int, offset: Int) = DBAction { implicit session => 15 | // val places = Global.index.listAllPlaceNetworks(offset, limit).flatMap(_.places).map(p => (p.label, p.uri)) 16 | val vectors = Associations.findThingVectorsForPlaces() 17 | val response = vectors.keySet.map(uri => { 18 | val place = Global.index.findPlaceByURI(uri) 19 | 20 | uri + ";" + 21 | place.map(_.label).getOrElse("?") + ";" + 22 | vectors.get(uri).map(_.mkString(",")).getOrElse("") 23 | }).mkString("\n") 24 | 25 | Ok(response) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/controllers/pages/AnnotatedThingPagesController.scala: -------------------------------------------------------------------------------- 1 | package controllers.pages 2 | 3 | import models.Associations 4 | import models.core.{ AnnotatedThings, Datasets, Images } 5 | import play.api.db.slick._ 6 | import play.api.mvc.Controller 7 | import controllers.AbstractController 8 | 9 | object AnnotatedThingPagesController extends AbstractController { 10 | 11 | def showAnnotatedThing(id: String) = loggingAction { implicit session => 12 | val thing = AnnotatedThings.findById(id) 13 | if (thing.isDefined) { 14 | val images = Images.findByAnnotatedThing(id) 15 | val datasetHierarchy = Datasets.findByIds(thing.get.dataset +: Datasets.getParentHierarchy(thing.get.dataset)).reverse 16 | Ok(views.html.annotatedThingDetails(thing.get, images, datasetHierarchy)) 17 | } else { 18 | NotFound 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/controllers/pages/DatasetPagesController.scala: -------------------------------------------------------------------------------- 1 | package controllers.pages 2 | 3 | import models.Associations 4 | import models.core.{ Annotations, AnnotatedThings, Datasets } 5 | import play.api.db.slick._ 6 | import play.api.mvc.Controller 7 | import controllers.AbstractController 8 | 9 | object DatasetPagesController extends AbstractController { 10 | 11 | def listAll = loggingAction { implicit session => 12 | val datasets = Datasets.countAll() 13 | val things = AnnotatedThings.countAll(true) 14 | val annotations = Annotations.countAll 15 | Ok(views.html.datasetList(datasets, things, annotations)) 16 | } 17 | 18 | def showDataset(id: String) = loggingAction { implicit session => 19 | val dataset = Datasets.findById(id) 20 | if (dataset.isDefined) { 21 | val id = dataset.get.id 22 | val things = AnnotatedThings.countByDataset(id) 23 | val places = Associations.countPlacesInDataset(id) 24 | val annotations = Annotations.countByDataset(id) 25 | val supersets = Datasets.findByIds(Datasets.getParentHierarchy(id)) 26 | val subsets = Datasets.listSubsets(id) 27 | Ok(views.html.datasetDetails(dataset.get, things, annotations, places, supersets, subsets)) 28 | } else { 29 | NotFound // TODO create decent 'not found' page 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/controllers/pages/LandingPageController.scala: -------------------------------------------------------------------------------- 1 | package controllers.pages 2 | 3 | import models.core.{ AnnotatedThings, Datasets } 4 | import models.geo.Gazetteers 5 | import global.Global 6 | import play.api.mvc.{ Action, Controller } 7 | import play.api.db.slick._ 8 | import play.api.Logger 9 | import index.objects.IndexedObjectTypes 10 | import index.Index 11 | import controllers.AbstractController 12 | 13 | object LandingPageController extends AbstractController { 14 | 15 | def index() = loggingAction { implicit session => 16 | // Placeholder for a future landing page - for now we just redirect to the map 17 | // Redirect(controllers.pages.routes.LandingPageController.map()) 18 | Ok(views.html.landingPage()) 19 | } 20 | 21 | def map() = loggingAction { implicit session => 22 | Ok(views.html.map()) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/controllers/pages/PlacePagesController.scala: -------------------------------------------------------------------------------- 1 | package controllers.pages 2 | 3 | import global.Global 4 | import index.Index 5 | import models.geo.Gazetteers 6 | import play.api.db.slick._ 7 | import play.api.mvc.{ Action, Controller } 8 | import controllers.AbstractController 9 | 10 | object PlacePagesController extends AbstractController { 11 | 12 | def listGazetteers() = DBAction { implicit session => 13 | // TODO implement 14 | Ok("") 15 | } 16 | 17 | def showGazetteer(name: String) = Action { 18 | Ok(views.html.showGazetteer(name)) 19 | } 20 | 21 | def showPlace(uri: String) = loggingAction { implicit session => 22 | val network = Global.index.findNetworkByPlaceURI(Index.normalizeURI(uri)) 23 | if (network.isDefined) { 24 | Ok(views.html.placeDetails(network.flatMap(_.getPlace(uri)).get, network.get)) 25 | } else { 26 | NotFound 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/index/FacetTree.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import models.core.Dataset 4 | import org.apache.lucene.facet.Facets 5 | import org.apache.lucene.facet.taxonomy.FastTaxonomyFacetCounts 6 | import scala.collection.JavaConverters._ 7 | import play.api.Logger 8 | 9 | /** A helper datastructure for easier access to search result facets **/ 10 | class FacetTree(facetCounts: Facets) { 11 | 12 | def dimensions(limit: Int = 10): Seq[String] = { 13 | // Warning: results can be an array with a single null value - need to catch this 14 | val results = facetCounts.getAllDims(limit).asScala 15 | results.flatMap(Option(_)).map(_.dim).toSeq 16 | } 17 | 18 | def getTopChildren(dimension: String, limit: Int = 10, path: Seq[String] = Seq.empty[String]): Seq[(String, Int)] = 19 | Option(facetCounts.getTopChildren(limit, dimension, path:_*)).map(result => 20 | result.labelValues.toSeq.map(lv => (lv.label, lv.value.intValue))).getOrElse(Seq.empty[(String, Int)]) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/index/Heatmap.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | class Heatmap(val cells: Seq[(Double, Double, Int)], val cellWidth: Double, val cellHeight: Double) { 4 | 5 | private lazy val values = cells.map(_._3) 6 | 7 | lazy val maxValue = if (values.size > 0) values.max else 0 8 | 9 | lazy val minValue = if (values.size > 0) values.min else 0 10 | 11 | def +(other: Heatmap): Heatmap = { 12 | val combined = 13 | (cells ++ other.cells) // concatenate 14 | .groupBy(t => (t._1, t._2)) // group by (lon, lat) 15 | .map { case (lonLat, tuple) => (lonLat._1, lonLat._2, tuple.map(_._3).sum) } // sum per cell 16 | .toSeq 17 | 18 | Heatmap(combined, cellWidth, cellHeight) // TODO throw exception if cell dimensions differ! 19 | } 20 | 21 | def isEmpty = cells.isEmpty 22 | 23 | } 24 | 25 | object Heatmap { 26 | 27 | def apply(cells: Seq[(Double, Double, Int)], cellWidth: Double, cellHeight: Double) = new Heatmap(cells, cellWidth, cellHeight) 28 | 29 | def empty = new Heatmap(Seq.empty[(Double, Double, Int)], 0, 0) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/index/IndexFields.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import org.apache.lucene.document.{ FieldType, TextField } 4 | 5 | object IndexFields { 6 | 7 | /** Fields for internal use **/ 8 | 9 | val BOOST = "boost" 10 | 11 | 12 | /** General fields **/ 13 | 14 | val ID = "id" 15 | 16 | val TITLE = "title" 17 | 18 | val DESCRIPTION = "description" 19 | 20 | val OBJECT_TYPE = "type" 21 | 22 | val SOURCE_DATASET = "source_dataset" 23 | 24 | val DATASET_HIERARCHY = "dataset_hierarchy" 25 | 26 | val HOMEPAGE = "homepage" 27 | 28 | val DEPICTION = "depiction" 29 | 30 | val LANGUAGE = "lang" 31 | 32 | val IS_PART_OF = "is_part_of" 33 | 34 | val DATE_FROM = "date_from" 35 | 36 | val DATE_TO = "date_to" 37 | 38 | val DATE_POINT = "date_xy" 39 | 40 | val GEOMETRY = "geometry" 41 | 42 | val BOUNDING_BOX = "bbox" 43 | 44 | val PLACE_URI = "place_uri" 45 | 46 | 47 | /** Item-specific fields **/ 48 | 49 | val ITEM_FULLTEXT = "fulltext" 50 | 51 | 52 | 53 | /** Place-specific fields **/ 54 | 55 | val SEED_URI = "seed_uri" 56 | 57 | val PLACE_NAME = "name" 58 | 59 | val PLACE_MATCH = "match" 60 | 61 | val PLACE_AS_JSON = "place" 62 | 63 | 64 | /** Annotation-specific fields **/ 65 | 66 | val ANNOTATION_THING = "annotated_thing" 67 | 68 | val ANNOTATION_QUOTE = "quote" 69 | 70 | val ANNOTATION_FULLTEXT_PREFIX = "fulltext_prefix" 71 | 72 | val ANNOTATION_FULLTEXT_SUFFIX = "fulltext_suffix" 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/index/NGramAnalyzer.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import java.io.{ Reader, StringReader } 4 | import org.apache.lucene.analysis.Analyzer 5 | import org.apache.lucene.analysis.Analyzer.TokenStreamComponents 6 | import org.apache.lucene.analysis.core.{ LowerCaseFilter, StopAnalyzer, StopFilter } 7 | import org.apache.lucene.analysis.shingle.ShingleFilter 8 | import org.apache.lucene.analysis.standard.StandardTokenizer 9 | import org.apache.lucene.analysis.tokenattributes.CharTermAttribute 10 | import org.apache.lucene.util.Version 11 | import play.api.Logger 12 | import scala.collection.mutable.ListBuffer 13 | 14 | class NGramAnalyzer(val size: Int) extends Analyzer { 15 | 16 | override def createComponents(fieldName: String) = { 17 | val source = new StandardTokenizer() 18 | 19 | val shingleFilter = new ShingleFilter(source, size) 20 | val lowerCaseFilter = new LowerCaseFilter(shingleFilter) 21 | val stopFilter = new StopFilter(lowerCaseFilter, StopAnalyzer.ENGLISH_STOP_WORDS_SET) 22 | 23 | new TokenStreamComponents(source, stopFilter) 24 | } 25 | 26 | } 27 | 28 | object NGramAnalyzer { 29 | 30 | private val CONTENTS = "contents" 31 | 32 | private val UNDERSCORE = "_" 33 | 34 | def tokenize(phrases: Seq[String], size: Int = 3): Seq[String] = { 35 | val reader = new StringReader(phrases.mkString("\n")) 36 | 37 | val stream = new StopAnalyzer().tokenStream(CONTENTS, reader) 38 | val shingleFilter = new ShingleFilter(stream, size) 39 | val lowerCaseFilter = new LowerCaseFilter(shingleFilter) 40 | 41 | val charTermAttribute = lowerCaseFilter.getAttribute(classOf[CharTermAttribute]) 42 | 43 | lowerCaseFilter.reset() 44 | val buffer = ListBuffer.empty[String] 45 | while(lowerCaseFilter.incrementToken) { 46 | // Remove Lucene's '_' stopword markers 47 | val token = charTermAttribute.toString.trim 48 | 49 | if (!token.contains(UNDERSCORE)) { 50 | // No stopword - just add 51 | buffer.append(token) 52 | } else if (token.startsWith(UNDERSCORE) || token.endsWith(UNDERSCORE)) { 53 | // Stopword on start or beginning - do nothing, the N-Gram without the stopword is already in the list 54 | } else { 55 | buffer.append(token.replace(UNDERSCORE, "").replace(" ", " ")) 56 | } 57 | } 58 | 59 | buffer.distinct.toSeq 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/index/SearchParameters.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import index.objects.IndexedObjectTypes 4 | import models.geo.BoundingBox 5 | import com.vividsolutions.jts.geom.Coordinate 6 | 7 | object DateFilterMode extends Enumeration { 8 | 9 | val INTERSECTS, CONTAINS = Value 10 | 11 | } 12 | 13 | /** A wrapper around a full complement of search arguments **/ 14 | case class SearchParameters( 15 | /** Keyword/ phrase query **/ 16 | query: Option[String], 17 | 18 | /** Object type filter **/ 19 | objectTypes: Seq[IndexedObjectTypes.Value], 20 | 21 | /** Inverse object type that excludes specific types **/ 22 | excludeObjectTypes: Seq[IndexedObjectTypes.Value], 23 | 24 | /** Dataset filter **/ 25 | datasets: Seq[String], 26 | 27 | /** Inverse dataset filter that excludes specific sets **/ 28 | excludeDatasets: Seq[String], 29 | 30 | /** Gazetteer filter **/ 31 | gazetteers: Seq[String], 32 | 33 | /** Inverse gazetteer filter that excludes specific gazetteers **/ 34 | excludeGazetteers: Seq[String], 35 | 36 | /** Language filter **/ 37 | languages: Seq[String], 38 | 39 | /** Inverse language filter **/ 40 | excludeLanguages: Seq[String], 41 | 42 | /** Date filter (start year) **/ 43 | from: Option[Int], 44 | 45 | /** Date filter (end year) **/ 46 | to: Option[Int], 47 | 48 | /** Date range filtering mode - match intersecting vs. contained ranges **/ 49 | dateFilterMode: DateFilterMode.Value, 50 | 51 | /** Restriction to specific place **/ 52 | places: Seq[String], 53 | 54 | /** Geo search filter: objects overlapping bounding box **/ 55 | bbox: Option[BoundingBox], 56 | 57 | /** Geo search filter: objects around a coordinate **/ 58 | coord: Option[Coordinate], 59 | 60 | /** Geo search filter: radius around coordinate **/ 61 | radius: Option[Double], 62 | 63 | /** Pagination limit (i.e. max. number of items returned **/ 64 | limit: Int, 65 | 66 | /** Pagination offset (i.e. number of items discarded **/ 67 | offset: Int) { 68 | 69 | private var _error: Option[String] = None 70 | 71 | lazy val error = { 72 | if (_error.isEmpty) 73 | isValid 74 | 75 | _error 76 | } 77 | 78 | /** Query is valid if at least one param is set **/ 79 | def isValid: Boolean = { 80 | val requiresOneOf = 81 | Seq(query, from, to, bbox, coord).map(_.isDefined) ++ 82 | Seq(places, objectTypes, excludeObjectTypes, datasets, excludeDatasets, gazetteers, excludeGazetteers).map(_.size > 0) 83 | 84 | if (requiresOneOf.forall(isTrue => isTrue)) { 85 | _error = Some("at least one of the following parameters is required: query, from, to, bbox, coord, places, objectTypes, datasets, gazetteers") 86 | false 87 | } else if (from.isDefined && to.isDefined && from.get > to.get) { 88 | _error = Some("from parameter must be less than to parameter") 89 | false 90 | } else { 91 | true 92 | } 93 | } 94 | 95 | } 96 | 97 | object SearchParameters { 98 | 99 | /** Helper: parameters for fetching all data for a specific place **/ 100 | def forPlace(uri: String, limit: Int, offset: Int) = 101 | SearchParameters( 102 | None, 103 | Seq.empty[IndexedObjectTypes.Value], 104 | Seq.empty[IndexedObjectTypes.Value], 105 | Seq.empty[String], 106 | Seq.empty[String], 107 | Seq.empty[String], 108 | Seq.empty[String], 109 | Seq.empty[String], 110 | Seq.empty[String], 111 | None, 112 | None, 113 | DateFilterMode.INTERSECTS, 114 | Seq(uri), 115 | None, 116 | None, 117 | None, 118 | limit, 119 | offset) 120 | 121 | } -------------------------------------------------------------------------------- /app/index/TimeHistogram.scala: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | class TimeHistogram private (val values: Seq[(Int, Int)]) { 4 | 5 | val startYear = values.headOption.map(_._1).getOrElse(0) 6 | 7 | val endYear = values.lastOption.map(_._1).getOrElse(0) 8 | 9 | lazy val maxCount = values.map(_._2).max 10 | 11 | } 12 | 13 | object TimeHistogram { 14 | 15 | def create(vals: Seq[(Int, Int)], maxBuckets: Int = -1): TimeHistogram = { 16 | if (vals.size > 0) { 17 | // Lucene delivers a sparse result - so we need to fill in the empty cells before resampling 18 | val paddedValues = vals.sortBy(_._1).foldLeft(Seq.empty[(Int, Int)]) { case (padded, (year, count)) => { 19 | padded.lastOption match { 20 | case Some(previous) => { 21 | if (previous._1 == year - 1) // Nothing missing - no need to append 22 | padded :+ (year, count) 23 | else // Pad cells in between previous and this year 24 | padded ++ Seq.range(previous._1 + 1, year).map((_, 0)) :+ (year, count) 25 | } 26 | 27 | case None => Seq((year, count)) 28 | } 29 | }} 30 | 31 | val resampledValues = 32 | if (maxBuckets < 0) { 33 | paddedValues 34 | } else { 35 | val stepSize = Math.ceil(paddedValues.size.toDouble / maxBuckets).toInt 36 | paddedValues.grouped(stepSize).map(values => (values.head._1, values.map(_._2).sum / values.size)).toSeq 37 | } 38 | 39 | new TimeHistogram(resampledValues) 40 | } else { 41 | new TimeHistogram(Seq.empty[(Int, Int)]) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/index/annotations/AnnotationWriter.scala: -------------------------------------------------------------------------------- 1 | package index.annotations 2 | 3 | import com.vividsolutions.jts.geom.Geometry 4 | import index.Index 5 | import index.places.IndexedPlaceNetwork 6 | import models.core.{ Annotation, AnnotatedThing } 7 | 8 | trait AnnotationWriter extends AnnotationReader { 9 | 10 | def addAnnotations(annotations: Seq[(AnnotatedThing, AnnotatedThing, Annotation, IndexedPlaceNetwork, Option[String], Option[String])]) = 11 | annotations.foreach { case (rootParent, parent, annotation, place, prefix, suffix) => 12 | annotationWriter.addDocument(Index.facetsConfig.build(taxonomyWriter, IndexedAnnotation.toDoc(rootParent, parent, annotation, place, prefix, suffix))) } 13 | 14 | } -------------------------------------------------------------------------------- /app/index/annotations/IndexedAnnotation.scala: -------------------------------------------------------------------------------- 1 | package index.annotations 2 | 3 | // import com.vividsolutions.jts.geom.Geometry 4 | import index.{ Index, IndexFields } 5 | import index.objects.IndexedObjectTypes 6 | import index.places.IndexedPlaceNetwork 7 | import java.util.UUID 8 | import models.core.{ Annotation, AnnotatedThing } 9 | import org.apache.lucene.document.{ Document, Field, IntField, StringField, TextField } 10 | import org.apache.lucene.facet.FacetField 11 | import models.geo.BoundingBox 12 | 13 | class IndexedAnnotation(private val doc: Document) { 14 | 15 | val uuid: UUID = UUID.fromString(doc.get(IndexFields.ID)) 16 | 17 | val dataset: String = doc.get(IndexFields.SOURCE_DATASET) 18 | 19 | val annotatedThing: String = doc.get(IndexFields.ANNOTATION_THING) 20 | 21 | val prefix: Option[String] = Option(doc.get(IndexFields.ANNOTATION_FULLTEXT_PREFIX)) 22 | 23 | val quote: Option[String] = Option(doc.get(IndexFields.ANNOTATION_QUOTE)) 24 | 25 | val suffix: Option[String] = Option(doc.get(IndexFields.ANNOTATION_FULLTEXT_SUFFIX)) 26 | 27 | val text: String = Seq( 28 | Option(doc.get(IndexFields.ANNOTATION_FULLTEXT_PREFIX)), 29 | Option(doc.get(IndexFields.ANNOTATION_QUOTE)), 30 | Option(doc.get(IndexFields.ANNOTATION_FULLTEXT_SUFFIX))).flatten.mkString(" ") 31 | 32 | } 33 | 34 | object IndexedAnnotation { 35 | 36 | def toDoc(rootParent: AnnotatedThing, parent: AnnotatedThing, annotation: Annotation, place: IndexedPlaceNetwork, 37 | fulltextPrefix: Option[String], fulltextSuffix: Option[String]): Document = { 38 | 39 | val doc = new Document() 40 | 41 | // UUID, containing dataset & annotated thing 42 | doc.add(new StringField(IndexFields.ID, annotation.uuid.toString, Field.Store.YES)) 43 | doc.add(new StringField(IndexFields.OBJECT_TYPE, IndexedObjectTypes.ANNOTATION.toString, Field.Store.NO)) 44 | doc.add(new StringField(IndexFields.SOURCE_DATASET, annotation.dataset, Field.Store.YES)) 45 | doc.add(new StringField(IndexFields.ANNOTATION_THING, annotation.annotatedThing, Field.Store.YES)) 46 | 47 | // Thing title and description 48 | doc.add(new TextField(IndexFields.TITLE, rootParent.title, Field.Store.YES)) 49 | rootParent.description.map(description => new TextField(IndexFields.DESCRIPTION, description, Field.Store.YES)) 50 | 51 | // Temporal bounds 52 | parent.temporalBoundsStart.map(start => doc.add(new IntField(IndexFields.DATE_FROM, start, Field.Store.YES))) 53 | parent.temporalBoundsEnd.map(end => doc.add(new IntField(IndexFields.DATE_TO, end, Field.Store.YES))) 54 | parent.temporalBoundsStart.map(start => { 55 | val end = parent.temporalBoundsEnd.getOrElse(start) 56 | val dateRange = 57 | if (start > end) // Minimal safety precaution... 58 | Index.dateRangeTree.parseShape("[" + end + " TO " + start + "]") 59 | else 60 | Index.dateRangeTree.parseShape("[" + start + " TO " + end + "]") 61 | 62 | Index.temporalStrategy.createIndexableFields(dateRange).foreach(doc.add(_)) 63 | }) 64 | 65 | // Text 66 | annotation.quote.map(quote => doc.add(new TextField(IndexFields.ANNOTATION_QUOTE, quote, Field.Store.YES))) 67 | fulltextPrefix.map(text => doc.add(new TextField(IndexFields.ANNOTATION_FULLTEXT_PREFIX, text, Field.Store.YES))) 68 | fulltextSuffix.map(text => doc.add(new TextField(IndexFields.ANNOTATION_FULLTEXT_SUFFIX, text, Field.Store.YES))) 69 | 70 | // Place & geometry 71 | doc.add(new StringField(IndexFields.PLACE_URI, Index.normalizeURI(annotation.gazetteerURI), Field.Store.NO)) 72 | doc.add(new FacetField(IndexFields.PLACE_URI, place.seedURI)) 73 | 74 | // Bounding box to enable efficient best-fit queries 75 | val b = place.geometry.get.getEnvelopeInternal() 76 | Index.bboxStrategy.createIndexableFields(Index.spatialCtx.makeRectangle(b.getMinX, b.getMaxX, b.getMinY, b.getMaxY)).foreach(doc.add(_)) 77 | 78 | doc 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /app/index/objects/ObjectWriter.scala: -------------------------------------------------------------------------------- 1 | package index.objects 2 | 3 | import index._ 4 | import models.core.{ Dataset, AnnotatedThing, Image } 5 | import org.apache.lucene.index.{ IndexWriterConfig, Term } 6 | import org.apache.lucene.search.{ BooleanQuery, BooleanClause, TermQuery } 7 | import play.api.db.slick._ 8 | import index.places.IndexedPlaceNetwork 9 | import play.api.Logger 10 | 11 | trait ObjectWriter extends IndexBase { 12 | 13 | def addAnnotatedThing(annotatedThing: AnnotatedThing, places: Seq[(IndexedPlaceNetwork, String)], images: Seq[Image], fulltext: Option[String], datasetHierarchy: Seq[Dataset])(implicit s: Session) = 14 | addAnnotatedThings(Seq((annotatedThing, places, images, fulltext)), datasetHierarchy) 15 | 16 | def addAnnotatedThings(annotatedThings: Seq[(AnnotatedThing, Seq[(IndexedPlaceNetwork, String)], Seq[Image], Option[String])], datasetHierarchy: Seq[Dataset])(implicit s: Session) = { 17 | var ctr = 0 18 | annotatedThings.foreach { case (thing, places, images, fulltext) => { 19 | ctr += 1 20 | objectWriter.addDocument(Index.facetsConfig.build(taxonomyWriter, IndexedObject.toDoc(thing, places, images, fulltext, datasetHierarchy))) 21 | if (ctr % 1000 == 0) 22 | Logger.info("... " + ctr) 23 | } 24 | } 25 | } 26 | 27 | def addDataset(dataset: Dataset) = addDatasets(Seq(dataset)) 28 | 29 | def addDatasets(datasets: Seq[Dataset]) = 30 | datasets.foreach(dataset => 31 | objectWriter.addDocument(Index.facetsConfig.build(taxonomyWriter, IndexedObject.toDoc(dataset)))) 32 | 33 | def updateDatasets(datasets: Seq[Dataset]) = { 34 | // Delete 35 | datasets.foreach(dataset => { 36 | val q = new BooleanQuery() 37 | q.add(new TermQuery(new Term(IndexFields.OBJECT_TYPE, IndexedObjectTypes.DATASET.toString)), BooleanClause.Occur.MUST) 38 | q.add(new TermQuery(new Term(IndexFields.ID, dataset.id)), BooleanClause.Occur.MUST) 39 | objectWriter.deleteDocuments(q) 40 | }) 41 | 42 | // Add updated versions 43 | datasets.foreach(dataset => 44 | objectWriter.addDocument(Index.facetsConfig.build(taxonomyWriter, IndexedObject.toDoc(dataset)))) 45 | } 46 | 47 | /** Removes the dataset (and all items inside it) from the index **/ 48 | def dropDataset(id: String) = { 49 | // Delete things and annotations for this dataset 50 | annotationWriter.deleteDocuments(new TermQuery(new Term(IndexFields.SOURCE_DATASET, id))) 51 | objectWriter.deleteDocuments(new TermQuery(new Term(IndexFields.SOURCE_DATASET, id))) 52 | 53 | // Delete the dataset 54 | val q = new BooleanQuery() 55 | q.add(new TermQuery(new Term(IndexFields.OBJECT_TYPE, IndexedObjectTypes.DATASET.toString)), BooleanClause.Occur.MUST) 56 | q.add(new TermQuery(new Term(IndexFields.ID, id)), BooleanClause.Occur.MUST) 57 | objectWriter.deleteDocuments(q) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/index/suggest/SuggestIndex.scala: -------------------------------------------------------------------------------- 1 | package index.suggest 2 | 3 | import index.{ IndexFields, NGramAnalyzer } 4 | import java.io.StringReader 5 | import java.nio.file.Path 6 | import org.apache.lucene.analysis.Analyzer 7 | import org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager 8 | import org.apache.lucene.index.{ DirectoryReader, IndexWriterConfig } 9 | import org.apache.lucene.search.SearcherManager 10 | import org.apache.lucene.search.spell.{ LuceneDictionary, SpellChecker, PlainTextDictionary } 11 | import org.apache.lucene.search.suggest.analyzing.AnalyzingSuggester 12 | import org.apache.lucene.store.FSDirectory 13 | import play.api.Logger 14 | import scala.collection.JavaConverters._ 15 | 16 | class SuggestIndex(directory: Path, placeSearcherManager: SearcherTaxonomyManager, objectSearcherManager: SearcherTaxonomyManager, analyzer: Analyzer) { 17 | 18 | protected val spellcheckIndex = FSDirectory.open(directory) 19 | 20 | protected val spellchecker = new SpellChecker(spellcheckIndex) 21 | 22 | lazy val suggester = { 23 | Logger.info("Initializing suggester") 24 | 25 | val reader = DirectoryReader.open(spellcheckIndex) 26 | val dictionary = new LuceneDictionary(reader, SpellChecker.F_WORD) 27 | 28 | val suggester = new AnalyzingSuggester(analyzer) 29 | // val suggester = new AnalyzingInfixSuggester(Version.LATEST, FSDirectory.open(new File(directory.getParent, "infix-suggester")), analyzer) 30 | 31 | // val suggester = new FuzzySuggester(analyzer, analyzer, 32 | // AnalyzingSuggester.EXACT_FIRST, 256, -1, true, 1, true, 33 | // 3, FuzzySuggester.DEFAULT_MIN_FUZZY_LENGTH, false) 34 | 35 | suggester.build(dictionary) 36 | reader.close() 37 | 38 | Logger.info("Suggester initialized") 39 | suggester 40 | } 41 | 42 | /** (Re-)builds the spellcheck index **/ 43 | def build() = { 44 | Logger.info("Building suggest index") 45 | 46 | spellchecker.clearIndex() 47 | 48 | val placeSearcher = placeSearcherManager.acquire() 49 | val objectSearcher = objectSearcherManager.acquire() 50 | 51 | val dictionarySources = 52 | // Relevant fields from the place index 53 | Seq(IndexFields.TITLE, IndexFields.PLACE_NAME, IndexFields.DESCRIPTION).map((_, placeSearcher.searcher.getIndexReader)) ++ 54 | // Relevant fields from the object index 55 | Seq(IndexFields.TITLE, IndexFields.DESCRIPTION).map((_, objectSearcher.searcher.getIndexReader)) 56 | 57 | try { 58 | dictionarySources.foreach { case (fieldName, reader) => 59 | spellchecker.indexDictionary(new LuceneDictionary(reader, fieldName), new IndexWriterConfig(analyzer), true) 60 | } 61 | } finally { 62 | placeSearcherManager.release(placeSearcher) 63 | objectSearcherManager.release(objectSearcher) 64 | } 65 | 66 | Logger.info("Suggest index updated") 67 | } 68 | 69 | def addTerms(terms: Seq[String]) = { 70 | val nGrams = NGramAnalyzer.tokenize(terms) 71 | val dictionary = new PlainTextDictionary(new StringReader(nGrams.mkString("\n"))) 72 | spellchecker.indexDictionary(dictionary, new IndexWriterConfig(analyzer), true) 73 | } 74 | 75 | def suggestCompletion(query: String, limit: Int): Seq[String] = 76 | suggester.lookup(query, false, limit).asScala.map(_.key.toString) // .sortBy(_.size) 77 | 78 | def suggestSimilar(query: String, limit: Int): Seq[String] = 79 | spellchecker.suggestSimilar(query, limit) 80 | 81 | def close() = { 82 | // suggester.close() 83 | spellchecker.close() 84 | spellcheckIndex.close() 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /app/ingest/VoIDImporter.scala: -------------------------------------------------------------------------------- 1 | package ingest 2 | 3 | import global.Global 4 | import java.io.FileInputStream 5 | import java.sql.Date 6 | import models.core.{ Dataset, Datasets } 7 | import org.pelagios.Scalagios 8 | import org.pelagios.api.dataset.{ Dataset => VoIDDataset } 9 | import play.api.db.slick._ 10 | import play.api.Logger 11 | import play.api.libs.Files.TemporaryFile 12 | 13 | object VoIDImporter extends AbstractImporter { 14 | 15 | def readVoID(file: TemporaryFile, filename: String): Seq[VoIDDataset]= { 16 | Logger.info("Reading VoID file: " + filename) 17 | val is = new FileInputStream(file.file) 18 | val datasets = Scalagios.readVoID(is, filename).toSeq 19 | is.close() 20 | datasets 21 | } 22 | 23 | def importVoID(file: TemporaryFile, filename: String, uri: Option[String] = None)(implicit s: Session): Seq[(Dataset, Seq[String])] = 24 | importVoID(readVoID(file, filename), uri) 25 | 26 | def importVoID(topLevelDatasets: Seq[VoIDDataset], uri: Option[String])(implicit s: Session): Seq[(Dataset, Seq[String])]= { 27 | // Helper to compute an ID for the dataset 28 | def id(dataset: VoIDDataset) = 29 | if (dataset.uri.startsWith("http://")) { 30 | sha256(dataset.uri) 31 | } else { 32 | sha256(dataset.title + " " + dataset.publisher) 33 | } 34 | 35 | // Helper to flatten the hierachy (of VoIDDatasets) into a list of (API) Datasets 36 | def flattenHierarchy(datasets: Seq[VoIDDataset], parent: Option[Dataset] = None): Seq[(Dataset, Seq[String])] = { 37 | val created = new Date(System.currentTimeMillis) 38 | 39 | val datasetEntities = datasets.map(d => { 40 | val publisher = 41 | if (d.publisher.isDefined) 42 | d.publisher.get 43 | else 44 | parent.map(_.publisher).getOrElse("[NO PUBLISHER]") 45 | 46 | val license = 47 | if (d.license.isDefined) 48 | d.license.get 49 | else 50 | parent.map(_.license).getOrElse("[NO LICENSE]") 51 | 52 | val datasetEntity = Dataset(id(d), d.title, publisher, license, created, created, 53 | uri, d.description, d.homepage, parent.map(_.id), 54 | None, None, None, None) 55 | 56 | (d, datasetEntity, d.datadumps) 57 | }) 58 | 59 | datasetEntities.map(t => (t._2, t._3)) ++ datasetEntities.flatMap { case (d, entity, dumpfiles) 60 | => flattenHierarchy(d.subsets, Some(entity)) } 61 | } 62 | 63 | val datasetsWithDumpfiles = flattenHierarchy(topLevelDatasets) 64 | val datasets = datasetsWithDumpfiles.map(_._1) 65 | Datasets.insertAll(datasets) 66 | 67 | datasets.foreach(Global.index.addDataset(_)) 68 | 69 | val titlesAndDescriptions = datasets.flatMap(d => Seq(Some(d.title), d.description).flatten).distinct 70 | Global.index.suggester.addTerms(titlesAndDescriptions) 71 | Global.index.refresh() 72 | 73 | datasetsWithDumpfiles 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/ingest/harvest/DataHarvester.scala: -------------------------------------------------------------------------------- 1 | package ingest.harvest 2 | 3 | import akka.pattern.ask 4 | import akka.util.Timeout 5 | import java.util.UUID 6 | import models.core.Dataset 7 | import play.api.Play.current 8 | import play.api.Logger 9 | import play.api.libs.concurrent.Akka 10 | import play.api.libs.concurrent.Execution.Implicits._ 11 | import akka.actor.{ Actor, ActorRef, Props } 12 | import scala.collection.mutable.ArrayBuffer 13 | import scala.concurrent.duration._ 14 | 15 | class DataHarvestActor(harvestId: UUID, voidURL: String, previous: Seq[Dataset]) extends Actor { 16 | 17 | def receive = { 18 | 19 | case Start => { 20 | new DataHarvestWorker().harvest(voidURL, previous) 21 | sender ! Stopped(true) 22 | } 23 | 24 | } 25 | 26 | } 27 | 28 | object DataHarvester { 29 | 30 | private val actors = new scala.collection.mutable.HashSet[ActorRef] 31 | 32 | def harvest(voidURL: String, previous: Seq[Dataset] = Seq.empty[Dataset]) = { 33 | val harvestId = UUID.randomUUID() 34 | 35 | val props = Props(classOf[DataHarvestActor], harvestId, voidURL, previous) 36 | val actor = Akka.system.actorOf(props, harvestId.toString) 37 | actors.add(actor) 38 | 39 | val startTime = System.currentTimeMillis 40 | ask(actor, Start)(Timeout(6 hours)) onSuccess { 41 | case s: Stopped => { 42 | Logger.info("Harvest " + harvestId + " complete - took " + (System.currentTimeMillis - startTime) + " ms") 43 | actors.remove(actor) 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/ingest/harvest/Messages.scala: -------------------------------------------------------------------------------- 1 | package ingest.harvest 2 | 3 | case class Start() 4 | 5 | case class Stopped(success: Boolean, message: Option[String] = None) -------------------------------------------------------------------------------- /app/models/AccessLog.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import java.sql.Timestamp 4 | import play.api.Play.current 5 | import play.api.db.slick.Config.driver.simple._ 6 | import scala.slick.lifted.{ Tag => SlickTag } 7 | import java.util.UUID 8 | 9 | /** LogRecord model entity **/ 10 | case class AccessLogRecord(uuid: UUID, timestamp: Timestamp, path: String, ip: String, userAgent: String, referrer: Option[String], accept: Option[String], responseTime: Int) 11 | 12 | /** AccessLog DB table **/ 13 | class AccessLog(slickTag: SlickTag) extends Table[AccessLogRecord](slickTag, "access_log") { 14 | 15 | def uuid = column[UUID]("uuid", O.PrimaryKey) 16 | 17 | def timestamp = column[Timestamp]("timestamp", O.NotNull) 18 | 19 | def path = column[String]("path", O.NotNull, O.DBType("text")) 20 | 21 | def ip = column[String]("ip", O.NotNull) 22 | 23 | def userAgent = column[String]("user_agent", O.NotNull) 24 | 25 | def referrer = column[String]("referrer", O.Nullable, O.DBType("text")) 26 | 27 | def accept = column[String]("accept", O.Nullable) 28 | 29 | def responseTime = column[Int]("response_time", O.NotNull) 30 | 31 | def * = (uuid, timestamp, path, ip, userAgent, referrer.?, accept.?, responseTime) <> (AccessLogRecord.tupled, AccessLogRecord.unapply) 32 | 33 | } 34 | 35 | /** Queries **/ 36 | object AccessLog { 37 | 38 | private[models] val query = TableQuery[AccessLog] 39 | 40 | def create()(implicit s: Session) = query.ddl.create 41 | 42 | def insert(logRecord: AccessLogRecord)(implicit s: Session) = query.insert(logRecord) 43 | 44 | def listAll()(implicit s: Session): Seq[AccessLogRecord] = 45 | query.sortBy(_.timestamp.desc).list 46 | 47 | def findAllBefore(timestamp: Long)(implicit s: Session): Seq[AccessLogRecord] = 48 | query.where(_.timestamp < new Timestamp(timestamp)).list 49 | 50 | def deleteAllBefore(timestamp: Long)(implicit s: Session) = 51 | query.where(_.timestamp < new Timestamp(timestamp)).delete 52 | 53 | } 54 | 55 | class AccessLogAnalytics(log: Seq[AccessLogRecord]) { 56 | 57 | /** Number of hits to page URLs **/ 58 | lazy val pageHits: Int = log.count(record => record.path.contains("/pages")) 59 | 60 | /** Number of hits to API URLs **/ 61 | lazy val apiHits: Int = log.size - pageHits 62 | 63 | private lazy val searchRecords = log.filter(_.path.contains("/search")) 64 | 65 | private def getSearchTerm(path: String): Option[String] = { 66 | val startIdx = path.indexOf("query=") 67 | if (startIdx > -1) { 68 | val endIdx = path.indexOf('&', startIdx + 6) 69 | if (endIdx > -1) 70 | Some(path.substring(startIdx + 6, endIdx)) 71 | else 72 | Some(path.substring(startIdx + 6)) 73 | } else { 74 | None 75 | } 76 | } 77 | 78 | /** Most frequent search terms on the API **/ 79 | lazy val searches: Seq[(String, Int)] = 80 | searchRecords 81 | .flatMap(record => getSearchTerm(record.path)) 82 | .groupBy(term => term) 83 | .map(t => (t._1, t._2.size)) 84 | .toSeq 85 | .sortBy(- _._2) 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /app/models/HarvestLog.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import java.util.UUID 4 | import java.sql.Timestamp 5 | import play.api.db.slick.Config.driver.simple._ 6 | import scala.slick.lifted.{ Tag => SlickTag } 7 | 8 | case class HarvestLogRecord( 9 | 10 | /** A globally unique ID for the harvest **/ 11 | uuid: UUID, 12 | 13 | /** Start time of the harvest **/ 14 | startTime: Timestamp, 15 | 16 | /** VoID URL **/ 17 | voidURL: String, 18 | 19 | /** MD5 hash of the VoID file **/ 20 | voidHash: Option[String], 21 | 22 | /** MD5 hash of the annotation file(s) **/ 23 | dataHash: Option[String], 24 | 25 | /** Time the harvest took **/ 26 | harvestDuration: Int, 27 | 28 | /** Timne it tool to ingest the new data **/ 29 | ingestDuration: Option[Int], 30 | 31 | /** Empty if everything went as planned, else contains an error message **/ 32 | error: Option[String]) 33 | 34 | /** AccessLog DB table **/ 35 | class HarvestLog(slickTag: SlickTag) extends Table[HarvestLogRecord](slickTag, "harvest_log") { 36 | 37 | def uuid = column[UUID]("uuid", O.PrimaryKey, O.AutoInc) 38 | 39 | def startTime = column[Timestamp]("start_time", O.NotNull) 40 | 41 | def voidURL = column[String]("void_url", O.NotNull) 42 | 43 | def voidHash = column[String]("void_hash", O.Nullable) 44 | 45 | def dataHash = column[String]("data_hash", O.Nullable) 46 | 47 | def harvestDuration = column[Int]("harvest_duration", O.NotNull) 48 | 49 | def ingestDuration = column[Int]("ingest_duration", O.Nullable) 50 | 51 | def error = column[String]("error", O.Nullable) 52 | 53 | def * = (uuid, startTime, voidURL, voidHash.?, dataHash.?, harvestDuration, ingestDuration.?, error.?) <> (HarvestLogRecord.tupled, HarvestLogRecord.unapply) 54 | 55 | } 56 | 57 | /** Queries **/ 58 | object HarvestLog { 59 | 60 | private[models] val query = TableQuery[HarvestLog] 61 | 62 | def create()(implicit s: Session) = query.ddl.create 63 | 64 | def insert(logRecord: HarvestLogRecord)(implicit s: Session) = query.insert(logRecord) 65 | 66 | } -------------------------------------------------------------------------------- /app/models/ImportStatus.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | 5 | object ImportStatus extends Enumeration { 6 | 7 | val PENDING = Value("PENDING") 8 | 9 | val DOWNLOADING = Value("DOWNLOADING") 10 | 11 | val IMPORTING = Value("IMPORTING") 12 | 13 | val COMPLETE = Value("IMPORT_COMPLETE") 14 | 15 | val FAILED = Value("IMPORT_FAILED") 16 | 17 | implicit val statusMapper = MappedColumnType.base[ImportStatus.Value, String]( 18 | { status => status.toString }, 19 | { status => ImportStatus.withName(status) }) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/models/Page.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /** A simple helper for wrapping paginated DB query results **/ 4 | case class Page[A](items: Seq[A], offset: Int, limit: Int, total: Long, query: Option[String] = None) { 5 | 6 | /** Helper to perform a map to the items in the page **/ 7 | def map[B](f: (A) => B): Page[B] = 8 | Page(items.map(f), offset, limit, total, query) 9 | 10 | } 11 | 12 | object Page { 13 | 14 | /** Helper to create an empty page **/ 15 | def empty[A] = Page(Seq.empty[A], 0, Int.MaxValue, 0) 16 | 17 | } -------------------------------------------------------------------------------- /app/models/Taxonomy.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /** A simple category taxonomy for the API **/ 4 | object Taxonomy { 5 | 6 | private val taxonomy = 7 | Map("Texts" 8 | -> Seq("Inscriptions", 9 | "Literary Texts"), 10 | 11 | "Archaeology" 12 | -> Seq("Artefacts", 13 | "Sites"), 14 | 15 | "Numismatics" -> Seq.empty[String], // no sub-categories 16 | 17 | "Maps" -> Seq.empty[String], // no sub-categories 18 | 19 | "Images" 20 | -> Seq("Drawings", 21 | "Photos"), 22 | 23 | "Scholarship" -> Seq.empty[String] // no sub-categories 24 | ) 25 | 26 | /** The top level terms, in alphabetical order **/ 27 | lazy val topLevelTerms = taxonomy.keys.toSeq.sorted 28 | 29 | /** Gets the sub-categories for the term, if any (and if the term exists) **/ 30 | def getSubCategories(term: String): Option[Seq[String]] = 31 | taxonomy.get(term) 32 | 33 | def getPaths(tags: Seq[String]): Seq[Seq[String]] = { 34 | def normalize(term: String) = { if (term.endsWith("s")) term.substring(0, term.size - 1) else term }.toLowerCase 35 | 36 | val normalizedTags = tags.map(normalize(_)).toSet 37 | 38 | val matchedSingleLevelCategories = taxonomy 39 | .filter(_._2.isEmpty) 40 | .map(_._1) 41 | .filter(category => normalizedTags.contains(normalize(category))) 42 | .toSeq 43 | .map(Seq(_)) 44 | 45 | val matchedSecondLevelCategories = taxonomy 46 | .mapValues(_.filter(subcategory => normalizedTags.contains(normalize(subcategory)))) 47 | .filter(_._2.size > 0) 48 | .toSeq 49 | .flatMap { case (topCategory, subcategories) => subcategories.map(Seq(topCategory, _))} 50 | 51 | matchedSecondLevelCategories ++ matchedSingleLevelCategories 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/models/adjacency/PlaceAdjacency.scala: -------------------------------------------------------------------------------- 1 | package models.adjacency 2 | 3 | import models.geo.GazetteerReference 4 | import play.api.db.slick.Config.driver.simple._ 5 | import scala.slick.lifted.{ Tag => SlickTag } 6 | import models.geo.GazetteerReference 7 | import models.geo.GazetteerReference 8 | import models.core.AnnotatedThings 9 | import play.api.Logger 10 | 11 | case class PlaceAdjacency( 12 | 13 | /** Auto-inc ID **/ 14 | id: Option[Int], 15 | 16 | /** Annotated thing ID **/ 17 | annotatedThing: String, 18 | 19 | /** A place on the annotated thing **/ 20 | place: GazetteerReference, 21 | 22 | /** The adjacent place **/ 23 | nextPlace: GazetteerReference, 24 | 25 | /** The weight, i.e. no. of annotation adjacencies for this place adjacency **/ 26 | weight: Int) 27 | 28 | class PlaceAdjacencys(tag: SlickTag) extends Table[PlaceAdjacency](tag, "adjacency_places") { 29 | 30 | def id = column[Int]("id", O.AutoInc, O.PrimaryKey) 31 | 32 | def annotatedThingId = column[String]("annotated_thing", O.NotNull) 33 | 34 | def placeURI = column[String]("place_uri", O.NotNull) 35 | 36 | def placeTitle = column[String]("place_title", O.NotNull) 37 | 38 | def placeLocation = column[String]("place_location", O.Nullable, O.DBType("text")) 39 | 40 | def nextPlaceURI = column[String]("next_place_uri", O.NotNull) 41 | 42 | def nextPlaceTitle = column[String]("next_place_title", O.NotNull) 43 | 44 | def nextPlaceLocation = column[String]("next_place_location", O.Nullable, O.DBType("text")) 45 | 46 | def weight = column[Int]("weight", O.NotNull) 47 | 48 | def * = (id.?, annotatedThingId, (placeURI, placeTitle, placeLocation.?), (nextPlaceURI, nextPlaceTitle, nextPlaceLocation.?), weight).shaped <> ( 49 | { case (id, annotatedThing, place, nextPlace, weight) => PlaceAdjacency(id, annotatedThing, GazetteerReference.tupled.apply(place), GazetteerReference.tupled.apply(nextPlace), weight) }, 50 | { p: PlaceAdjacency => Some(p.id, p.annotatedThing, GazetteerReference.unapply(p.place).get, GazetteerReference.unapply(p.nextPlace).get, p.weight) }) 51 | 52 | /** Indices **/ 53 | 54 | def annotatedThingIdx = index("idx_annotated_thing", annotatedThingId, unique = false) 55 | 56 | } 57 | 58 | /** Queries **/ 59 | object PlaceAdjacencys { 60 | 61 | private[models] val query = TableQuery[PlaceAdjacencys] 62 | 63 | /** Creates the DB table **/ 64 | def create()(implicit s: Session) = query.ddl.create 65 | 66 | /** Inserts a list of adjacency pairs into the DB **/ 67 | def insertAll(adjacencies: Seq[PlaceAdjacency])(implicit s: Session) = 68 | query.insertAll(adjacencies:_*) 69 | 70 | /** Retrieves adjacency pairs for an annotated thing **/ 71 | def findByAnnotatedThing(id: String)(implicit s: Session): PlaceAdjacencyGraph = 72 | new PlaceAdjacencyGraph(query.where(_.annotatedThingId === id).list) 73 | 74 | def findByAnnotatedThingRecursive(id: String)(implicit s: Session): PlaceAdjacencyGraph = { 75 | val allIds = id +: AnnotatedThings.listChildrenRecursive(id) 76 | 77 | val result = query.where(_.annotatedThingId inSet allIds) 78 | .groupBy(t => (t.placeURI, t.placeTitle, t.placeLocation, t.nextPlaceURI, t.nextPlaceTitle, t.nextPlaceLocation)) 79 | .map(t => (t._1._1, t._1._2, t._1._3.?, t._1._4, t._1._5, t._1._6.?, t._2.map(_.weight).sum)) 80 | .list 81 | .map { case (placeURI, placeTitle, placeGeom, nextPlaceURI, nextPlaceTitle, nextPlaceGeom, weight) => 82 | PlaceAdjacency(None, id, GazetteerReference(placeURI, placeTitle, placeGeom), GazetteerReference(nextPlaceURI, nextPlaceTitle, nextPlaceGeom), weight.get) } 83 | 84 | new PlaceAdjacencyGraph(result) 85 | } 86 | 87 | /** Deletes adjacency pairs for a list of annotated things **/ 88 | def deleteForAnnotatedThings(ids: Seq[String])(implicit s: Session) = 89 | query.where(_.annotatedThingId inSet ids).delete 90 | 91 | } -------------------------------------------------------------------------------- /app/models/adjacency/PlaceAdjacencyGraph.scala: -------------------------------------------------------------------------------- 1 | package models.adjacency 2 | 3 | import models.geo.GazetteerReference 4 | import play.api.Logger 5 | 6 | class PlaceAdjacencyGraph(adjacencies: Seq[PlaceAdjacency]) { 7 | 8 | case class Edge(from: Int, to: Int, weight: Int) 9 | 10 | val nodes: Seq[GazetteerReference] = adjacencies.flatMap(a => Seq(a.place, a.nextPlace)).distinct 11 | 12 | val edges: Seq[Edge] = adjacencies.map(pair => 13 | Edge(nodes.indexOf(pair.place), nodes.indexOf(pair.nextPlace), pair.weight)) 14 | 15 | Logger.info(nodes.toString) 16 | 17 | } -------------------------------------------------------------------------------- /app/models/core/Image.scala: -------------------------------------------------------------------------------- 1 | package models.core 2 | 3 | import java.sql.Timestamp 4 | import play.api.Play.current 5 | import play.api.db.slick.Config.driver.simple._ 6 | import scala.slick.lifted.{ Tag => SlickTag } 7 | 8 | case class Image( 9 | 10 | /** ID **/ 11 | id: Option[Int], 12 | 13 | /** ID of the dataset **/ 14 | dataset: String, 15 | 16 | /** ID of the annotated thing **/ 17 | annotatedThing: String, 18 | 19 | /** Image URL **/ 20 | url: String, 21 | 22 | /** An image caption **/ 23 | title: Option[String] = None, 24 | 25 | /** A (longer) description **/ 26 | description: Option[String] = None, 27 | 28 | /** URL to a thumbnail image **/ 29 | thumbnail: Option[String] = None, 30 | 31 | /** Image creator **/ 32 | creator: Option[String] = None, 33 | 34 | /** Time of creation info **/ 35 | created: Option[Timestamp] = None, 36 | 37 | /** Image license **/ 38 | license: Option[String] = None) 39 | 40 | 41 | class Images(tag: SlickTag) extends Table[Image](tag, "images") { 42 | 43 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc) 44 | 45 | def datasetId = column[String]("dataset", O.NotNull) 46 | 47 | def annotatedThingId = column[String]("annotated_thing", O.NotNull) 48 | 49 | def url = column[String]("url", O.NotNull) 50 | 51 | def title = column[String]("title", O.Nullable) 52 | 53 | def description = column[String]("description", O.Nullable, O.DBType("text")) 54 | 55 | def thumbnail = column[String]("thumbnail", O.Nullable) 56 | 57 | def creator = column[String]("creator", O.Nullable) 58 | 59 | def created = column[Timestamp]("created", O.Nullable) 60 | 61 | def license = column[String]("license", O.Nullable) 62 | 63 | def * = (id.?, datasetId, annotatedThingId, url, title.?, description.?, 64 | thumbnail.?, creator.?, created.?, license.?) <> (Image.tupled, Image.unapply) 65 | 66 | /** Foreign key constraints **/ 67 | 68 | def datasetFk = foreignKey("dataset_fk", datasetId, Datasets.query)(_.id) 69 | 70 | def annotatedThingFk = foreignKey("annotated_thing_fk", annotatedThingId, AnnotatedThings.query)(_.id) 71 | 72 | /** Indices **/ 73 | 74 | def annotatedThingIdx = index("idx_images_by_thing", annotatedThingId, unique = false) 75 | 76 | } 77 | 78 | /** Queries **/ 79 | object Images { 80 | 81 | private[models] val query = TableQuery[Images] 82 | 83 | /** Creates the DB table **/ 84 | def create()(implicit s: Session) = query.ddl.create 85 | 86 | /** Inserts a list of Images into the DB **/ 87 | def insertAll(images: Seq[Image])(implicit s: Session) = 88 | query.insertAll(images:_*) 89 | 90 | def deleteForDatasets(ids: Seq[String])(implicit s: Session) = 91 | query.where(_.datasetId inSet ids).delete 92 | 93 | def findByAnnotatedThing(id: String)(implicit s: Session): Seq[Image] = 94 | query.where(_.annotatedThingId === id).list 95 | 96 | } -------------------------------------------------------------------------------- /app/models/core/Tag.scala: -------------------------------------------------------------------------------- 1 | package models.core 2 | 3 | import play.api.Play.current 4 | import play.api.db.slick.Config.driver.simple._ 5 | import scala.slick.lifted.{ Tag => SlickTag } 6 | 7 | /** Tag model entity **/ 8 | case class Tag(id: Option[Int], dataset: String, annotatedThing: String, label: String) 9 | 10 | /** Tag DB table **/ 11 | class Tags(slickTag: SlickTag) extends Table[Tag](slickTag, "tags") { 12 | 13 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc) 14 | 15 | def datasetId = column[String]("dataset", O.NotNull) 16 | 17 | def annotatedThingId = column[String]("annotated_thing", O.NotNull) 18 | 19 | def label = column[String]("label", O.NotNull) 20 | 21 | def * = (id.?, datasetId, annotatedThingId, label) <> (Tag.tupled, Tag.unapply) 22 | 23 | /** Foreign key constraints **/ 24 | 25 | def datasetFk = foreignKey("dataset_fk", datasetId, Datasets.query)(_.id) 26 | 27 | def annotatedThingFk = foreignKey("annotated_thing_fk", annotatedThingId, AnnotatedThings.query)(_.id) 28 | 29 | /** Indices **/ 30 | 31 | def annotatedThingIdx = index("idx_tags_by_thing", annotatedThingId, unique = false) 32 | 33 | } 34 | 35 | /** Queries **/ 36 | object Tags { 37 | 38 | private[models] val query = TableQuery[Tags] 39 | 40 | /** Creates the DB table **/ 41 | def create()(implicit s: Session) = query.ddl.create 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/models/core/TemporalProfile.scala: -------------------------------------------------------------------------------- 1 | package models.core 2 | 3 | import controllers.common.JSONWrites._ 4 | import play.api.libs.json.Json 5 | 6 | class TemporalProfile(data: Seq[(Int, Int)]) { 7 | 8 | val histogram = data.foldLeft(scala.collection.mutable.Map.empty[Int, Int]) { case (h, (nextStart, nextEnd)) => 9 | Seq.range(nextStart, nextEnd + 1).foreach(year => h.put(year, h.get(year).getOrElse(0) + 1)) 10 | h 11 | }.toMap 12 | 13 | val maxValue = histogram.map(_._2).max 14 | 15 | val boundsStart = histogram.map(_._1).min 16 | 17 | val boundsEnd = histogram.map(_._1).max 18 | 19 | lazy val asJSON = Json.toJson(this) 20 | 21 | override lazy val toString = Json.stringify(asJSON) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/models/geo/BoundingBox.scala: -------------------------------------------------------------------------------- 1 | package models.geo 2 | 3 | import index.places.IndexedPlaceNetwork 4 | import com.vividsolutions.jts.geom.{ Envelope, Geometry } 5 | import play.api.db.slick.Config.driver.simple._ 6 | import play.api.Logger 7 | 8 | case class BoundingBox(minLon: Double, maxLon: Double, minLat: Double, maxLat: Double) { 9 | 10 | if (invalidBounds) 11 | throw new IllegalArgumentException("Illegal bounds: " + minLon + ", " + maxLon + ", " + minLat + ", " + maxLat) 12 | 13 | override lazy val toString = Seq(minLon, maxLon, minLat, maxLat).mkString(",") 14 | 15 | private def invalidBounds: Boolean = { 16 | if (minLon < -180) 17 | true 18 | else if (maxLon > 180) 19 | true 20 | else if (minLat < -90) 21 | true 22 | else if (maxLat > 90) 23 | true 24 | else 25 | false 26 | } 27 | 28 | } 29 | 30 | object BoundingBox { 31 | 32 | /** DB mapper function **/ 33 | implicit val statusMapper = MappedColumnType.base[BoundingBox, String]( 34 | { bbox => bbox.toString }, 35 | { bbox => BoundingBox.fromString(bbox).get }) 36 | 37 | def fromGeometry(geometry: Geometry): BoundingBox = { 38 | val envelope = geometry.getEnvelopeInternal 39 | BoundingBox(envelope.getMinX, envelope.getMaxX, envelope.getMinY, envelope.getMaxY) 40 | } 41 | 42 | /** Computes a bounding box from a list of geometries **/ 43 | def fromGeometries(geometries: Seq[Geometry]): Option[BoundingBox] = { 44 | if (geometries.size > 0) { 45 | try { 46 | val envelope = new Envelope() 47 | geometries.foreach(geom => envelope.expandToInclude(geom.getEnvelopeInternal)) 48 | Some(BoundingBox(envelope.getMinX, envelope.getMaxX, envelope.getMinY, envelope.getMaxY)) 49 | } catch { 50 | case e:IllegalArgumentException => { 51 | Logger.warn(e.getMessage) 52 | None 53 | } 54 | } 55 | } else { 56 | None 57 | } 58 | } 59 | 60 | /** Helper function to get the bounds of a list of places **/ 61 | def fromPlaces(places: Seq[IndexedPlaceNetwork]): Option[BoundingBox] = 62 | fromGeometries(places.flatMap(_.geometry)) 63 | 64 | /** Helper function to parse a comma-separated string representation **/ 65 | def fromString(s: String): Option[BoundingBox] = { 66 | val coords = s.split(",").map(_.trim) 67 | if (coords.size == 4) { 68 | try { 69 | Some(BoundingBox(coords(0).toDouble, coords(1).toDouble, coords(2).toDouble, coords(3).toDouble)) 70 | } catch { 71 | case _:Throwable => None 72 | } 73 | } else { 74 | None 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/models/geo/GazetteerReference.scala: -------------------------------------------------------------------------------- 1 | package models.geo 2 | 3 | import com.vividsolutions.jts.geom.{ Coordinate, Geometry } 4 | import index.places.IndexedPlace 5 | import org.geotools.geojson.geom.GeometryJSON 6 | import play.api.db.slick.Config.driver.simple._ 7 | 8 | /** GazetteerReference model class. 9 | * 10 | * Note: a gazetteer reference caches some information that normally resides in the 11 | * gazetteer index. This way, we don't always have to introduce an extra index resolution 12 | * step when retrieving place URIs from the database. 13 | */ 14 | case class GazetteerReference(uri: String, title: String, geometryJson: Option[String]) { 15 | 16 | lazy val geometry: Option[Geometry] = geometryJson.map(geoJson => new GeometryJSON().read(geoJson)) 17 | 18 | lazy val centroid: Option[Coordinate] = geometry.map(_.getCentroid.getCoordinate) 19 | 20 | } -------------------------------------------------------------------------------- /app/models/geo/Hull.scala: -------------------------------------------------------------------------------- 1 | package models.geo 2 | 3 | import com.vividsolutions.jts.geom.{ Geometry, GeometryCollection, GeometryFactory } 4 | import com.vividsolutions.jts.algorithm.{ ConvexHull => JTSConvexHull } 5 | import index.places.IndexedPlaceNetwork 6 | import java.io.StringWriter 7 | import org.geotools.geojson.geom.GeometryJSON 8 | import org.geotools.geometry.jts.{ JTS, JTSFactoryFinder } 9 | import play.api.Logger 10 | import play.api.libs.json.Json 11 | import play.api.db.slick.Config.driver.simple._ 12 | import scala.collection.JavaConverters._ 13 | import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier 14 | 15 | case class Hull(geometry: Geometry) { 16 | 17 | val bounds: BoundingBox = { 18 | val envelope = geometry.getEnvelopeInternal() 19 | BoundingBox(envelope.getMinX, envelope.getMaxX, envelope.getMinY, envelope.getMaxY) 20 | } 21 | 22 | lazy val asGeoJSON = 23 | Json.parse(toString) 24 | 25 | override lazy val toString = { 26 | val writer = new StringWriter() 27 | new GeometryJSON().write(geometry, writer) 28 | writer.toString 29 | } 30 | 31 | } 32 | 33 | object Hull { 34 | 35 | /** DB mapper function **/ 36 | implicit val hullMapper = MappedColumnType.base[Hull, String]( 37 | { hull => hull.toString }, 38 | { hull => Hull.fromGeoJSON(hull) }) 39 | 40 | def fromGeoJSON(json: String): Hull = 41 | Hull(new GeometryJSON().read(json.trim)) 42 | 43 | /** Shortcut to the preferred hull type **/ 44 | def compute(geometries: Seq[Geometry]): Option[Hull] = 45 | try { 46 | // Implementation not 100% stable - fall back to convex hull in case of problems 47 | ConcaveHull.compute(geometries) 48 | } catch { 49 | case t: Throwable => { 50 | Logger.info("Falling back to convex hull for geometry " + geometries.toString) 51 | ConvexHull.compute(geometries) 52 | } 53 | } 54 | 55 | def fromPlaces(places: Seq[IndexedPlaceNetwork]): Option[Hull] = 56 | compute(places.flatMap(_.geometry)) 57 | 58 | } 59 | 60 | private object ConvexHull { 61 | 62 | def compute(geometries: Seq[Geometry]): Option[Hull] = { 63 | // Make sure convex hull does not throw exceptions - we're using it as fallback in case ConcaveHull breaks 64 | try { 65 | if (geometries.size > 0) { 66 | val factory = JTSFactoryFinder.getGeometryFactory() 67 | val mergedGeometry = factory.buildGeometry(geometries.asJava).union 68 | val cvGeometry = new JTSConvexHull(mergedGeometry).getConvexHull() 69 | Some(Hull(cvGeometry)) 70 | } else { 71 | None 72 | } 73 | } catch { 74 | case t: Throwable => { 75 | Logger.warn(t.getMessage) 76 | None 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | private object ConcaveHull { 84 | 85 | private val THRESHOLD = 2.0 86 | 87 | private val POLYGON_SIMPLIFICATION_TOLERANCE = 1.0 88 | 89 | private val SMOOTHING = 0.8 90 | 91 | private val factory = new GeometryFactory 92 | 93 | def compute(geometries: Seq[Geometry]): Option[Hull] = { 94 | if (geometries.size > 0) { 95 | val union = new GeometryCollection(geometries.toArray, factory).buffer(0) 96 | val smoothed = JTS.smooth(union, SMOOTHING) 97 | val simplified = TopologyPreservingSimplifier.simplify(smoothed, POLYGON_SIMPLIFICATION_TOLERANCE) 98 | Some(Hull(simplified)) 99 | } else { 100 | None 101 | } 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /app/views/admin/accessLog.scala.html: -------------------------------------------------------------------------------- 1 | @(analytics: AccessLogAnalytics, mostRecent: Seq[AccessLogRecord]) 2 | 3 | 4 | Pelagios API » Admin » Access Analytics 5 | 6 | 7 | 19 | 20 | 21 | 22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    Number of API hits:@analytics.apiHits
    Number of page hits:@analytics.pageHits
    Most frequent searches:@analytics.searches.map(search => search._1 + " (" + search._2 + ")").mkString(", ")
    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | @for(r <- mostRecent) { 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | } 60 | 61 |
    IPmsPathReferrerUser Agent
    @r.ip@r.responseTime@r.path@r.referrer@r.userAgent
    62 |
    63 | 64 | 65 | -------------------------------------------------------------------------------- /app/views/admin/login.scala.html: -------------------------------------------------------------------------------- 1 | @(form: Form[(String,String)])(implicit flash: Flash) 2 | 3 |

    Sign in

    4 | @helper.form(controllers.admin.routes.AuthController.authenticate) { 5 | @form.globalError.map { error =>
    @error.message
    } 6 | @flash.get("success").map { message =>
    @message
    } 7 | 8 | 9 |
    10 | 11 |
    12 | 13 | 14 |
    15 | 16 |
    17 | 18 |

    19 | } 20 | -------------------------------------------------------------------------------- /app/views/annotatedThingDetails.scala.html: -------------------------------------------------------------------------------- 1 | @(thing: models.core.AnnotatedThing, thumbnails: Seq[models.core.Image], datasetHierarchy: Seq[models.core.Dataset]) 2 | 3 | 4 | Item » @thing.title 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 50 | 51 |
    52 |
    53 | @for(thumbnail <- thumbnails) { 54 | 55 | @thing.homepage.map { homepage =>

    Visit Source

    } 56 | } 57 |
    58 |
    59 | 60 | 61 | -------------------------------------------------------------------------------- /app/views/datasetList.scala.html: -------------------------------------------------------------------------------- 1 | @(datasets: Int, things: Int, annotations: Int) 2 | 3 | 4 | Pelagios API - Datasets 5 | 6 | 7 | 8 | 9 | 10 | 11 | 34 | 35 |
    36 |
    37 | 38 | 39 | 40 |
    41 |
    42 |
    43 | 44 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/views/home.scala.html: -------------------------------------------------------------------------------- 1 | @(datasets: Int, items: Int, gazetteers:Int, places: Int) 2 | 3 | 4 | Pelagios - Home 5 | 6 | 7 | 8 | 9 | @helper.requireJs(core = routes.Assets.at("javascripts/require.js").url, module = routes.Assets.at("javascripts/search").url) 10 | 11 | 12 | 40 | 41 |
    42 |
    43 |

    Explore

    44 |

    45 | Pelagios (Greek for 'of the Sea') is a community network that facilitates linking 46 | of online resources that document the past, based on the places they refer to. 47 | This Website is a demo search and exploration tool, running on 48 | a limited test set of classical literature, historic 49 | maps, and information from databases of historic places and artefacts from 50 | our partner network. Start by searching for a place, object 51 | or topic you are interested in. Narrow down your search, or combine 52 | multiple searches to explore similarities, differences and connections. 53 |

    54 | 55 |

    Join

    56 |

    57 | Find out more about us on our blog, 58 | or learn how to get your own data connected to the Pelagios network in our 59 | Cookbook. 60 |

    61 |
    62 |
    63 | 64 | 65 | -------------------------------------------------------------------------------- /app/views/landingPage.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | 3 | 4 | Welcome to Peripleo 5 | 6 | 7 | 8 | 9 | 28 |

    You are being redirected to the new version of 29 | Peripleo.

    30 | 31 | 32 | -------------------------------------------------------------------------------- /app/views/map.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | 3 | 4 | Peripleo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | @* helper.requireJs(core = routes.Assets.at("javascripts/require.js").url, module = routes.Assets.at("javascripts/peripleo-ui").url) *@ 20 | 21 | 22 | 27 |

    You are being redirected to the new version of 28 | Peripleo.

    29 | 30 | 31 | -------------------------------------------------------------------------------- /app/views/placeAdjacencyHack.scala.html: -------------------------------------------------------------------------------- 1 | @(annotatedThingId: String) 2 | 3 | 4 | Place Adjacency 5 | 6 | 7 | 8 | @helper.requireJs(core = routes.Assets.at("javascripts/require.js").url, module = routes.Assets.at("javascripts/placeAdjacencyNetwork").url) 9 | 26 | 27 | 28 |
    29 | 30 | 31 | -------------------------------------------------------------------------------- /app/views/showGazetteer.scala.html: -------------------------------------------------------------------------------- 1 | @(gazetteerName: String) 2 | 3 | 4 | Explore 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/views/tags/gazetteerURI.scala.html: -------------------------------------------------------------------------------- 1 | @(uri: String, inline: Boolean = false) 2 | 3 | @{ 4 | val cssClass = if (inline) "gazetteer-uri inline" else "gazetteer-uri" 5 | 6 | uri match { 7 | case s if s.startsWith("http://pleiades.stoa.org/places/") => 8 | pleiades:{ s.substring(32) } 9 | 10 | case s if s.startsWith("http://dare.ht.lu.se/places/") => 11 | dare:{ s.substring(28) } 12 | 13 | case s if s.startsWith("http://gazetteer.dainst.org/place/") => 14 | dai:{ s.substring(34) } 15 | 16 | case s if s.startsWith("http://vici.org/vici/") => 17 | vici:{ s.substring(21) } 18 | 19 | case s if s.startsWith("http://data.pastplace.org/") => 20 | pastplace:{ s.substring(35) } 21 | 22 | case s if s.startsWith("http://www.maphistory.info") => 23 | maphistory:{ s.substring(44) } 24 | 25 | case s if s.startsWith("http://www.trismegistos.org/place/") => 26 | trismegistos:{ s.substring(34) } 27 | 28 | case s => 29 | { s } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/views/tags/timespan.scala.html: -------------------------------------------------------------------------------- 1 | @(start: Option[Int], end: Option[Int]) 2 | 3 | @{ 4 | 5 | def prefix(date: Int) = if (date < 0) "BC" else "AD" 6 | 7 | if (start.isDefined && end.isDefined) { 8 | val startPrefix = prefix(start.get) 9 | val endPrefix = prefix(end.get) 10 | 11 | if (start == end) { 12 | startPrefix + " " + Math.abs(start.get) 13 | } else { 14 | if (startPrefix == endPrefix) { 15 | Html(startPrefix + " " + Math.abs(start.get) + " – " + Math.abs(end.get)) 16 | } else { 17 | Html(startPrefix + " " + Math.abs(start.get) + " – " + endPrefix + " " + Math.abs(end.get)) 18 | } 19 | } 20 | } else { 21 | "" 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "peripleo" 2 | 3 | version := "0.0.1" 4 | 5 | play.Project.playScalaSettings 6 | 7 | resolvers += "Open Source Geospatial Foundation Repository" at "http://download.osgeo.org/webdav/geotools/" 8 | 9 | libraryDependencies ++= Seq(jdbc, cache) 10 | 11 | libraryDependencies ++= Seq( 12 | "com.typesafe.play" %% "play-slick" % "0.6.0.1", 13 | "org.apache.lucene" % "lucene-analyzers-common" % "5.3.1", 14 | "org.apache.lucene" % "lucene-queryparser" % "5.3.1", 15 | "org.apache.lucene" % "lucene-facet" % "5.3.1", 16 | "org.apache.lucene" % "lucene-spatial" % "5.3.1", 17 | "org.apache.lucene" % "lucene-suggest" % "5.3.1", 18 | "org.apache.lucene" % "lucene-highlighter" % "5.3.1", 19 | "com.vividsolutions" % "jts" % "1.13", 20 | "com.spatial4j" % "spatial4j" % "0.5", 21 | "org.geotools" % "gt-geojson" % "14.0", 22 | "org.xerial" % "sqlite-jdbc" % "3.7.2", 23 | "postgresql" % "postgresql" % "9.1-901.jdbc4" 24 | ) 25 | 26 | /** Transient dependencies required by Scalagios 27 | * 28 | * TODO: remove once Scalagios is included as managed dependency! 29 | */ 30 | libraryDependencies ++= Seq( 31 | "org.openrdf.sesame" % "sesame-rio-n3" % "2.7.5", 32 | "org.openrdf.sesame" % "sesame-rio-rdfxml" % "2.7.5", 33 | "org.slf4j" % "slf4j-simple" % "1.7.7" 34 | ) 35 | 36 | requireJs ++= Seq("peripleo-ui.js", "placeAdjacencyNetwork.js") 37 | 38 | requireJsShim += "build.js" 39 | -------------------------------------------------------------------------------- /conf/.gitignore: -------------------------------------------------------------------------------- 1 | /application.conf 2 | -------------------------------------------------------------------------------- /conf/application.conf.template: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="qvrS[/b=c@t=<[;dR3_GPrwwn;9sDP=mXFf/b1@MV9D9raWRx;:h72pDQB];XlbH" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | application.langs="en" 13 | 14 | # Global object class 15 | # ~~~~~ 16 | # Define the Global object class for this application. 17 | # Default to Global in the root package. 18 | application.global=global.Global 19 | 20 | # Router 21 | # ~~~~~ 22 | # Define the Router object to use for this application. 23 | # This router will be looked up first when the application is starting up, 24 | # so make sure this is the entry point. 25 | # Furthermore, it's assumed your route file is named properly. 26 | # So for an application router like `my.application.Router`, 27 | # you may need to define a router file `conf/my.application.routes`. 28 | # Default to Routes in the root package (and conf/routes) 29 | # application.router=my.application.Routes 30 | 31 | # Database configuration 32 | # ~~~~~ 33 | # You can declare as many datasources as you want. 34 | # By convention, the default datasource is named `default` 35 | # 36 | db.default.driver="org.sqlite.JDBC" 37 | db.default.url="jdbc:sqlite:db/pelagios-api.db" 38 | 39 | # Postgres configuration example 40 | # db.default.driver="org.postgresql.Driver" 41 | # db.default.url="jdbc:postgresql://localhost/peripleo" 42 | # db.default.user="postgres" 43 | # db.default.password="postgres" 44 | 45 | # Admin username/password - IMPORTANT: change this when moving app into production! 46 | admin.user = admin 47 | admin.password = admin 48 | 49 | # Logger 50 | # ~~~~~ 51 | # You can also configure logback (http://logback.qos.ch/), 52 | # by providing an application-logger.xml file in the conf directory. 53 | 54 | # Root logger: 55 | logger.root=INFO 56 | 57 | # Logger used by the framework: 58 | logger.play=INFO 59 | 60 | # Logger provided to your application: 61 | logger.application=DEBUG 62 | 63 | # API specific settings 64 | # ~~~~~ 65 | 66 | # Set to true if the application should add the "Access-Control-Allow-Origin" -> "*" header 67 | # for CORS support. Note that this needs to be set to false, if CORS is enabled through 68 | # a proxy setting 69 | peripleo.enable.cors=false 70 | 71 | # Gazetteers 72 | peripleo.gazetteer.names="Pleiades, DARE" 73 | peripleo.gazetteer.files="pleiades-201207-migrated.ttl.gz, dare-2015-0422.ttl.gz" 74 | 75 | # A comma-separated list of gazetteer patch files 76 | # peripleo.gazetteer.patches="pelagios_geojson.ttl" 77 | 78 | # Date (day of month) and time for conducting monthly access log archivals 79 | peripleo.log.archival.day=1 80 | peripleo.log.archival.time="13:23" 81 | 82 | # Number of records to ingest in one batch - defaults to 30000 83 | # Set lower on lower-end machines 84 | # peripleo.ingest.batchsize=10000 85 | -------------------------------------------------------------------------------- /db/.gitignore: -------------------------------------------------------------------------------- 1 | /pelagios-api.db 2 | -------------------------------------------------------------------------------- /gazetteer/.gitignore: -------------------------------------------------------------------------------- 1 | /dai.ttl.gz 2 | /vici.rdf.gz 3 | /tir.ttl 4 | /CHGIS.ttl 5 | /CHGIS.ttl.gz 6 | /pleiades-regions-magis-pelagios.ttl 7 | /trismegistos.ttl.gz 8 | /Opcine1994* 9 | -------------------------------------------------------------------------------- /gazetteer/dare-2015-1014.ttl.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/gazetteer/dare-2015-1014.ttl.gz -------------------------------------------------------------------------------- /gazetteer/pleiades-201207-migrated.ttl.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/gazetteer/pleiades-201207-migrated.ttl.gz -------------------------------------------------------------------------------- /lib/concave_hull.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/lib/concave_hull.jar -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Warn 3 | 4 | // The Typesafe repository 5 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 6 | 7 | // Use the Play sbt plugin for Play projects 8 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.2.2") -------------------------------------------------------------------------------- /public/images/cc-by-nc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/cc-by-nc.png -------------------------------------------------------------------------------- /public/images/cc-zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/cc-zero.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/open-data-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/open-data-generic.png -------------------------------------------------------------------------------- /public/images/wait-anim-fb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/wait-anim-fb.gif -------------------------------------------------------------------------------- /public/images/wait-circle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/images/wait-circle.gif -------------------------------------------------------------------------------- /public/javascripts/lib/jquery.ui.touch-punch.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Touch Punch 0.2.3 3 | * 4 | * Copyright 2011–2014, Dave Furfero 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * Depends: 8 | * jquery.ui.widget.js 9 | * jquery.ui.mouse.js 10 | */ 11 | !function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/layers-2x.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/layers.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/marker-icon-2x.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/marker-icon-blue-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/marker-icon-blue-2x.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/marker-icon-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/marker-icon-blue.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/marker-icon.png -------------------------------------------------------------------------------- /public/javascripts/lib/leaflet/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/javascripts/lib/leaflet/images/marker-shadow.png -------------------------------------------------------------------------------- /public/stylesheets/Cinzel-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/Cinzel-Regular.ttf -------------------------------------------------------------------------------- /public/stylesheets/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/Roboto-Light.ttf -------------------------------------------------------------------------------- /public/stylesheets/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/stylesheets/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/stylesheets/fontello-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/fontello-icons.ttf -------------------------------------------------------------------------------- /public/stylesheets/peripleo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pelagios/peripleo/46db78009a38bf6ffd8dd0dd6d45b5a1cec1b96e/public/stylesheets/peripleo.ttf -------------------------------------------------------------------------------- /test-scenarios.md: -------------------------------------------------------------------------------- 1 | ## Tetradrachm (see [blog](http://pelagios-project.blogspot.co.uk/2015/07/peripleo-sneak-preview.html)) 2 | 3 | * Search for 'tetradrachm' 4 | ** Verify result coverage 5 | * Zoom in to Sicily 6 | ** Verify no. of results 7 | ** Verify Syracusae as top place 8 | ** Select Syracusae and verify result count 9 | ** 'Search at' for 'tetradrachm' - verify result list 10 | * Deselect Syracuse and zoom out 11 | * Verify time filter functionality 12 | 13 | ## Theatres (see [blog](http://pelagios-project.blogspot.co.uk/2015/07/peripleo-sneak-preview.html)) 14 | 15 | * Search for 'theatre' 16 | ** Verify result coverage 17 | * Zoom in to North Africa 18 | ** Verify thumbnails 19 | * Switch to satellite baselayer 20 | * Select Djemila thumbnail 21 | ** Verify zoom-in functionality 22 | * Click explore 23 | ** Verify Cuicul marker 24 | * Search at Cuicul 25 | * Select an inscription and go to EDH page 26 | * Deselect Cuicul 27 | * Deactive explore and zoom out 28 | ** Verify theatre result coverage 29 | 30 | ## Search at Rome 31 | 32 | * Search 'rome' 33 | * Search at rome, query 'epitaph' 34 | ** Verify result counts 35 | ** Verify functionality of 'Show all results' and 'Show results at Roma' buttons 36 | 37 | ## Linkability 38 | 39 | [...TODO...] 40 | -------------------------------------------------------------------------------- /test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | @RunWith(classOf[JUnitRunner]) 9 | class ApplicationSpec extends Specification { 10 | 11 | "Application" should { 12 | 13 | "send 404 on a bad request" in new WithApplication{ 14 | route(FakeRequest(GET, "/boum")) must beNone 15 | } 16 | 17 | "render the index page" in new WithApplication{ 18 | val home = route(FakeRequest(GET, "/")).get 19 | 20 | status(home) must equalTo(OK) 21 | contentType(home) must beSome.which(_ == "text/html") 22 | contentAsString(home) must contain ("Your new application is ready.") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * add your integration spec here. 10 | * An integration test will fire up a whole play application in a real (or headless) browser 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class IntegrationSpec extends Specification { 14 | 15 | "Application" should { 16 | 17 | "work from within a browser" in new WithBrowser { 18 | 19 | browser.goTo("http://localhost:" + port) 20 | 21 | browser.pageSource must contain("Your new application is ready.") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/index/places/IndexedPlaceNetworkSpec.scala: -------------------------------------------------------------------------------- 1 | package index.places 2 | 3 | import org.specs2.mutable._ 4 | import org.specs2.runner._ 5 | import org.junit.runner._ 6 | import play.api.test._ 7 | import play.api.test.Helpers._ 8 | 9 | @RunWith(classOf[JUnitRunner]) 10 | class IndexedPlaceNetworkSpec extends Specification { 11 | 12 | private def createTestPlace(uri: String, label: String, closeMatch: String) = { 13 | new IndexedPlace("{\"uri\":\"" + uri + "\", \"label\":\"" + label + "\", \"source_gazetteer\":\"none\", \"names\":[], \"close_matches\":[\"" + closeMatch + "\"], \"exact_matches\":[] }") 14 | } 15 | 16 | // The test network looks like this [DAI]-->(geonames)<--[DARE]<--[VICI] 17 | // If we remove DARE, the network falls apart into two parts (containing [VICI], and [DAI], respectively) 18 | val testPlaces = Seq( 19 | createTestPlace("http://vici.org/vici/2725", "Colonia Vienna", "http://www.imperium.ahlfeldt.se/places/106"), 20 | createTestPlace("http://www.imperium.ahlfeldt.se/places/106", "Vienne", "http://sws.geonames.org/2969284"), 21 | createTestPlace("http://gazetteer.dainst.org/place/2080717", "Vienne", "http://sws.geonames.org/2969284") 22 | ) 23 | 24 | "IndexedPlaceNetwork.buildNetworks" should { 25 | 26 | "build a single network from the test data" in { 27 | val networks = IndexedPlaceNetwork.buildNetworks(testPlaces) 28 | (networks.size) must equalTo (1) 29 | } 30 | 31 | "build two networks if DARE is removed from the test data" in { 32 | val placesWithoutDARE = testPlaces.filter(!_.uri.startsWith("http://www.imperium.ahlfeldt.se")) 33 | val networks = IndexedPlaceNetwork.buildNetworks(placesWithoutDARE) 34 | (networks.size) must equalTo (2) 35 | } 36 | 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /test/ingest/TEIImporterSpec.scala: -------------------------------------------------------------------------------- 1 | package ingest 2 | 3 | import java.io.File 4 | import org.specs2.mutable._ 5 | import org.specs2.runner._ 6 | import org.junit.runner._ 7 | import play.api.Play.current 8 | import play.api.test._ 9 | import play.api.test.Helpers._ 10 | import scala.io.Source 11 | import models.core.Dataset 12 | import java.sql.Date 13 | import play.api.db.slick._ 14 | 15 | @RunWith(classOf[JUnitRunner]) 16 | class TEIImporterTest extends Specification { 17 | 18 | val TEST_FILE = "test/resources/sample-short.tei.xml" 19 | 20 | "TEIImporter.importTEI" should { 21 | 22 | "not fail" in { 23 | running(FakeApplication()) { 24 | val teiFile = new File(TEST_FILE) 25 | 26 | val now = new Date(System.currentTimeMillis) 27 | val dataset = 28 | Dataset("42", "Sample Dataset", "Sample Publisher", "CC-BY 3.0", 29 | now, now, None, None, None, None, None, None, None, None) 30 | 31 | DB.withSession { implicit session: Session => 32 | TEIImporter.importTEI(Source.fromFile(TEST_FILE, "UTF-8"), dataset) 33 | } 34 | 35 | (1) must equalTo (1) 36 | } 37 | } 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /test/models/TaxonomySpec.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import org.specs2.mutable._ 4 | import org.specs2.runner._ 5 | import org.junit.runner._ 6 | 7 | import models.geo._ 8 | 9 | import play.api.test._ 10 | import play.api.test.Helpers._ 11 | 12 | @RunWith(classOf[JUnitRunner]) 13 | class TaxonomySpec extends Specification { 14 | 15 | "Taxonomy" should { 16 | 17 | "return correct path for a set of tags" in { 18 | val tags = Seq("Inscription", "Scholarship") 19 | val paths = Taxonomy.getPaths(tags) 20 | 21 | println(paths.toString) 22 | 23 | paths must contain(===(Seq("Texts", "Inscriptions"))) 24 | paths must contain(===(Seq("Scholarship"))) 25 | } 26 | 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /test/resources/sample-short.tei.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Krieg gegen Terror Bush erwartet von Nato-Verbündeten Unterstützung 7 | 8 | 9 |

    Not for re-distribution. Crawled on 2014-09-09T13:03:57Z

    10 |
    11 | 12 |

    Orginal publication date

    13 |
    14 |
    15 |
    16 | 17 | 18 |

    In this way, the Persians say (and not as the Greeks), was how Io came to 19 | Egypt, and this, 20 | according to them, was the first wrong that was done. Next, according to their 21 | story, some Greeks (they cannot say who) landed at 22 | Tyre in 23 | Phoenicia and carried 24 | off the king's daughter Europa.

    25 | 26 |
    27 |
    28 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # TODOs & Future Features 2 | 3 | * While in local search, the 'Show all results' hit is confusing. It 4 | should better convey that this number relates to all results on the current map area, 5 | e.g. something like 'Total results in map area:'. 6 | * Preview thumbnails are currently not 'local search sensitive'. They definitely MUST 7 | be restricted to the place while in local search, possibly SHOULD be restricted to 8 | the selected place (at least at the time of selection, until next map pan). 9 | * Re-implement proper display of text snippet previews (plus 10 | correct behavior of the JSON `snippet` field). 11 | * Revise hull computation (less smoothing, use original geometry 12 | when hull is only based on a single place). 13 | * Proper handling of dataset objects. (Also think about what to 14 | do when a dataset is selected from the search result list.) 15 | * Support PeriodO periods. 16 | * Timeslider touch Support. 17 | * Timeslider: 1AD marker overlap issue (when interval boundary 18 | is close to 1AD). 19 | * Timeslider: general visibility of current interval setting? 20 | * 'You have filters' warning icon (plus button to clear all 21 | filters at once?) 22 | * Filter editor popup: replace current X icon with checkmark + X 23 | icon for 'Accept'/'Cancel' functionality. 24 | * Help window (also document search syntax - prefix, fuzzy, 25 | booleans). 26 | * Proper 404 page. 27 | * Wait spinners all over. 28 | * Clean up custom icons (roll everything into an icon font). 29 | * How to handle objects contained in René's gazetteer? 30 | * Minor issue with histogram boundaries: the lower boundary is ok; the upper boundary should 31 | be: bucket value (=bucket interval start time) plus bucket width. 32 | * When dropping a dataset, it seems only items are properly removed (but not annotations). 33 | * __BUG:__ orphaned selection marker. 34 | --------------------------------------------------------------------------------