spatial_features 3.10.3 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/spatial_features/has_spatial_features/feature_import.rb +34 -4
- data/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +3 -1
- data/lib/spatial_features/importers/file.rb +19 -8
- data/lib/spatial_features/importers/kml.rb +24 -1
- data/lib/spatial_features/importers/shapefile.rb +9 -4
- data/lib/spatial_features/importers/unreadable_file.rb +47 -0
- data/lib/spatial_features/unzip.rb +57 -4
- data/lib/spatial_features/validation.rb +10 -3
- data/lib/spatial_features/version.rb +1 -1
- data/lib/spatial_features.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70aa244618e985dd8f41069f861b2356b99f36acf433fdd47dca49a02ffb0f94
|
|
4
|
+
data.tar.gz: 5c70438c1e82fcfa9a4d50d6c29a26dac323f623e45bf843e0fe18636b838c77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0348cdd4d19cc28556df7737f547363184311445c98c20e9c6025977e03be5724cef94c116f96c8ed2744013c6ce9f5b37adab61ba80ac19c38cc0e9917ddcb8'
|
|
7
|
+
data.tar.gz: 6355ba185d8ee20a0f7154c5c9c05e739ef6ad2718b24259c9b7bbf4178f3fc5b63d217817aad4bbca8a0572c5a51c61b576bad24e62fb15a14d0a74d368e378
|
|
@@ -6,6 +6,10 @@ module SpatialFeatures
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
include QueuedSpatialProcessing
|
|
8
8
|
|
|
9
|
+
# Read by submitters as often as by staff, so it says what happened rather than which
|
|
10
|
+
# method failed. Any per-file reasons are appended after it.
|
|
11
|
+
EMPTY_IMPORT_MESSAGE = "No mapped areas could be imported.".freeze
|
|
12
|
+
|
|
9
13
|
included do
|
|
10
14
|
extend ActiveModel::Callbacks
|
|
11
15
|
define_model_callbacks :update_features
|
|
@@ -49,7 +53,7 @@ module SpatialFeatures
|
|
|
49
53
|
store_feature_update_warnings(import_warnings)
|
|
50
54
|
|
|
51
55
|
if imports.present? && features.compact_blank.empty? && !allow_blank
|
|
52
|
-
raise EmptyImportError, [
|
|
56
|
+
raise EmptyImportError, [EMPTY_IMPORT_MESSAGE, *import_warnings].join(' ')
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
59
|
end
|
|
@@ -105,14 +109,30 @@ module SpatialFeatures
|
|
|
105
109
|
options = {}
|
|
106
110
|
end
|
|
107
111
|
|
|
108
|
-
Array.wrap(send(data_method)).flat_map do |data|
|
|
112
|
+
Array.wrap(send(data_method)).each_with_index.flat_map do |data, index|
|
|
109
113
|
next unless data.present?
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
source_tmpdir = spatial_import_tmpdir(tmpdir, data_method, index)
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
spatial_importer_from_name(importer_name).create_all(data, **options, make_valid: make_valid, tmpdir: source_tmpdir)
|
|
119
|
+
rescue ImportError, Zip::Error, Errno::ENOENT => e
|
|
120
|
+
# One unreadable file must not discard the geometry of the files uploaded
|
|
121
|
+
# beside it. Stand in for it so the rest of the import proceeds and the
|
|
122
|
+
# reason reaches the user as a warning against that file.
|
|
123
|
+
Importers::UnreadableFile.new(data, e, **options, make_valid: make_valid, tmpdir: source_tmpdir)
|
|
124
|
+
end
|
|
112
125
|
end
|
|
113
126
|
end.compact
|
|
114
127
|
end
|
|
115
128
|
|
|
129
|
+
# Every source file unpacks into its own directory. Two KMZs on one record both hold a
|
|
130
|
+
# `doc.kml`, so sharing one tmpdir means the second extraction collides with the first
|
|
131
|
+
# and the whole import dies on an archive that is perfectly fine.
|
|
132
|
+
def spatial_import_tmpdir(tmpdir, data_method, index)
|
|
133
|
+
::File.join(tmpdir, "#{data_method}-#{index}").tap {|dir| FileUtils.mkdir_p(dir) }
|
|
134
|
+
end
|
|
135
|
+
|
|
116
136
|
def spatial_importer_from_name(importer_name)
|
|
117
137
|
"SpatialFeatures::Importers::#{importer_name}".constantize
|
|
118
138
|
end
|
|
@@ -133,7 +153,7 @@ module SpatialFeatures
|
|
|
133
153
|
features.delete_all
|
|
134
154
|
valid, invalid = Feature.defer_aggregate_refresh do
|
|
135
155
|
Feature.without_caching_derivatives do
|
|
136
|
-
imports.flat_map(
|
|
156
|
+
imports.flat_map {|import| features_from(import) }.partition do |feature|
|
|
137
157
|
feature.spatial_model = self
|
|
138
158
|
if feature.save
|
|
139
159
|
handle_images(feature)
|
|
@@ -167,6 +187,16 @@ module SpatialFeatures
|
|
|
167
187
|
valid
|
|
168
188
|
end
|
|
169
189
|
|
|
190
|
+
# Parse failures surface lazily, when an importer's features are first read (e.g. a
|
|
191
|
+
# shapefile archive missing its `.shx`), so they need the same containment as a file
|
|
192
|
+
# that couldn't be opened at all: record the reason against that source and keep going.
|
|
193
|
+
def features_from(import)
|
|
194
|
+
import.features
|
|
195
|
+
rescue ImportError => e
|
|
196
|
+
import.warnings << e.message
|
|
197
|
+
[]
|
|
198
|
+
end
|
|
199
|
+
|
|
170
200
|
def features_cache_key_matches?(cache_key)
|
|
171
201
|
has_spatial_features_hash? && cache_key == features_hash
|
|
172
202
|
end
|
|
@@ -102,8 +102,10 @@ module SpatialFeatures
|
|
|
102
102
|
spatial_processing_jobs('update_features!').where(failed_at: nil, locked_at: nil)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Most recent first: a record that has failed more than once must report why the
|
|
106
|
+
# current attempt failed, not whichever row the database happened to return.
|
|
105
107
|
def failed_feature_update_jobs
|
|
106
|
-
spatial_processing_jobs('update_features!').where.not(failed_at: nil)
|
|
108
|
+
spatial_processing_jobs('update_features!').where.not(failed_at: nil).order(failed_at: :desc)
|
|
107
109
|
end
|
|
108
110
|
|
|
109
111
|
def spatial_processing_jobs(method_name = nil)
|
|
@@ -3,16 +3,27 @@ require 'open-uri'
|
|
|
3
3
|
module SpatialFeatures
|
|
4
4
|
module Importers
|
|
5
5
|
class File < SimpleDelegator
|
|
6
|
-
INVALID_ARCHIVE = "
|
|
7
|
-
SUPPORTED_FORMATS = "
|
|
6
|
+
INVALID_ARCHIVE = "This file doesn't contain any map data.".freeze
|
|
7
|
+
SUPPORTED_FORMATS = "Please upload a KMZ, KML, zipped ArcGIS shapefile, ESRI JSON, or GeoJSON file.".freeze
|
|
8
8
|
|
|
9
9
|
FILE_PATTERNS = [/\.kml$/, /\.shp$/, /\.json$/, /\.geojson$/]
|
|
10
10
|
def self.create_all(data, **options)
|
|
11
11
|
Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).map do |file|
|
|
12
12
|
new(data, **options, current_file: file)
|
|
13
13
|
end
|
|
14
|
-
rescue Unzip::PathNotFound
|
|
15
|
-
raise ImportError,
|
|
14
|
+
rescue Unzip::PathNotFound => e
|
|
15
|
+
raise ImportError, invalid_archive_message(e)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Name what the archive held instead of only what we needed — a submitter who is
|
|
19
|
+
# told their upload contains PDFs knows immediately that they attached the wrong
|
|
20
|
+
# file, where "did not contain a .shp" leaves them guessing.
|
|
21
|
+
def self.invalid_archive_message(path_not_found)
|
|
22
|
+
found = path_not_found.extensions
|
|
23
|
+
count = path_not_found.paths.count {|path| !path.end_with?('/') }
|
|
24
|
+
contents = " It contains #{count} #{found.to_sentence} #{'file'.pluralize(count)}." if found.any?
|
|
25
|
+
|
|
26
|
+
[INVALID_ARCHIVE, contents, " ", SUPPORTED_FORMATS].compact.join
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
# The File importer may be initialized multiple times by `::create_all` if it
|
|
@@ -24,8 +35,8 @@ module SpatialFeatures
|
|
|
24
35
|
def initialize(data, current_file: nil, **options)
|
|
25
36
|
begin
|
|
26
37
|
@current_file = current_file || Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
|
|
27
|
-
rescue Unzip::PathNotFound
|
|
28
|
-
raise ImportError,
|
|
38
|
+
rescue Unzip::PathNotFound => e
|
|
39
|
+
raise ImportError, self.class.invalid_archive_message(e)
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
case ::File.extname(data).downcase
|
|
@@ -43,14 +54,14 @@ module SpatialFeatures
|
|
|
43
54
|
when '.json', '.geojson'
|
|
44
55
|
__setobj__(ESRIGeoJSON.new(@current_file.path, **options))
|
|
45
56
|
else
|
|
46
|
-
import_error
|
|
57
|
+
import_error!
|
|
47
58
|
end
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
private
|
|
51
62
|
|
|
52
63
|
def import_error!
|
|
53
|
-
raise ImportError, "
|
|
64
|
+
raise ImportError, "#{::File.basename(filename)} isn't a file type we can read. " + SUPPORTED_FORMATS
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
def filename
|
|
@@ -47,10 +47,32 @@ module SpatialFeatures
|
|
|
47
47
|
doc.remove_namespaces! # We don't care about namespaces since the document is going to be filled with placemark geometry and we want it all without needing to deal with namespaces
|
|
48
48
|
raise ImportError, "Invalid KML document (root node was '#{doc.root&.name}')" unless doc.root&.name.to_s.casecmp?('kml')
|
|
49
49
|
discard_network_links(doc)
|
|
50
|
+
discard_overlays(doc)
|
|
50
51
|
doc
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# Overlays drape a georeferenced picture over the map — often a WMS raster exported
|
|
56
|
+
# from a government catalogue — instead of describing an area, so there is no
|
|
57
|
+
# geometry in them to import. A PhotoOverlay also carries a `<Point>` marking where
|
|
58
|
+
# the photo was taken, which is not a footprint either, so the nodes are removed
|
|
59
|
+
# rather than left for `each_record` to pick up. Treated like NetworkLinks: any real
|
|
60
|
+
# geometry in the file still imports, and a file that is nothing but overlays fails
|
|
61
|
+
# with a reason that tells the user what they actually uploaded.
|
|
62
|
+
OVERLAY_ELEMENTS = %w[GroundOverlay PhotoOverlay ScreenOverlay].freeze
|
|
63
|
+
|
|
64
|
+
def discard_overlays(doc)
|
|
65
|
+
overlays = doc.search(*OVERLAY_ELEMENTS)
|
|
66
|
+
return if overlays.empty?
|
|
67
|
+
|
|
68
|
+
names = overlays.map {|overlay| overlay.at_css('name')&.text.presence }.compact.uniq
|
|
69
|
+
described = names.any? ? ": #{names.to_sentence}" : ''
|
|
70
|
+
@warnings << "Skipped #{overlays.size} map #{'image'.pluralize(overlays.size)}#{described}. " \
|
|
71
|
+
"A map image is a picture laid over the map, not a marked area, so there is no boundary to import from it."
|
|
72
|
+
|
|
73
|
+
overlays.remove
|
|
74
|
+
end
|
|
75
|
+
|
|
54
76
|
# NetworkLinks reference geometry hosted elsewhere (e.g. a remote KMZ) rather
|
|
55
77
|
# than embedding it, so there is nothing for us to import from them. Rather than
|
|
56
78
|
# failing the whole file, we drop them and record a warning so any embedded
|
|
@@ -63,7 +85,8 @@ module SpatialFeatures
|
|
|
63
85
|
|
|
64
86
|
names = network_links.map {|link| link.at_css('name')&.text.presence }.compact.uniq
|
|
65
87
|
described = names.any? ? ": #{names.to_sentence}" : ''
|
|
66
|
-
@warnings << "Skipped #{network_links.size} network-linked #{'layer'.pluralize(network_links.size)}
|
|
88
|
+
@warnings << "Skipped #{network_links.size} network-linked #{'layer'.pluralize(network_links.size)}#{described}. " \
|
|
89
|
+
"Network links point at data stored somewhere else rather than holding it, so there is nothing to import from them."
|
|
67
90
|
|
|
68
91
|
network_links.remove
|
|
69
92
|
end
|
|
@@ -21,8 +21,11 @@ module SpatialFeatures
|
|
|
21
21
|
Download.open_each(data, unzip: [/\.shp$/], downcase: true).map do |file|
|
|
22
22
|
new(file, **options)
|
|
23
23
|
end
|
|
24
|
-
rescue Unzip::PathNotFound
|
|
25
|
-
|
|
24
|
+
rescue Unzip::PathNotFound => e
|
|
25
|
+
# `INVALID_ARCHIVE` lives on `Importers::File` and isn't in this class's constant
|
|
26
|
+
# lookup path, so the bare reference here raised NameError instead of the intended
|
|
27
|
+
# ImportError whenever a model imported with `'Shapefile'` directly.
|
|
28
|
+
raise ImportError, File.invalid_archive_message(e)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
private
|
|
@@ -36,7 +39,9 @@ module SpatialFeatures
|
|
|
36
39
|
rescue Errno::ENOENT => e
|
|
37
40
|
case e.message
|
|
38
41
|
when /No such file or directory @ rb_sysopen - (.+)/
|
|
39
|
-
raise IncompleteShapefileArchive,
|
|
42
|
+
raise IncompleteShapefileArchive,
|
|
43
|
+
"This shapefile is incomplete — #{::File.basename($1)} is missing. " \
|
|
44
|
+
"A shapefile is a set of files that have to be zipped up together: .shp, .shx, .dbf and .prj."
|
|
40
45
|
else
|
|
41
46
|
raise e
|
|
42
47
|
end
|
|
@@ -104,7 +109,7 @@ module SpatialFeatures
|
|
|
104
109
|
@possible_shp_files ||= begin
|
|
105
110
|
Download.open_each(archive, unzip: /\.shp$/, downcase: true)
|
|
106
111
|
rescue Unzip::PathNotFound
|
|
107
|
-
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "
|
|
112
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "This archive has no shapefile (.shp) in it."
|
|
108
113
|
end
|
|
109
114
|
end
|
|
110
115
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'digest/md5'
|
|
2
|
+
|
|
3
|
+
module SpatialFeatures
|
|
4
|
+
module Importers
|
|
5
|
+
# Stands in for a source file we couldn't read at all — an archive with no map data in
|
|
6
|
+
# it, a shapefile missing a component, an unsupported format. It behaves like an
|
|
7
|
+
# importer that found nothing and carries the reason as a warning, so the files
|
|
8
|
+
# uploaded beside it still import and the user is told which file was skipped and why.
|
|
9
|
+
#
|
|
10
|
+
# When every source is unreadable the import ends up empty and `update_features!`
|
|
11
|
+
# raises `EmptyImportError` with these warnings as the reason, the same way a file
|
|
12
|
+
# containing only NetworkLinks does.
|
|
13
|
+
class UnreadableFile < Base
|
|
14
|
+
# Fallbacks for failures that didn't come from an importer, whose own messages would
|
|
15
|
+
# mean nothing to the person who uploaded the file — and in the missing-file case
|
|
16
|
+
# would put a server filesystem path in front of them.
|
|
17
|
+
UNREADABLE = "This file couldn't be opened. It may be damaged, or saved in a format we can't read.".freeze
|
|
18
|
+
MISSING = "This file is no longer available on the server. Please upload it again.".freeze
|
|
19
|
+
|
|
20
|
+
def initialize(data, error, **options)
|
|
21
|
+
super(data, **options)
|
|
22
|
+
self.source_identifier ||= ::File.basename(data.to_s)
|
|
23
|
+
@warnings << reason_for(error)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Include the reason so that fixing an importer (or the user re-uploading) produces a
|
|
27
|
+
# different key and the record re-imports rather than matching its cached hash.
|
|
28
|
+
def cache_key
|
|
29
|
+
@cache_key ||= Digest::MD5.hexdigest([@data, *@warnings].join)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def reason_for(error)
|
|
35
|
+
case error
|
|
36
|
+
when ImportError then error.message
|
|
37
|
+
when Errno::ENOENT then MISSING
|
|
38
|
+
else UNREADABLE
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def each_record
|
|
43
|
+
# Nothing could be read, so there is nothing to yield.
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -5,17 +5,52 @@ module SpatialFeatures
|
|
|
5
5
|
# paths containing '__macosx' or beginning with a '.'
|
|
6
6
|
IGNORED_ENTRY_PATHS = /(\A|\/)(__macosx|\.)/i.freeze
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Archives that may themselves hold the file we're looking for. A KMZ is a ZIP with a
|
|
9
|
+
# KML inside it, and proponents routinely forward a ZIP of per-layer ZIPs rather than
|
|
10
|
+
# the layers themselves.
|
|
11
|
+
NESTED_ARCHIVE_PATTERN = /\.(zip|kmz)$/i.freeze
|
|
12
|
+
|
|
13
|
+
# How many times we'll unwrap a nested archive before giving up. Two levels covers
|
|
14
|
+
# every case we've seen in practice (a ZIP of shapefile ZIPs, a ZIP holding a KMZ)
|
|
15
|
+
# while bounding the work a deeply nested archive can cost us.
|
|
16
|
+
MAX_NESTING_DEPTH = 2
|
|
17
|
+
|
|
18
|
+
def self.paths(file_path, find: nil, depth: 0, **extract_options)
|
|
9
19
|
paths = extract(file_path, **extract_options)
|
|
10
20
|
|
|
11
21
|
if find = Array.wrap(find).presence
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
matches = matching_paths(paths, find)
|
|
23
|
+
# Only unwrap nested archives once the archive has yielded nothing usable, so
|
|
24
|
+
# archives that already import keep their current behaviour exactly.
|
|
25
|
+
matches = paths_in_nested_archives(paths, find: find, depth: depth, **extract_options) if matches.empty?
|
|
26
|
+
raise(PathNotFound.new("Archive did not contain a file matching #{find}", paths)) if matches.empty?
|
|
27
|
+
paths = matches
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
return Array(paths)
|
|
17
31
|
end
|
|
18
32
|
|
|
33
|
+
def self.matching_paths(paths, find)
|
|
34
|
+
paths.select {|path| find.any? {|pattern| path.index(pattern) } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Unwrap any archives the archive itself contained and search those instead. Each one
|
|
38
|
+
# extracts into its own directory so identically named entries (`doc.kml` in several
|
|
39
|
+
# KMZs) don't overwrite one another, and so a shapefile's sibling components stay
|
|
40
|
+
# beside it for `Validation.validate_shapefile!`.
|
|
41
|
+
def self.paths_in_nested_archives(paths, find:, depth:, tmpdir: nil, **extract_options)
|
|
42
|
+
return [] unless depth < MAX_NESTING_DEPTH
|
|
43
|
+
|
|
44
|
+
paths.grep(NESTED_ARCHIVE_PATTERN).flat_map do |archive|
|
|
45
|
+
begin
|
|
46
|
+
paths(archive, find: find, depth: depth + 1,
|
|
47
|
+
tmpdir: Dir.mktmpdir(nil, File.dirname(archive)), **extract_options)
|
|
48
|
+
rescue PathNotFound, Zip::Error
|
|
49
|
+
[] # This nested archive held nothing we can use; the others may still.
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
19
54
|
def self.extract(file_path, tmpdir: nil, downcase: false)
|
|
20
55
|
tmpdir ||= Dir.mktmpdir
|
|
21
56
|
[].tap do |paths|
|
|
@@ -55,6 +90,24 @@ module SpatialFeatures
|
|
|
55
90
|
|
|
56
91
|
# EXCEPTIONS
|
|
57
92
|
|
|
58
|
-
class PathNotFound < StandardError
|
|
93
|
+
class PathNotFound < StandardError
|
|
94
|
+
# The paths the archive did contain, so callers can tell the user what we found
|
|
95
|
+
# instead of only what we needed.
|
|
96
|
+
attr_reader :paths
|
|
97
|
+
|
|
98
|
+
def initialize(message = nil, paths = [])
|
|
99
|
+
super(message)
|
|
100
|
+
@paths = Array(paths)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Distinct file types in the archive, upcased for display (e.g. `["PDF", "PNG"]`).
|
|
104
|
+
def extensions
|
|
105
|
+
paths.reject {|path| path.end_with?('/') }
|
|
106
|
+
.map {|path| File.extname(path).delete('.').upcase }
|
|
107
|
+
.reject(&:empty?)
|
|
108
|
+
.uniq
|
|
109
|
+
.sort
|
|
110
|
+
end
|
|
111
|
+
end
|
|
59
112
|
end
|
|
60
113
|
end
|
|
@@ -22,11 +22,18 @@ module SpatialFeatures
|
|
|
22
22
|
component_path = "#{path}.#{ext}"
|
|
23
23
|
next if ::File.file?(component_path) && ::File.readable?(component_path)
|
|
24
24
|
|
|
25
|
+
# A shapefile is a set of files that must travel together, and an export that
|
|
26
|
+
# drops one is the single most common thing wrong with an upload. Name the
|
|
27
|
+
# missing part and the way out, rather than only the file we couldn't find.
|
|
25
28
|
case ext
|
|
26
29
|
when "prj"
|
|
27
|
-
raise ::SpatialFeatures::Importers::IndeterminateShapefileProjection,
|
|
30
|
+
raise ::SpatialFeatures::Importers::IndeterminateShapefileProjection,
|
|
31
|
+
"This shapefile has no projection file — #{File.basename(component_path)} is missing, " \
|
|
32
|
+
"so there is no way to tell where on the earth it belongs. Re-export it with the projection included."
|
|
28
33
|
else
|
|
29
|
-
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive,
|
|
34
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive,
|
|
35
|
+
"This shapefile is incomplete — #{File.basename(component_path)} is missing. " \
|
|
36
|
+
"A shapefile is a set of files that have to be zipped up together: .shp, .shx, .dbf and .prj."
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
|
|
@@ -41,7 +48,7 @@ module SpatialFeatures
|
|
|
41
48
|
validate_shapefile!(shp_file, default_proj4_projection: default_proj4_projection)
|
|
42
49
|
end
|
|
43
50
|
rescue Unzip::PathNotFound
|
|
44
|
-
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "
|
|
51
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "This archive has no shapefile (.shp) in it." \
|
|
45
52
|
unless allow_generic_zip_files
|
|
46
53
|
end
|
|
47
54
|
end
|
data/lib/spatial_features.rb
CHANGED
|
@@ -28,6 +28,7 @@ require 'spatial_features/importers/kml_file'
|
|
|
28
28
|
require 'spatial_features/importers/kml_file_arcgis'
|
|
29
29
|
require 'spatial_features/importers/geomark'
|
|
30
30
|
require 'spatial_features/importers/shapefile'
|
|
31
|
+
require 'spatial_features/importers/unreadable_file'
|
|
31
32
|
|
|
32
33
|
require 'spatial_features/engine'
|
|
33
34
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spatial_features
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Wallace
|
|
@@ -210,6 +210,7 @@ files:
|
|
|
210
210
|
- lib/spatial_features/importers/kml_file.rb
|
|
211
211
|
- lib/spatial_features/importers/kml_file_arcgis.rb
|
|
212
212
|
- lib/spatial_features/importers/shapefile.rb
|
|
213
|
+
- lib/spatial_features/importers/unreadable_file.rb
|
|
213
214
|
- lib/spatial_features/uncached_result.rb
|
|
214
215
|
- lib/spatial_features/unzip.rb
|
|
215
216
|
- lib/spatial_features/utils.rb
|