diff --git a/Dockerfile b/Dockerfile index a63efb22c2..7f37b0fc1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN npm install COPY . /app -# See https://github.com/phusion/passenger-docker +# See https://github.com/phusion/passenger-docker RUN mkdir -p /etc/my_init.d ADD config/docker/nginx/init.sh /etc/my_init.d/init.sh RUN chmod +x /etc/my_init.d/init.sh && \ diff --git a/Gemfile b/Gemfile index 39a122db02..4ac85ca567 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem 'zip_tricks', '~> 5.6' gem 'daemons', '~> 1.4.1' gem 'tzinfo-data', '~> 1.2019' # , '>= 1.2019.3' gem 'psych', '~> 5.1' -gem 'rmagick', '~> 6.0' +gem 'rmagick', '~> 6.0' gem 'roo', '~> 2.8', '>= 2.8.3' gem 'roo-xls', '~> 1.2' @@ -36,8 +36,6 @@ gem 'matrix', '~> 0.4.2' # Geo -# gem 'rgeo-shapefile', '~> 0.4.2' # deprecated? not compatible- perhaps only used in - # gem 'ffi-geos', '~> 2.3.0' # gem 'rgeo', '~> 2.2' # gem 'rgeo-geojson', '~> 2.1', '>= 2.1.1' @@ -47,6 +45,7 @@ gem 'ffi-geos', '~> 2.4.0' gem 'rgeo', '~> 3.0' gem 'rgeo-geojson', '~> 2.1', '>= 2.1.1' gem 'rgeo-proj4', '~> 4.0' #, '>= 3.0.1' +gem 'rgeo-shapefile', '~> 3.0' gem 'postgresql_cursor', '~> 0.6.1' diff --git a/Gemfile.lock b/Gemfile.lock index 10b4c5d114..4752e9044f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,6 +203,8 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.4.1) + dbf (4.3.2) + csv debug_inspector (1.2.0) delayed_job (4.1.13) activesupport (>= 3.0, < 9.0) @@ -528,6 +530,9 @@ GEM rgeo (>= 1.0.0) rgeo-proj4 (4.0.0) rgeo (~> 3.0.0) + rgeo-shapefile (3.1.0) + dbf (~> 4.0) + rgeo (>= 1.0) rmagick (6.0.1) observer (~> 0.1) pkg-config (~> 1.4) @@ -842,6 +847,7 @@ DEPENDENCIES rgeo (~> 3.0) rgeo-geojson (~> 2.1, >= 2.1.1) rgeo-proj4 (~> 4.0) + rgeo-shapefile (~> 3.0) rmagick (~> 6.0) roo (~> 2.8, >= 2.8.3) roo-xls (~> 1.2) diff --git a/app/controllers/asserted_distributions_controller.rb b/app/controllers/asserted_distributions_controller.rb index f02041c5bc..3144e0cbe9 100644 --- a/app/controllers/asserted_distributions_controller.rb +++ b/app/controllers/asserted_distributions_controller.rb @@ -116,7 +116,7 @@ def batch_load # PATCH /asserted_distributions/batch_update.json?asserted_distributions_query=<>&asserted_distribution={taxon_name_id=123}} def batch_update if r = AssertedDistribution.batch_update( - preview: params[:preview], + preview: params[:preview], asserted_distribution: asserted_distribution_params.merge(by: sessions_current_user_id), asserted_distribution_query: params[:asserted_distribution_query], ) diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 4602094fa4..a97b97f718 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -96,7 +96,10 @@ def search end def autocomplete - @documents = Queries::Document::Autocomplete.new(params[:term], project_id: params[:project_id]).all + @documents = Queries::Document::Autocomplete.new( + params[:term], + project_id: sessions_current_project_id + ).all end # GET /documents/select_options?target=Source diff --git a/app/controllers/gazetteer_imports_controller.rb b/app/controllers/gazetteer_imports_controller.rb new file mode 100644 index 0000000000..1195607b62 --- /dev/null +++ b/app/controllers/gazetteer_imports_controller.rb @@ -0,0 +1,79 @@ +class GazetteerImportsController < ApplicationController + before_action :set_gazetteer_import, only: %i[ show edit update destroy ] + + # GET /gazetteer_imports or /gazetteer_imports.json + def index + @gazetteer_imports = GazetteerImport.all + end + + # GET /gazetteer_imports/1 or /gazetteer_imports/1.json + def show + end + + # GET /gazetteer_imports/new + def new + @gazetteer_import = GazetteerImport.new + end + + # GET /gazetteer_imports/1/edit + def edit + end + + # POST /gazetteer_imports or /gazetteer_imports.json + def create + @gazetteer_import = GazetteerImport.new(gazetteer_import_params) + + respond_to do |format| + if @gazetteer_import.save + format.html { redirect_to gazetteer_import_url(@gazetteer_import), notice: "Gazetteer import was successfully created." } + format.json { render :show, status: :created, location: @gazetteer_import } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @gazetteer_import.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /gazetteer_imports/1 or /gazetteer_imports/1.json + def update + respond_to do |format| + if @gazetteer_import.update(gazetteer_import_params) + format.html { redirect_to gazetteer_import_url(@gazetteer_import), notice: "Gazetteer import was successfully updated." } + format.json { render :show, status: :ok, location: @gazetteer_import } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @gazetteer_import.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /gazetteer_imports/1 or /gazetteer_imports/1.json + def destroy + @gazetteer_import.destroy! + + respond_to do |format| + format.html { redirect_to gazetteer_imports_url, notice: "Gazetteer import was successfully destroyed." } + format.json { head :no_content } + end + end + + # GET /gazetteer_imports/all.json + def all + @import_jobs = GazetteerImport + .joins('JOIN users ON gazetteer_imports.created_by_id = users.id') + .select('gazetteer_imports.*, users.name AS submitted_by') + .where(project_id: sessions_current_project_id) + .order(created_at: :desc) + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_gazetteer_import + @gazetteer_import = GazetteerImport.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def gazetteer_import_params + params.fetch(:gazetteer_import, {}) + end +end diff --git a/app/controllers/gazetteers_controller.rb b/app/controllers/gazetteers_controller.rb new file mode 100644 index 0000000000..ed0867af70 --- /dev/null +++ b/app/controllers/gazetteers_controller.rb @@ -0,0 +1,207 @@ +class GazetteersController < ApplicationController + include DataControllerConfiguration::ProjectDataControllerConfiguration + include Lib::Vendor::RgeoShapefileHelper + require_dependency Rails.root.to_s + '/lib/vendor/rgeo.rb' + + before_action :set_gazetteer, only: %i[ show edit update destroy ] + + # GET /gazetteers + # GET /gazetteers.json + def index + respond_to do |format| + format.html do + @recent_objects = Gazetteer + .recent_from_project_id(sessions_current_project_id) + .order(updated_at: :desc) + .limit(10) + render '/shared/data/all/index' + end + format.json do + return + # no filter on GZs yet + end + end + end + + # GET /gazetteers/1 or /gazetteers/1.json + def show + end + + # GET /gazetteers/new + def new + respond_to do |format| + format.html { redirect_to new_gazetteer_task_path } + end + end + + # GET /gazetteers/1/edit + def edit + respond_to do |format| + format.html { + redirect_to new_gazetteer_task_path gazetteer_id: @gazetteer.id + } + end + end + + # GET /gazetteers/list + def list + @gazetteers = Gazetteer + .with_project_id(sessions_current_project_id) + .page(params[:page]).per(params[:per]) + end + + # POST /gazetteers.json + def create + @gazetteer = Gazetteer.new(gazetteer_params) + + @gazetteer.build_gi_from_shapes( + shape_params['shapes'], params.require('geometry_operation_is_union') + ) + if @gazetteer.errors.include?(:base) + render json: @gazetteer.errors, status: :unprocessable_entity + return + end + + begin + Gazetteer + .save_and_clone_to_projects(@gazetteer, projects_param['projects']) + + render :show, status: :created, location: @gazetteer + rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.message }, status: :unprocessable_entity + end + end + + # PATCH/PUT /gazetteers/1 + # PATCH/PUT /gazetteers/1.json + def update + respond_to do |format| + if @gazetteer.update(gazetteer_params) + format.html { redirect_to gazetteer_url(@gazetteer) } + format.json { render :show, status: :ok } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @gazetteer.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /gazetteers/1 + # DELETE /gazetteers/1.json + def destroy + @gazetteer.destroy! + + respond_to do |format| + format.html { + redirect_to gazetteers_url, + notice: 'Gazetteer was successfully destroyed.' + } + format.json { head :no_content } + end + end + + def autocomplete + @gazetteers = ::Queries::Gazetteer::Autocomplete.new( + params.require(:term), + project_id: sessions_current_project_id, + ).autocomplete + end + + def search + if params[:id].blank? + redirect_to(gazetteer_path, + alert: 'You must select an item from the list with a click or tab press before clicking show.') + else + redirect_to gazetteer_path(params[:id]) + end + end + + # GET /gazetteers/download + def download + send_data Export::CSV.generate_csv( + Gazetteer.where(project_id: sessions_current_project_id) + ), + type: 'text', + filename: "gazetteers_#{DateTime.now}.tsv" + end + + # POST /gazetteers/import.json + def import + begin + addShapefileImportJobToQueue( + shapefile_params, + citation_params, projects_param['projects'], + sessions_current_project_id, sessions_current_user_id + ) + rescue TaxonWorks::Error => e + render json: { errors: e.message }, status: :unprocessable_entity + return + end + + head :no_content + end + + # Using POST instead of GET to support long WKT strings + # POST /gazetteers/preview.json + def preview + begin + s = Gazetteer.combine_shapes_to_rgeo( + shape_params['shapes'], params.require('geometry_operation_is_union') + ) + rescue TaxonWorks::Error => e + render json: { base: [e.message] }, status: :unprocessable_entity + return + end + + f = RGeo::GeoJSON::Feature.new(s) + @shape = RGeo::GeoJSON.encode(f) + end + + # GET /gazetteers/shapefile_fields.json + def shapefile_fields + begin + @shapefile_fields = + fields_from_shapefile( + params[:shp_doc_id], params[:dbf_doc_id], sessions_current_project_id + ) + rescue TaxonWorks::Error => e + render json: { errors: e }, status: :unprocessable_entity + return + end + end + + private + + def set_gazetteer + @gazetteer = Gazetteer.find(params[:id]) + end + + def gazetteer_params + params.require(:gazetteer).permit(:name, :parent_id, + :iso_3166_a2, :iso_3166_a3) + end + + def projects_param + params.permit(projects: []) + end + + def shape_params + params.require(:gazetteer).permit( + shapes: { geojson: [], wkt: [], points: [], ga_combine: [], gz_combine: [] } + ) + end + + def shapefile_params + params.require(:shapefile).permit( + :shp_doc_id, :shx_doc_id, :dbf_doc_id, :prj_doc_id, :cpg_doc_id, + :name_field, :iso_a2_field, :iso_a3_field + ) + end + + def citation_params + params.require(:citation_options).permit( + :cite_gzs, citation: [:source_id, :pages, :is_original] + ) + end + +end diff --git a/app/controllers/tasks/gazetteers/import_gazetteers_controller.rb b/app/controllers/tasks/gazetteers/import_gazetteers_controller.rb new file mode 100644 index 0000000000..875286599b --- /dev/null +++ b/app/controllers/tasks/gazetteers/import_gazetteers_controller.rb @@ -0,0 +1,4 @@ +class Tasks::Gazetteers::ImportGazetteersController < ApplicationController + include TaskControllerConfiguration + +end \ No newline at end of file diff --git a/app/controllers/tasks/gazetteers/new_gazetteer_controller.rb b/app/controllers/tasks/gazetteers/new_gazetteer_controller.rb new file mode 100644 index 0000000000..3b25c272b9 --- /dev/null +++ b/app/controllers/tasks/gazetteers/new_gazetteer_controller.rb @@ -0,0 +1,4 @@ +class Tasks::Gazetteers::NewGazetteerController < ApplicationController + include TaskControllerConfiguration + +end \ No newline at end of file diff --git a/app/controllers/tasks/gis/match_georeference_controller.rb b/app/controllers/tasks/gis/match_georeference_controller.rb index f5fee75445..ee3991f937 100644 --- a/app/controllers/tasks/gis/match_georeference_controller.rb +++ b/app/controllers/tasks/gis/match_georeference_controller.rb @@ -70,7 +70,7 @@ def drawn_collecting_events when 'polygon' @collecting_events = CollectingEvent.with_project_id(sessions_current_project_id) .joins(:geographic_items) - .where(GeographicItem.contained_by_wkt_sql(geometry)) + .where(GeographicItem.covered_by_wkt_sql(geometry)) else end end @@ -143,7 +143,7 @@ def drawn_georeferences when 'polygon' @georeferences = Georeference.with_project_id(sessions_current_project_id) .joins(:geographic_item) - .where(GeographicItem.contained_by_wkt_sql(geometry)) + .where(GeographicItem.covered_by_wkt_sql(geometry)) else end if @georeferences.blank? diff --git a/app/helpers/collecting_events_helper.rb b/app/helpers/collecting_events_helper.rb index b9eba27498..3b0f903d43 100644 --- a/app/helpers/collecting_events_helper.rb +++ b/app/helpers/collecting_events_helper.rb @@ -241,8 +241,7 @@ def collecting_event_to_simple_json_feature(collecting_event) if collecting_event.geographic_items.any? geo_item_id = collecting_event.geographic_items.select(:id).first.id - query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" - base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) + base['geometry'] = GeographicItem.find(geo_item_id).to_geo_json end base end diff --git a/app/helpers/gazetteer_imports_helper.rb b/app/helpers/gazetteer_imports_helper.rb new file mode 100644 index 0000000000..2e6ef6b560 --- /dev/null +++ b/app/helpers/gazetteer_imports_helper.rb @@ -0,0 +1,2 @@ +module GazetteerImportsHelper +end diff --git a/app/helpers/gazetteers_helper.rb b/app/helpers/gazetteers_helper.rb new file mode 100644 index 0000000000..09e4b0e2ee --- /dev/null +++ b/app/helpers/gazetteers_helper.rb @@ -0,0 +1,33 @@ +module GazetteersHelper + def gazetteer_tag(gazetteer) + return nil if gazetteer.nil? + gazetteer.name + end + + def label_for_gazetteer(gazetteer) + return nil if gazetteer.nil? + gazetteer.name + end + + def gazetteer_link(gazetteer, link_text = nil) + return nil if gazetteer.nil? + link_text ||= gazetteer.name + link_to(link_text, gazetteer) + end + + def gazetteer_link_list(gazetteers) + content_tag(:ul) do + gazetteers.collect { |a| content_tag(:li, gazetteer_link(a)) }.join.html_safe + end + end + + def gazetteer_autocomplete_tag(gazetteer) + gazetteer_tag(gazetteer) + end + + def gazetteers_search_form + render('/gazetteers/quick_search_form') + end + + +end diff --git a/app/helpers/geographic_areas_helper.rb b/app/helpers/geographic_areas_helper.rb index 427a68c3b8..21502f0c8f 100644 --- a/app/helpers/geographic_areas_helper.rb +++ b/app/helpers/geographic_areas_helper.rb @@ -10,15 +10,14 @@ def label_for_geographic_area(geographic_area) geographic_area.name end - def geographic_area_autocomplete_tag(geographic_area, term) + def geographic_area_autocomplete_tag(geographic_area, term, mark = true) return nil if geographic_area.nil? - - if term + if term && mark s = geographic_area.name.gsub(/#{Regexp.escape(term)}/i, "#{term}") + ' ' else s = geographic_area.name + ' ' end - + s = [geographic_area&.parent&.parent&.name, geographic_area&.parent&.name, s].compact.join(': ').gsub('Earth: ', '') s += content_tag(:span, geographic_area.geographic_area_type.name, class: [:feedback, 'feedback-info', 'feedback-thin']) unless geographic_area.geographic_area_type.nil? diff --git a/app/helpers/geographic_items_helper.rb b/app/helpers/geographic_items_helper.rb index ddc571854d..8f4f31c5e3 100644 --- a/app/helpers/geographic_items_helper.rb +++ b/app/helpers/geographic_items_helper.rb @@ -2,16 +2,11 @@ module GeographicItemsHelper def geographic_item_tag(geographic_item) return nil if geographic_item.nil? - geographic_item.to_param + geographic_item.to_param end def json_tag(geographic_item) - retval = geographic_item.to_geo_json_feature.to_json - retval - end - - def center_coord_tag(geographic_item) - geographic_item.center_coords.join(', ') + geographic_item&.to_geo_json_feature.to_json end def geographic_item_link(geographic_item, link_text = nil) @@ -26,10 +21,10 @@ def geographic_item_links(geographic_items) end def geographic_item_parent_nav_links(geographic_item) - data = {} + data = {} geographic_item.parent_geographic_areas.each do |a| data[a] = a.geographic_items - end + end content_tag(:div, data.collect { |k, v| [ @@ -40,17 +35,17 @@ def geographic_item_parent_nav_links(geographic_item) def children_through_geographic_areas_links(geographic_item) data = {} - geographic_item.geographic_areas.each do |a| + geographic_item.geographic_areas.each do |a| a.children.collect{ |c| data[c] = c.geographic_items.all } - end + end links = [] data.each do |k,v| next if v.nil? links += v.collect{ |i| geographic_item_link(i, k.name) } - end + end links.join(', ').html_safe end diff --git a/app/helpers/lib/vendor/rgeo_shapefile_helper.rb b/app/helpers/lib/vendor/rgeo_shapefile_helper.rb new file mode 100644 index 0000000000..7e8bff47e4 --- /dev/null +++ b/app/helpers/lib/vendor/rgeo_shapefile_helper.rb @@ -0,0 +1,219 @@ +require_dependency Rails.root.to_s + '/lib/vendor/rgeo.rb' + +module Lib::Vendor::RgeoShapefileHelper + # Raises TaxonWorks::Error on error. + def addShapefileImportJobToQueue( + shapefile, citation, projects, project_id, user_id + ) + shapefile_docs = validate_shape_file(shapefile, project_id) + + if citation[:cite_gzs] && !citation[:citation]&.dig(:source_id) + raise TaxonWorks::Error, 'No citation source selected' + end + + complete_shapefile = shapefile + # shp_doc_id was required, the following may have been determined instead + # during validation. + complete_shapefile[:shx_doc_id] = shapefile_docs[:shx].id + complete_shapefile[:dbf_doc_id] = shapefile_docs[:dbf].id + complete_shapefile[:prj_doc_id] = shapefile_docs[:prj].id + complete_shapefile[:cpg_doc_id] = shapefile_docs[:cpg]&.id + + progress_tracker = GazetteerImport.create!( + shapefile: shapefile_docs[:shp].document_file_file_name + ) + ImportGazetteersJob.perform_later( + complete_shapefile, + citation, + user_id, project_id, + progress_tracker, + projects + ) + end + + # @return [Hash of Documents] Raises TaxonWorks::Error on error. + def fetch_shapefile_documents(shapefile, project_id) + begin + docs = { + shp: shapefile[:shp_doc_id] ? Document.find(shapefile[:shp_doc_id]) : nil, + shx: shapefile[:shx_doc_id] ? Document.find(shapefile[:shx_doc_id]) : nil, + dbf: shapefile[:dbf_doc_id] ? Document.find(shapefile[:dbf_doc_id]) : nil, + prj: shapefile[:prj_doc_id] ? Document.find(shapefile[:prj_doc_id]) : nil, + cpg: shapefile[:cpg_doc_id] ? Document.find(shapefile[:cpg_doc_id]) : nil + } + rescue ActiveRecord::RecordNotFound => e + raise TaxonWorks::Error, e + end + + raise TaxonWorks::Error, 'A .shp file is required' if docs[:shp].nil? + + base = basename(docs[:shp].document_file_file_name) + + docs.each do |ext, doc| + if !doc + docs[ext] = find_doc_for_extension(base, ext, project_id) + elsif basename(doc.document_file_file_name) != base + raise TaxonWorks::Error, ".#{ext} file must have the same name as the .shp file: '#{base}'" + end + end + + return docs + end + + # Assumes an extension of the form .xyz + def basename(filename) + filename[0, filename.size - 4] + end + + # @return [Hash] of shapefile ext => Document. + # Raises TaxonWorks::Error on error. + def validate_shape_file(shapefile, project_id) + if shapefile[:name_field].nil? + raise TaxonWorks::Error, 'Name field is required' + end + name_field = shapefile[:name_field] + + docs = fetch_shapefile_documents(shapefile, project_id) + + # Check that we can transform from the input CRS to our WGS84 CRS + prj = File.read(docs[:prj].document_file.path) + begin + cs = RGeo::CoordSys::CS.create_from_wkt(prj) + rescue RGeo::Error::ParseError => e + raise TaxonWorks::Error, "Failed to parse the prj file: #{e}" + end + + if !cs.geographic? && !cs.projected? + raise TaxonWorks::Error, '.prj must be either geographic or projected' + end + + if !Vendor::Rgeo.coord_sys_is_wgs84?(cs) + # Make sure we can create a proj4 for the source CRS, it's needed for + # transforming coordinates. + begin + RGeo::CoordSys::Proj4.create(cs.to_s) + rescue RGeo::Error::InvalidProjection => e + raise TaxonWorks::Error, "Invalid prj file? #{e}" + end + end + + # Check that each record has a name. + dbf = ::DBF::Table.new(docs[:dbf].document_file.path) + if dbf.record_count == 0 + raise TaxonWorks::Error, 'Empty dbf file: shapefile must contain records' + end + + if !dbf.column_names.include?(name_field) + raise TaxonWorks::Error, "No column named '#{name_field}'" + end + + rv = dbf_column_type_is_string(dbf, name_field) + if rv != true + raise TaxonWorks::Error, "Name error: column '#{name_field}' for Gazetteer names should be a string field, not '#{rv}'" + end + + for i in 0...dbf.record_count + record = dbf.find(i) + if Utilities::Rails::Strings.nil_squish_strip(record[name_field]).nil? + raise TaxonWorks::Error, "Record #{i} has no name - names are required for all records" + end + end + + # Check that iso a2/a3 fields, if provided, exist and are of type String + iso_a2_field = shapefile[:iso_a2_field] + if iso_a2_field.present? + if !dbf.column_names.include?(iso_a2_field) + raise TaxonWorks::Error, "No column named '#{iso_a2_field}'" + end + + rv = dbf_column_type_is_string(dbf, iso_a2_field) + if rv != true + raise TaxonWorks::Error, "Iso_3166_a2 error: column '#{iso_a2_field}' for a2 values should be a string field, not '#{rv}'" + end + end + + iso_a3_field = shapefile[:iso_a3_field] + if iso_a3_field.present? + if !dbf.column_names.include?(iso_a3_field) + raise TaxonWorks::Error, "No column named '#{iso_a3_field}'" + end + + rv = dbf_column_type_is_string(dbf, iso_a3_field) + if rv != true + raise TaxonWorks::Error, "Iso_3166_a3 error: column '#{iso_a3_field}' for a3 values should be a string field, not '#{rv}'" + end + end + + # Check that the cpg encoding is recognized - strings can get returned + # encoded as binary if failure here is allowed. + # cf. https://github.com/rgeo/rgeo-shapefile/blob/d278da0b613425d64e3792497ac9cf474eec6e53/lib/rgeo/shapefile/reader.rb#L194-L198 + if docs[:cpg].present? + begin + encoding = nil + File.open(docs[:cpg].document_file.path, 'r') do |cpg| + encoding = cpg.read + end + Encoding.find(encoding.strip) + rescue Errno::ENOENT => e + raise TaxonWorks::Error, + "Failed to open .cpg document '#{docs[:cpg].id}'" + rescue ArgumentError => e # Unrecognized encoding + raise TaxonWorks::Error, + "'#{e}' from .cpg document '#{docs[:cpg].id}'" + end + end + + docs + end + + # @return true if true, else return actual column type as a string + # Assumes the column_name is a valid dbf column name + def dbf_column_type_is_string(dbf, column_name) + column = dbf.columns.find { |c| c.name == column_name } + column.type == 'C' ? # 'C' is for 'C'haracter + true : DBF::Column::TYPE_CAST_CLASS[column.type.to_sym].to_s + end + + # Raises Taxonworks::Error on error + def fields_from_shapefile(shp_doc_id, dbf_doc_id, project_id) + if !shp_doc_id && !dbf_doc_id + raise TaxonWorks::Error, '.shp or .dbf required to read shapefile fields' + end + + if dbf_doc_id + dbf_doc = Document.find(dbf_doc_id) + else + shp_doc = Document.find(shp_doc_id) + base = basename(shp_doc.document_file_file_name) + dbf_doc = find_doc_for_extension(base, :dbf, project_id) + end + + dbf = ::DBF::Table.new(dbf_doc.document_file.path) + + dbf.column_names + end + + # Raises TaxonWorks::Error on error + def find_doc_for_extension(base, ext, project_id) + ext = ext.to_s + ext_filename = base + '.' + ext + ext_docs = Document.where( + document_file_file_name: ext_filename, + project_id: + ) + + if ext_docs.count == 0 + return nil if ext == 'cpg' # cpg isn't required + + raise TaxonWorks::Error, "Failed to find a '#{ext_filename}' document, has one been uploaded?" + elsif ext_docs.count > 1 + ids = ext_docs.map { |d| d.id } + # (This makes cpg required when there are multiple matching) + raise TaxonWorks::Error, "More than one '#{ext_filename}' document exists (ids #{ids.join(',')}), please add the correct one in the document selector" + end + + # exactly one matching document + ext_docs.first + end + +end \ No newline at end of file diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 5603d109dd..10b8beec55 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -113,6 +113,8 @@ import '../vue/tasks/data_attributes/field_synchronize/main.js' import '../vue/tasks/containers/new_container/main.js' import '../vue/tasks/observation_matrices/import_nexus/main.js' import '../vue/tasks/dwc_occurrences/filter/main.js' +import '../vue/tasks/gazetteers/new_gazetteer/main.js' +import '../vue/tasks/gazetteers/import_gazetteers/main.js' import '../vue/tasks/unify/objects/main.js' import '../vue/tasks/images/new_filename_depicting_image/main.js' import '../vue/tasks/biological_associations/new/main.js' diff --git a/app/javascript/vue/components/Filter/Facets/shared/FacetGazetteer.vue b/app/javascript/vue/components/Filter/Facets/shared/FacetGazetteer.vue new file mode 100644 index 0000000000..59f34e095b --- /dev/null +++ b/app/javascript/vue/components/Filter/Facets/shared/FacetGazetteer.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/app/javascript/vue/components/georeferences/manuallyComponent.vue b/app/javascript/vue/components/georeferences/manuallyComponent.vue index 8c18f5f119..7f37b25758 100644 --- a/app/javascript/vue/components/georeferences/manuallyComponent.vue +++ b/app/javascript/vue/components/georeferences/manuallyComponent.vue @@ -1,6 +1,7 @@