aqila-mapas 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1223 -0
- data/.travis.yml +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +49 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/aqila-mapas.gemspec +36 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/aqila/mapas/railtie.rb +13 -0
- data/lib/aqila/mapas/version.rb +7 -0
- data/lib/aqila/mapas.rb +57 -0
- data/lib/map/download_tiles_service.rb +74 -0
- data/lib/map/file_import_basic.rb +30 -0
- data/lib/map/float.rb +13 -0
- data/lib/map/gdal/base.rb +66 -0
- data/lib/map/gdal/colorize_service.rb +82 -0
- data/lib/map/gdal/contrast_stretch_service.rb +26 -0
- data/lib/map/gdal/crop_service.rb +36 -0
- data/lib/map/gdal/gdal_info_service.rb +32 -0
- data/lib/map/gdal/grid_service.rb +36 -0
- data/lib/map/gdal/merge_service.rb +22 -0
- data/lib/map/gdal/ndvi_service.rb +32 -0
- data/lib/map/gdal/ogr2ogr_service.rb +23 -0
- data/lib/map/gdal/ogri_info_service.rb +35 -0
- data/lib/map/gdal/polygonize_service.rb +36 -0
- data/lib/map/gdal/raster_service.rb +36 -0
- data/lib/map/gdal/rgb_service.rb +27 -0
- data/lib/map/gdal/table_colors.txt +52 -0
- data/lib/map/gdal/tiles_service.rb +21 -0
- data/lib/map/gdal/translate_service.rb +37 -0
- data/lib/map/gdal/warp_service.rb +22 -0
- data/lib/map/gleba_tiles_service.rb +29 -0
- data/lib/map/gpx_service.rb +34 -0
- data/lib/map/kml_creator_line_service.rb +31 -0
- data/lib/map/kml_creator_service.rb +31 -0
- data/lib/map/kml_edit_service.rb +122 -0
- data/lib/map/kml_offset_service.rb +59 -0
- data/lib/map/kml_service.rb +35 -0
- data/lib/map/lat_lon_service.rb +93 -0
- data/lib/map/polygon_service.rb +100 -0
- data/lib/map/rgeo_service.rb +42 -0
- data/lib/map/shape_to_tif_service.rb +89 -0
- data/lib/map/tile_service.rb +22 -0
- data/lib/map/tiles_base.rb +11 -0
- data/lib/map/tracking_cleaner_service.rb +78 -0
- data/lib/satellite/imagery_proccessor.rb +70 -0
- data/lib/satellite/landsat8/coordinate_converter_service.rb +40 -0
- data/lib/satellite/landsat8/imagery_service.rb +113 -0
- data/lib/satellite/sentinel2/coordinate_converter_service.rb +90 -0
- data/lib/satellite/sentinel2/imagery_service.rb +144 -0
- metadata +137 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::NdviService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
def initialize(tif_red, tif_near_red)
|
7
|
+
@tif_red = tif_red
|
8
|
+
@tif_near_red = tif_near_red
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
tif_out = get_path_to_temp_file('calculated', 'tif')
|
13
|
+
add_to_clean(tif_out)
|
14
|
+
add_to_clean("#{tif_out}.aux.xml")
|
15
|
+
|
16
|
+
run_command(
|
17
|
+
%{
|
18
|
+
gdal_calc.py
|
19
|
+
--overwrite
|
20
|
+
-B #{@tif_red}
|
21
|
+
-A #{@tif_near_red}
|
22
|
+
--outfile=#{tif_out}
|
23
|
+
--calc="(A.astype(float)-B)/(A.astype(float)+B)"
|
24
|
+
--overwrite
|
25
|
+
--type=Float32
|
26
|
+
--NoDataValue=-9999
|
27
|
+
}.squish
|
28
|
+
)
|
29
|
+
|
30
|
+
tif_out
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# ogr2ogr - Converts simple features data between file formats.
|
3
|
+
# http://www.gdal.org/ogr2ogr.html
|
4
|
+
class Map::Gdal::Ogr2ogrService
|
5
|
+
include Map::Gdal::Base
|
6
|
+
|
7
|
+
attr_reader :file
|
8
|
+
def initialize(file)
|
9
|
+
raise 'File does not exist' unless File.exist?(file)
|
10
|
+
@file = file
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(options)
|
14
|
+
raise 'File does no exist' unless File.exist?(file)
|
15
|
+
raise 'Format does not specified' unless options[:f]
|
16
|
+
|
17
|
+
out = get_path_to_temp_file(:ogr2ogr, options[:f])
|
18
|
+
add_to_clean(out)
|
19
|
+
|
20
|
+
run_command(%{ogr2ogr #{options_to_command_line(options)} "#{out}" "#{file}"})
|
21
|
+
out
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::OgriInfoService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
attr_reader :file
|
7
|
+
def initialize(file)
|
8
|
+
raise 'File does not exist' unless File.exist?(file)
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def layer_name
|
13
|
+
# quando é um shapefile a informação vem seguida do tipo de dado
|
14
|
+
# Exemplo: Campinho (Point)
|
15
|
+
call(q: '').gsub(/\d+:/, '').gsub(/\s\(.+$/, '').strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def limits
|
19
|
+
# Saída de exemplo Extent: (-52.726602, -27.567020) - (-52.714728, -27.554208)
|
20
|
+
# O match retorna[1] (-52.726602, -27.567020) - (-52.714728, -27.554208)
|
21
|
+
# O primeiro gsub retorna (-52.726602, -27.567020) (-52.714728, -27.554208)
|
22
|
+
# O segundo gsub retorna -52.726602 -27.567020 -52.714728 -27.554208
|
23
|
+
call({}, layer_name)
|
24
|
+
.match(/Extent: (.+)/)[1]
|
25
|
+
.gsub(/\s-\s/, ' ')
|
26
|
+
.gsub(/[^0-9\.\s-]/, '')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def call(options = {}, layer = nil)
|
32
|
+
layer = "\"#{layer}\"" if layer
|
33
|
+
run_command(%{ogrinfo -geom=NO -ro #{options_to_command_line(options)} "#{file}" #{layer}})
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zip'
|
4
|
+
|
5
|
+
class Map::Gdal::PolygonizeService
|
6
|
+
include Map::Gdal::Base
|
7
|
+
|
8
|
+
def initialize(tif_path)
|
9
|
+
@tif_path = tif_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(options = {})
|
13
|
+
if options[:kml]
|
14
|
+
@tif_path = Map::Gdal::CropService.new(@tif_path).call(reference: IO.binread(options[:kml]))
|
15
|
+
add_to_clean(@tif_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
file_id = "#{Time.current.to_i}-#{(rand * 1_000).to_i}"
|
19
|
+
destination_shp = get_path_to_temp_file('shapefile', 'shp', file_id)
|
20
|
+
zip_file = get_path_to_temp_file('shapefile', 'zip', file_id)
|
21
|
+
|
22
|
+
run_command(%{gdal_polygonize.py #{@tif_path} -f "ESRI Shapefile" #{destination_shp}})
|
23
|
+
|
24
|
+
Zip::File.open(zip_file, Zip::File::CREATE) do |zip|
|
25
|
+
%w(dbf prj shp shx).each do |extension|
|
26
|
+
file_name = "shapefile-#{file_id}.#{extension}"
|
27
|
+
file_with_path = get_path_to_temp_file('shapefile', extension, file_id)
|
28
|
+
zip.add(file_name, file_with_path)
|
29
|
+
add_to_clean(file_with_path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
add_to_clean(zip_file)
|
34
|
+
zip_file
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::RasterService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
def initialize(file_input)
|
7
|
+
raise 'File does not exist' unless File.exist?(file_input)
|
8
|
+
|
9
|
+
@file = file_input
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(options)
|
13
|
+
out = get_path_to_temp_file(:raster, options[:format])
|
14
|
+
add_to_clean(out)
|
15
|
+
add_to_clean("#{out}.aux.xml")
|
16
|
+
|
17
|
+
format_out = if options[:format] == 'tif'
|
18
|
+
'GTiff'
|
19
|
+
else
|
20
|
+
options[:format].upcase
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
run_command(%{gdal_rasterize -q -of #{format_out} #{options_to_command_line(options, :format)} -l "#{get_layer_name}" "#{@file}" "#{out}"})
|
25
|
+
rescue RuntimeError => re
|
26
|
+
if re.message.include?('Failed to find field')
|
27
|
+
cpg_iso = "#{get_file_name_with_path(@file)}.cpg"
|
28
|
+
run_command(%{echo 885910 > "#{cpg_iso}"})
|
29
|
+
retry
|
30
|
+
elsif
|
31
|
+
raise re
|
32
|
+
end
|
33
|
+
end
|
34
|
+
out
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::RgbService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
attr_reader :merge
|
7
|
+
|
8
|
+
delegate :clean, to: :merge
|
9
|
+
|
10
|
+
def initialize(files)
|
11
|
+
@merge = Map::Gdal::MergeService.new(files)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
# http://www.processamentodigital.com.br/2013/11/23/qgis-2-0-composicao-colorida-rgb-para-imagens-landsat-8/
|
16
|
+
|
17
|
+
out = @merge.call([
|
18
|
+
'-separate',
|
19
|
+
'-co TFW=yes',
|
20
|
+
'-of GTiff'
|
21
|
+
])
|
22
|
+
|
23
|
+
add_to_clean(out)
|
24
|
+
|
25
|
+
out
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Exportar arquivo de mapa de cores gerado pela QGIS
|
2
|
+
#INTERPOLATION:INTERPOLATED
|
3
|
+
1,26,150,65,255,1.000000
|
4
|
+
1.00142,37,155,68,255,1.001421
|
5
|
+
1.00284,48,160,71,255,1.002843
|
6
|
+
1.00426,60,166,75,255,1.004264
|
7
|
+
1.00569,71,171,78,255,1.005686
|
8
|
+
1.00711,83,177,81,255,1.007107
|
9
|
+
1.00853,94,182,85,255,1.008529
|
10
|
+
1.00995,106,188,88,255,1.009950
|
11
|
+
1.01137,117,193,91,255,1.011371
|
12
|
+
1.01279,128,199,95,255,1.012793
|
13
|
+
1.01421,140,204,98,255,1.014214
|
14
|
+
1.01564,151,210,101,255,1.015636
|
15
|
+
1.01706,163,215,105,255,1.017057
|
16
|
+
1.01848,171,219,111,255,1.018479
|
17
|
+
1.0199,178,222,118,255,1.019900
|
18
|
+
1.02132,185,225,125,255,1.021321
|
19
|
+
1.02274,193,228,132,255,1.022743
|
20
|
+
1.02416,200,231,139,255,1.024164
|
21
|
+
1.02559,207,234,146,255,1.025586
|
22
|
+
1.02701,215,237,153,255,1.027007
|
23
|
+
1.02843,222,241,160,255,1.028429
|
24
|
+
1.02985,229,244,167,255,1.029850
|
25
|
+
1.03127,236,247,174,255,1.031271
|
26
|
+
1.03269,244,250,181,255,1.032693
|
27
|
+
1.03411,251,253,188,255,1.034114
|
28
|
+
1.03554,254,251,188,255,1.035536
|
29
|
+
1.03696,254,245,180,255,1.036957
|
30
|
+
1.03838,254,238,172,255,1.038379
|
31
|
+
1.0398,254,231,164,255,1.039800
|
32
|
+
1.04122,254,225,157,255,1.041221
|
33
|
+
1.04264,254,218,149,255,1.042643
|
34
|
+
1.04406,253,212,141,255,1.044064
|
35
|
+
1.04549,253,205,133,255,1.045486
|
36
|
+
1.04691,253,198,126,255,1.046907
|
37
|
+
1.04833,253,192,118,255,1.048329
|
38
|
+
1.04975,253,185,110,255,1.049750
|
39
|
+
1.05117,253,178,102,255,1.051171
|
40
|
+
1.05259,252,170,95,255,1.052593
|
41
|
+
1.05401,249,158,89,255,1.054014
|
42
|
+
1.05544,246,146,84,255,1.055436
|
43
|
+
1.05686,242,134,78,255,1.056857
|
44
|
+
1.05828,239,122,73,255,1.058279
|
45
|
+
1.0597,236,110,67,255,1.059700
|
46
|
+
1.06112,233,97,61,255,1.061121
|
47
|
+
1.06254,230,85,56,255,1.062543
|
48
|
+
1.06396,227,73,50,255,1.063964
|
49
|
+
1.06539,224,61,44,255,1.065386
|
50
|
+
1.06681,221,49,39,255,1.066807
|
51
|
+
1.06823,218,37,33,255,1.068229
|
52
|
+
1.06965,215,25,28,255,1.069650
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::TilesService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
DEFAULT_ZOOM_MAX = 15
|
7
|
+
|
8
|
+
def initialize(file)
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(options = {})
|
13
|
+
tiles_folder = options[:output] || File.join(Dir.tmpdir, "tiles-#{(Time.current.to_i * rand).to_i}")
|
14
|
+
|
15
|
+
run_command("gdal2tiles.py --profile=mercator -z #{options[:zoom_min].to_i}-#{options[:zoom_max] || DEFAULT_ZOOM_MAX} #{@file} #{tiles_folder}")
|
16
|
+
|
17
|
+
add_to_clean(tiles_folder)
|
18
|
+
|
19
|
+
tiles_folder
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::Gdal::TranslateService
|
4
|
+
include Map::Gdal::Base
|
5
|
+
|
6
|
+
def initialize(file)
|
7
|
+
@file = file
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_png(options = {})
|
11
|
+
self.call(options.merge({
|
12
|
+
of: 'PNG',
|
13
|
+
co: 'worldfile=yes',
|
14
|
+
scale: '',
|
15
|
+
ot: 'Byte'
|
16
|
+
}))
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_jpeg(options = {})
|
20
|
+
self.call(options.merge({
|
21
|
+
of: 'JPEG',
|
22
|
+
co: 'worldfile=yes'
|
23
|
+
}))
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(options = {})
|
27
|
+
out = get_path_to_temp_file(:translate, options[:of])
|
28
|
+
|
29
|
+
run_command("gdal_translate #{options_to_command_line(options)} #{@file} #{out}")
|
30
|
+
|
31
|
+
add_to_clean(out)
|
32
|
+
add_to_clean("#{out}.aux.xml")
|
33
|
+
add_to_clean(out.gsub('.jpg', '.wld'))
|
34
|
+
|
35
|
+
out
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# gdalwarp - image reprojection and warping utility
|
4
|
+
# http://www.gdal.org/gdalwarp.html
|
5
|
+
class Map::Gdal::WarpService
|
6
|
+
include Map::Gdal::Base
|
7
|
+
|
8
|
+
attr_reader :tif
|
9
|
+
def initialize(tif)
|
10
|
+
@tif = tif
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(options)
|
14
|
+
raise "Arquivo TIF inválido. [#{tif.inspect}]" unless tif && File.exist?(tif)
|
15
|
+
|
16
|
+
out = get_path_to_temp_file(:warp, :tif)
|
17
|
+
run_command(%{gdalwarp -q -overwrite #{options_to_command_line(options)} "#{tif}" "#{out}"})
|
18
|
+
|
19
|
+
add_to_clean(out)
|
20
|
+
out
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::GlebaTilesService
|
4
|
+
ZOOM_MIN = 1
|
5
|
+
ZOOM_MAX = 15
|
6
|
+
OUTPUT_DIR = 'public/map_tiles'
|
7
|
+
|
8
|
+
def initialize(gleba)
|
9
|
+
@gleba = gleba
|
10
|
+
end
|
11
|
+
|
12
|
+
def download_all_tiles
|
13
|
+
download_service.download
|
14
|
+
end
|
15
|
+
|
16
|
+
def download_service
|
17
|
+
@download_service ||= Map::DownloadTilesService.new({
|
18
|
+
boundary_ne: kml_service.point_more_north_east,
|
19
|
+
boundary_sw: kml_service.point_more_south_west,
|
20
|
+
zoom_min: ZOOM_MIN,
|
21
|
+
zoom_max: ZOOM_MAX,
|
22
|
+
output_directory: OUTPUT_DIR
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
def kml_service
|
27
|
+
@kml_service ||= Map::KmlService.new(@gleba.arquivo_kml)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::GpxService
|
4
|
+
include Map::FileImportBasic
|
5
|
+
|
6
|
+
def initialize(gpx)
|
7
|
+
@gpx = gpx
|
8
|
+
end
|
9
|
+
|
10
|
+
def coordinates
|
11
|
+
unless @coordinates
|
12
|
+
xml_doc = Nokogiri::XML(@gpx)
|
13
|
+
coordinates = xml_doc.xpath("//*[local-name()='trkseg']")
|
14
|
+
raise 'Arquivo inválido' if coordinates.empty?
|
15
|
+
@coordinates = coordinates.to_a.collect do |coordinate|
|
16
|
+
coordinate.children.to_a.collect do |point|
|
17
|
+
to_coordinate(point)
|
18
|
+
end.compact
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
@coordinates
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def to_coordinate(point)
|
28
|
+
[
|
29
|
+
point.attributes['lon'].value.to_f,
|
30
|
+
point.attributes['lat'].value.to_f
|
31
|
+
]
|
32
|
+
rescue
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::KmlCreatorLineService
|
4
|
+
def initialize
|
5
|
+
@lines = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_line(coordinates)
|
9
|
+
@lines << coordinates
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_xml
|
14
|
+
Nokogiri::XML::Builder.new do |xml|
|
15
|
+
xml.kml do
|
16
|
+
xml.Document do
|
17
|
+
xml.name 'KML_GENERATED'
|
18
|
+
@lines.each_with_index do |coordinates, index|
|
19
|
+
xml.Placemark do
|
20
|
+
xml.name "Line#{index}"
|
21
|
+
xml.LineString do
|
22
|
+
xml.tessellate 1
|
23
|
+
xml.coordinates coordinates.map{ |item| "#{item.second},#{item.first},0" }.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end.to_xml
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::KmlCreatorService
|
4
|
+
def add_polygon(polygon)
|
5
|
+
(@polygons ||= []) << polygon
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_xml
|
10
|
+
Nokogiri::XML::Builder.new do |xml|
|
11
|
+
xml.kml do
|
12
|
+
xml.Document do
|
13
|
+
xml.name 'KML_GENERATED'
|
14
|
+
@polygons.to_a.each_with_index do |polygon, index|
|
15
|
+
xml.Placemark do
|
16
|
+
xml.name "Polygon#{index}"
|
17
|
+
xml.Polygon do
|
18
|
+
xml.outerBoundaryIs do
|
19
|
+
xml.LinearRing do
|
20
|
+
xml.tessellate 1
|
21
|
+
xml.coordinates polygon.map{ |item| "#{item.second},#{item.first},0" }.join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end.to_xml
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::KmlEditService
|
4
|
+
def initialize(raw_kml)
|
5
|
+
@raw_kml = raw_kml
|
6
|
+
adjust_invalid_kml!
|
7
|
+
end
|
8
|
+
|
9
|
+
# add extended data do KML
|
10
|
+
# Based on https://developers.google.com/kml/documentation/extendeddata
|
11
|
+
def add_definition_custom_field(name, type)
|
12
|
+
schema = Nokogiri::XML::Node.new('Schema', nokogiri)
|
13
|
+
schema['name'] = name
|
14
|
+
schema['id'] = "#{name}Id"
|
15
|
+
simple_field = Nokogiri::XML::Node.new('SimpleField', nokogiri)
|
16
|
+
simple_field['type'] = type
|
17
|
+
simple_field['name'] = name
|
18
|
+
schema << simple_field
|
19
|
+
|
20
|
+
document << schema
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add new point to KML (Placemark)
|
26
|
+
def add_point(lat, lon, data = {})
|
27
|
+
index = document.css('Placemark').length + 1
|
28
|
+
placemark = Nokogiri::XML::Node.new('Placemark', nokogiri)
|
29
|
+
name = Nokogiri::XML::Node.new('name', nokogiri)
|
30
|
+
name.content = "Point #{index}"
|
31
|
+
|
32
|
+
# ExtendedData
|
33
|
+
data.each do |key, value|
|
34
|
+
add_extended_data(key, value, placemark)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Point
|
38
|
+
point = Nokogiri::XML::Node.new('Point', nokogiri)
|
39
|
+
coordinates = Nokogiri::XML::Node.new('coordinates', nokogiri)
|
40
|
+
coordinates.content = "#{lon},#{lat}"
|
41
|
+
point << coordinates
|
42
|
+
|
43
|
+
placemark << name
|
44
|
+
placemark << point
|
45
|
+
|
46
|
+
document_or_folder << placemark
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_default_value_to_placemarks(data)
|
52
|
+
data.each do |name, value|
|
53
|
+
check_definition!(name)
|
54
|
+
|
55
|
+
document.css('Placemark').each do |placemark|
|
56
|
+
add_extended_data(name, value, placemark)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_xml
|
64
|
+
nokogiri.to_xml
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove_node(name)
|
68
|
+
document.css(name.to_s).each(&:remove)
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def nokogiri
|
76
|
+
@nokogiri ||= Nokogiri::XML(@raw_kml)
|
77
|
+
end
|
78
|
+
|
79
|
+
def document
|
80
|
+
@document ||= nokogiri.at_css('Document')
|
81
|
+
end
|
82
|
+
|
83
|
+
def document_or_folder
|
84
|
+
@folder ||= nokogiri.at_css('Folder') || document
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_definition!(name)
|
88
|
+
has_definition = document.css("Schema SimpleField[name=#{name}]").any?
|
89
|
+
raise "#{name} is not defined" unless has_definition
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_extended_data(name, value, node)
|
93
|
+
check_definition!(name)
|
94
|
+
|
95
|
+
extendedData = Nokogiri::XML::Node.new('ExtendedData', nokogiri)
|
96
|
+
schemaData = Nokogiri::XML::Node.new('SchemaData', nokogiri)
|
97
|
+
schemaData['schemaUrl'] = "##{name}Id"
|
98
|
+
extendedData << schemaData
|
99
|
+
|
100
|
+
simpleData = Nokogiri::XML::Node.new('SimpleData', nokogiri)
|
101
|
+
simpleData['name'] = name
|
102
|
+
simpleData.content = value
|
103
|
+
schemaData << simpleData
|
104
|
+
|
105
|
+
node << extendedData
|
106
|
+
end
|
107
|
+
|
108
|
+
def adjust_invalid_kml!
|
109
|
+
# Existem alguns KMLs (inválidos) que o nodo root é KML e não existe um document dentro
|
110
|
+
# Nestes casos é necessário trocar o KML por Document
|
111
|
+
unless nokogiri.at_css('Document')
|
112
|
+
@raw_kml = @raw_kml.gsub('<kml', '<Document').gsub('</kml', '</Document')
|
113
|
+
@nokogiri = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# Gera um nome randomico para a layer
|
117
|
+
if document_or_folder
|
118
|
+
node_name = document_or_folder.at_css('name')
|
119
|
+
node_name.content = "layer_name_#{Time.current.to_i}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::KmlOffsetService
|
4
|
+
MINIMUM_POINTS = 3
|
5
|
+
|
6
|
+
attr_reader :kml
|
7
|
+
|
8
|
+
def initialize(kml)
|
9
|
+
@kml = kml
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(offset)
|
13
|
+
polygons = coordinates.flat_map do |point|
|
14
|
+
call_python(point.map(&:reverse), offset) if point.length > MINIMUM_POINTS
|
15
|
+
end.compact
|
16
|
+
|
17
|
+
generate_kml(polygons)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def coordinates
|
23
|
+
@coordinates ||= Map::KmlService.new(kml).coordinates
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_kml(polygons)
|
27
|
+
service = Map::KmlCreatorService.new
|
28
|
+
polygons.each { |polygon| service.add_polygon(polygon) }
|
29
|
+
service.to_xml
|
30
|
+
end
|
31
|
+
|
32
|
+
# Este método gera um programa em python para depois rodar
|
33
|
+
# Foi a forma encontrada para poder usar a lib pyclipper
|
34
|
+
# https://pypi.python.org/pypi/pyclipper
|
35
|
+
def call_python(points, offset)
|
36
|
+
str = <<-EOF.strip_heredoc
|
37
|
+
import pyclipper
|
38
|
+
|
39
|
+
subj = (#{points.map { |point| "(#{point.first.to_f}, #{point.second.to_f})" }.join(',')})
|
40
|
+
|
41
|
+
pco = pyclipper.PyclipperOffset()
|
42
|
+
pco.AddPath(pyclipper.scale_to_clipper(subj), pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
|
43
|
+
|
44
|
+
solution = pco.Execute(#{offset})
|
45
|
+
|
46
|
+
print(pyclipper.scale_from_clipper(solution))
|
47
|
+
EOF
|
48
|
+
|
49
|
+
file = "/tmp/offset-#{Time.current.to_i}-#{(rand * 1_000).to_i}"
|
50
|
+
IO.write(file, str)
|
51
|
+
command = "python3 #{file}"
|
52
|
+
result = `#{command} 2>&1`
|
53
|
+
raise "Falha ao rodar comando [$ #{command}]\n[#{result}]" unless $?.success?
|
54
|
+
|
55
|
+
FileUtils.rm_rf(file)
|
56
|
+
|
57
|
+
JSON.parse(result)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Map::KmlService
|
4
|
+
include Map::FileImportBasic
|
5
|
+
|
6
|
+
def initialize(kml)
|
7
|
+
@kml = kml
|
8
|
+
end
|
9
|
+
|
10
|
+
def coordinates
|
11
|
+
unless @coordinates
|
12
|
+
xml_doc =
|
13
|
+
if @kml.is_a? Nokogiri::XML::Element
|
14
|
+
@kml
|
15
|
+
else
|
16
|
+
Nokogiri::XML(@kml)
|
17
|
+
end
|
18
|
+
coordinates = xml_doc.xpath(".//*[local-name()='coordinates']")
|
19
|
+
|
20
|
+
@coordinates = coordinates.to_a.collect do |coordinate|
|
21
|
+
coordinate.text.split(' ').collect do |polygon|
|
22
|
+
string_to_coordinate(polygon)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@coordinates
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def string_to_coordinate(str)
|
33
|
+
str.split(',')[0..1].map(&:to_f)
|
34
|
+
end
|
35
|
+
end
|