osmer 0.0.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.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Alexey Noskov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,87 @@
1
+ = Osmer
2
+
3
+ Osmer is a tool to manage local OSM database: import data, transform it to convenient scheme and update it.
4
+
5
+ == Features
6
+
7
+ * Importing and updating data in osm2pgsql schema (osm2pgsql required)
8
+ * Custom derived schemas for analysis or rendering convenience
9
+ * BBox restriction of imported data
10
+ * Data reprojection between schemas
11
+ * [planned] Rails-like config/database.yml support for seamless integration with webapps
12
+ * [planned] Importing and updating data in pgsnapshot schema (osmosis required)
13
+
14
+ == Configuration
15
+
16
+ Database schemas definition goes to Osmfile which contains basic flags and schema definitions:
17
+
18
+ prefix 'osmer' # Prefix for all tables
19
+
20
+ schema :source, :type => :osm2pgsql do # Osm2pgsql schema named 'source'
21
+
22
+ bbox 35.859375, 54.367759, 36.562495, 54.775345 # It has bounding box restriction as minlon, minlat, maxlon, maxlat
23
+
24
+ updates :daily do # It may be updated on daily basis
25
+ dump 'http://data.gis-lab.info/osm_dump/dump/RU-KLU/RU-KLU-{today}.osm.pbf' # Where initial dump should be downloaded from here
26
+ diff 'http://data.gis-lab.info/osm_dump/diff/RU/RU-{yesterday}-{today}.osc.gz' # And diff should be downloaded from here
27
+ end
28
+
29
+ end
30
+
31
+ schema :rendering, :projection => 900913 do # Rendering schema containing only necessary data
32
+
33
+ multipolygons :places do # Table of place boundaries
34
+ map :place => [:city, :town, :village, :hamlet] # It contains features which have place tag with one of values: city, town, village, hamlet
35
+
36
+ with :area # And also polygon area
37
+ end
38
+
39
+ lines :roads do # Table of roads
40
+ map :highway # It contains features with highway tag
41
+
42
+ with :length, :ref => :string # And also road length and ref tag to additional table column
43
+ end
44
+
45
+ end
46
+
47
+ == Usage
48
+
49
+ Osmer may be used as command-line executable with following commands:
50
+
51
+ * osmer schema create [SCHEMA] # Create given osm schema in database (create all if none specified)
52
+ * osmer schema drop [SCHEMA] # Drop given osm schema in database (drop all if none specified)
53
+ * osmer schema recreate [SCHEMA] # Recreate given osm schema in database (recreate all if none specified)
54
+
55
+ * osmer data import [SCHEMA] [FILE] # Import data to given schema from file
56
+ * osmer data update [SCHEMA] [FILE] # Update data in schema from given change file
57
+
58
+ == Details
59
+
60
+ === Schema
61
+
62
+ Schema is a set of tables representing osm data in a common way. Examples of schemas may be:
63
+
64
+ * Osm2Pgsql schema
65
+ * PgSnapshot schema populated with Osmosis
66
+ * Custom set of tables, representing data in convenient way
67
+
68
+ All tables in schema share common projection.
69
+ In database schemas are represented with table prefixes (native schemas will be supported later)
70
+
71
+ In code, schema is representd by an object, which have following methods:
72
+
73
+ * create! - to create schema in database
74
+ * drop! - to drop schema in database
75
+ * attach_listener! - (optional) - to add listener to schema data, which may be used to have autoupdate functionality
76
+
77
+ listener with name <name> is a stored procedure set:
78
+
79
+ <name>_insert(id,*args)
80
+ <name>_update(id,*args)
81
+ <name>_delete(id)
82
+
83
+ == Contributors
84
+
85
+ See https://github.com/alno/osmer/graphs/contributors
86
+
87
+ Copyright © 2012 Alexey Noskov, released under the MIT license
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ require 'osmer/app'
6
+
7
+ Osmer::App.start
@@ -0,0 +1,2 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <osm version="0.6" generator="No gen" />
@@ -0,0 +1,92 @@
1
+ class Osmer
2
+
3
+ module Data; end
4
+ module Mapper; end
5
+ module Schema; end
6
+ module Target; end
7
+
8
+ module Utils
9
+
10
+ # Based on https://github.com/intridea/omniauth/blob/v1.0.2/lib/omniauth.rb#L129-139
11
+ def camelize(str)
12
+ str.to_s.gsub(/\/(.?)/){ "::" + $1.upcase }.gsub(/(^|_)(.)/){ $2.upcase }
13
+ end
14
+
15
+ def underscore(str)
16
+ str.to_s.gsub(/::(.?)/){ "/" + $1.downcase }.gsub(/(.)([A-Z])/){ "#{$1}_#{$2.downcase}" }.downcase
17
+ end
18
+
19
+ end
20
+
21
+ module Configurable
22
+
23
+ def configure(file = nil, &block)
24
+ if file
25
+ dsl.instance_eval{ eval File.read(file) }
26
+ elsif block
27
+ dsl.instance_eval(&block)
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ private
34
+
35
+ def dsl
36
+ self.class.const_get('Dsl').new(self)
37
+ end
38
+
39
+ end
40
+
41
+ include Utils
42
+ include Configurable
43
+
44
+ attr_reader :schemas
45
+ attr_accessor :prefix
46
+
47
+ def initialize
48
+ @schemas = []
49
+ end
50
+
51
+ def add_schema(name, options)
52
+ type = options.delete(:type) || 'custom'
53
+
54
+ require "osmer/schema/#{type}"
55
+
56
+ schema = Osmer::Schema.const_get(camelize type).new self, name, options
57
+ schemas << schema
58
+ schema
59
+ end
60
+
61
+ def find_schema(name)
62
+ schemas.find{|s| s.name.to_s == name.to_s }
63
+ end
64
+
65
+ def create_all!(db)
66
+ @schemas.each{|s| s.create! db }
67
+ end
68
+
69
+ def drop_all!(db)
70
+ @schemas.reverse.each{|s| s.drop! db }
71
+ end
72
+
73
+ def recreate_all!(db)
74
+ drop_all! db
75
+ create_all! db
76
+ end
77
+
78
+ class Dsl < Struct.new(:osmer)
79
+
80
+ def schema(name, options, &block)
81
+ schema = osmer.add_schema name, options
82
+ schema.configure &block if block
83
+ schema
84
+ end
85
+
86
+ def prefix(value)
87
+ osmer.prefix = value
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,12 @@
1
+ require 'osmer/thor_base'
2
+ require 'osmer/data/app'
3
+ require 'osmer/schema/app'
4
+
5
+ require 'thor/group'
6
+
7
+ class Osmer::App < Osmer::ThorBase
8
+
9
+ register Osmer::Data::App, 'data', 'data <command>', 'OSM data management'
10
+ register Osmer::Schema::App, 'schema', 'schema <command>', 'OSM schema management'
11
+
12
+ end
@@ -0,0 +1,16 @@
1
+ require 'osmer/thor_base'
2
+
3
+ class Osmer::Data::App < Osmer::ThorBase
4
+ namespace :data
5
+
6
+ desc "import SCHEMA [FILE]", "Import data to given schema from file"
7
+ def import(schema, file = nil)
8
+ osmer.find_schema(schema).import_data! db, file
9
+ end
10
+
11
+ desc "update SCHEMA [FILE]", "Update data in schema from given change file"
12
+ def update(schema, file = nil)
13
+ osmer.find_schema(schema).update_data! db, file
14
+ end
15
+
16
+ end
@@ -0,0 +1,19 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::Address < Osmer::Mapper::Base
4
+
5
+ def assigns
6
+ { :street => "src_tags->'addr:street'",
7
+ :housenumber => "src_tags->'addr:housenumber'",
8
+ :city => "src_tags->'addr:city'",
9
+ :postcode => "src_tags->'addr:postcode'" }
10
+ end
11
+
12
+ def fields
13
+ { :street => "TEXT",
14
+ :housenumber => 'TEXT',
15
+ :city => "TEXT",
16
+ :postcode => "TEXT" }
17
+ end
18
+
19
+ end
@@ -0,0 +1,13 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::Area < Osmer::Mapper::Base
4
+
5
+ def assigns
6
+ { :area => "ST_Area(ST_Transform(src_geometry,#{table.projection}))" }
7
+ end
8
+
9
+ def fields
10
+ { :area => "REAL" }
11
+ end
12
+
13
+ end
@@ -0,0 +1,28 @@
1
+ require 'osmer'
2
+
3
+ class Osmer::Mapper::Base
4
+
5
+ attr_reader :table, :name
6
+
7
+ def initialize(table, name, options = {})
8
+ @table = table
9
+ @name = name
10
+ end
11
+
12
+ def fields
13
+ { name => "TEXT" }
14
+ end
15
+
16
+ def assigns
17
+ { name => "src_tags->'#{name}'" }
18
+ end
19
+
20
+ def conditions
21
+ []
22
+ end
23
+
24
+ def indexes
25
+ {}
26
+ end
27
+
28
+ end
@@ -0,0 +1,13 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::Length < Osmer::Mapper::Base
4
+
5
+ def assigns
6
+ { :area => "ST_Length(ST_Transform(src_geometry,#{table.projection}))" }
7
+ end
8
+
9
+ def fields
10
+ { :area => "REAL" }
11
+ end
12
+
13
+ end
@@ -0,0 +1,4 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::Name < Osmer::Mapper::Base
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::String < Osmer::Mapper::Base
4
+ end
@@ -0,0 +1,91 @@
1
+ require 'osmer/mapper/base'
2
+
3
+ class Osmer::Mapper::Type < Osmer::Mapper::Base
4
+
5
+ attr_reader :mappings
6
+
7
+ def initialize(*args)
8
+ super
9
+
10
+ @mappings = []
11
+ @multi = false
12
+ end
13
+
14
+ def assigns
15
+ if @multi
16
+ { :type => expression, :types => expression_multi }
17
+ else
18
+ { :type => expression }
19
+ end
20
+ end
21
+
22
+ def fields
23
+ if @multi
24
+ { :type => 'TEXT NOT NULL', :types => 'TEXT[]' }
25
+ else
26
+ { :type => 'TEXT NOT NULL' }
27
+ end
28
+ end
29
+
30
+ def indexes
31
+ if @multi
32
+ { :type => "BTREE(type)", :types => "GIN(types)" }
33
+ else
34
+ { :type => "BTREE(type)" }
35
+ end
36
+ end
37
+
38
+ def conditions
39
+ [ @mappings.map{|m| map_cond(m)}.join(' OR ') ]
40
+ end
41
+
42
+ def expression
43
+ "CASE #{@mappings.map{|m| "WHEN #{map_cond(m)} THEN trim(regexp_replace(src_tags->'#{m[0]}', ',.*', ''))"}.join(' ')} END"
44
+ end
45
+
46
+ def expression_multi
47
+ "(#{@mappings.map{|m| "string_to_array(coalesce(CASE WHEN #{map_cond(m)} THEN replace(src_tags->'#{m[0]}',' ','') END,''),',')"}.join(' || ')})"
48
+ end
49
+
50
+ def map_cond(m)
51
+ m[1] ? "(src_tags->'#{m[0]}') IN ('#{m[1].join("','")}')" : "(src_tags->'#{m[0]}') IS NOT NULL"
52
+ end
53
+
54
+ def add_args(*args)
55
+ args.each do |arg|
56
+ if arg.is_a? Hash
57
+ arg.each(&method(:add_arg))
58
+ else
59
+ add_arg arg, nil
60
+ end
61
+ end
62
+ end
63
+
64
+ def add_arg(key, value)
65
+ key = to_key(key)
66
+
67
+ if key == 'multi'
68
+ @multi = value
69
+ else
70
+ @mappings << [key, to_keylist(value)]
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def to_key(key)
77
+ raise StandardError.new("Invalid key #{key.inspect}") unless key.is_a? String or key.is_a? Symbol
78
+ key.to_s
79
+ end
80
+
81
+ def to_keylist(list)
82
+ if list.nil?
83
+ list
84
+ elsif list.is_a? Array
85
+ list.map(&method(:to_key))
86
+ else
87
+ [to_key(list)]
88
+ end
89
+ end
90
+
91
+ end
@@ -0,0 +1,33 @@
1
+ require 'osmer/thor_base'
2
+
3
+ class Osmer::Schema::App < Osmer::ThorBase
4
+ namespace :schema
5
+
6
+ desc "create [SCHEMA]", "Create given osm schema in database (create all if none specified)"
7
+ def create(schema = nil)
8
+ if schema
9
+ osmer.find_schema(schema).create! db
10
+ else
11
+ osmer.create_all! db
12
+ end
13
+ end
14
+
15
+ desc "recreate [SCHEMA]", "Recreate given osm schema in database (recreate all if none specified)"
16
+ def recreate(schema = nil)
17
+ if schema
18
+ osmer.find_schema(schema).recreate! db
19
+ else
20
+ osmer.recreate_all! db
21
+ end
22
+ end
23
+
24
+ desc "drop [SCHEMA]", "Drop given osm schema in database (drop all if none specified)"
25
+ def drop(schema = nil)
26
+ if schema
27
+ osmer.find_schema(schema).drop! db
28
+ else
29
+ osmer.drop_all! db
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,68 @@
1
+ require 'osmer'
2
+
3
+ class Osmer::Schema::Base
4
+
5
+ include Osmer::Configurable
6
+
7
+ attr_reader :ns, :name
8
+ attr_accessor :projection
9
+
10
+ def initialize(ns, name, options = {})
11
+ @ns = ns
12
+ @name = name
13
+ @projection = 4326
14
+
15
+ options.each do |k,v| # Assign all schema options
16
+ send "#{k}=", v
17
+ end
18
+ end
19
+
20
+ # Create schema in given database
21
+ def create!(db)
22
+ raise StandardError.new("Not implemented")
23
+ end
24
+
25
+ # Drop schema in given database
26
+ def drop!(db)
27
+ raise StandardError.new("Not implemented")
28
+ end
29
+
30
+ def recreate!(db)
31
+ drop! db
32
+ create! db
33
+ end
34
+
35
+ # Add collection listener
36
+ # name - stored procedure name
37
+ #
38
+ def attach_listener!(conn, collection, name, fields)
39
+ raise StandardError.new("Not implemented")
40
+ end
41
+
42
+ def detach_listener!(conn, collection, name, fields)
43
+ raise StandardError.new("Not implemented")
44
+ end
45
+
46
+ def table_prefix
47
+ [@ns.prefix, name].compact.join('_')
48
+ end
49
+
50
+ private
51
+
52
+ class Dsl < Struct.new(:schema)
53
+
54
+ def method_missing(method, *args)
55
+ if schema.respond_to?("#{method}=") && !args.empty?
56
+ if args.size > 1
57
+ schema.send "#{method}=", args
58
+ else
59
+ schema.send "#{method}=", args.first
60
+ end
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,198 @@
1
+ require 'osmer/schema/base'
2
+
3
+ class Osmer::Schema::Custom < Osmer::Schema::Base
4
+
5
+ attr_reader :tables
6
+ attr_accessor :source
7
+
8
+ def initialize(*args)
9
+ @tables = []
10
+ super
11
+ end
12
+
13
+ def dsl
14
+ Dsl.new(self)
15
+ end
16
+
17
+ def create!(db)
18
+ db.in_transaction do |conn|
19
+ tables.each do |table|
20
+ create_table! db, conn, table
21
+ end
22
+ end
23
+ end
24
+
25
+ def drop!(db)
26
+ db.in_transaction do |conn|
27
+ tables.reverse.each do |table|
28
+ drop_table! db, conn, table
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def create_table!(db, conn, table)
36
+ table_name = "#{table_prefix}_#{table.name}"
37
+ table_fields = { :id => 'INT8', :tags => 'HSTORE' }
38
+ table_assigns = { :tags => 'src_tags' }
39
+ table_conditions = []
40
+ table_indexes = { :geometry => 'GIST(geometry)' }
41
+
42
+ if table.type.to_s.start_with? 'multi'
43
+ table_assigns[:geometry] = "ST_Transform(ST_Multi(src_geometry), #{projection})"
44
+ else
45
+ table_assigns[:geometry] = "ST_Transform(src_geometry, #{projection})"
46
+ end
47
+
48
+ table.mappers.each do |k,v|
49
+ table_fields.merge! v.fields
50
+ table_assigns.merge! v.assigns
51
+ table_indexes.merge! v.indexes
52
+ table_conditions |= v.conditions
53
+ end
54
+
55
+ table_assigns_keys = table_assigns.keys.to_a
56
+ table_assigns_values = table_assigns_keys.map{|k| table_assigns[k] }
57
+ table_condition = table_conditions.map{|c| "(#{c})"}.join(' AND ')
58
+
59
+ conn.exec "CREATE TABLE #{table_name}(#{table_fields.map{|k,v| "#{k} #{v}"}.join(', ')})"
60
+ conn.exec "SELECT AddGeometryColumn('#{table_name}', 'geometry', #{projection}, '#{db.geometry_type table.type}', 2)"
61
+
62
+ table_indexes.each do |key,desc|
63
+ conn.exec "CREATE INDEX #{table_name}_#{key}_index ON #{table_name} USING #{desc}"
64
+ end
65
+
66
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{table_name}_insert(src_id BIGINT, src_tags HSTORE, src_geometry GEOMETRY) RETURNS BOOLEAN AS $$
67
+ BEGIN
68
+ IF #{table_condition} THEN
69
+ INSERT INTO #{table_name} (id, #{table_assigns_keys.join(', ')}) VALUES (src_id, #{table_assigns_values.join(', ')});
70
+ RETURN FOUND;
71
+ ELSE
72
+ RETURN FALSE;
73
+ END IF;
74
+ END; $$ LANGUAGE plpgsql;}
75
+
76
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{table_name}_update(src_id BIGINT, src_tags HSTORE, src_geometry GEOMETRY) RETURNS BOOLEAN AS $$
77
+ BEGIN
78
+ IF #{table_condition} THEN
79
+ UPDATE #{table_name} SET #{table_assigns.map{|k,v| "#{k} = #{v}"}.join(', ')} WHERE id = src_id;
80
+
81
+ IF NOT FOUND THEN
82
+ INSERT INTO #{table_name} (id, #{table_assigns_keys.join(', ')}) VALUES (src_id, #{table_assigns_values.join(', ')});
83
+ END IF;
84
+
85
+ RETURN FOUND;
86
+ ELSE
87
+ DELETE FROM #{table_name} WHERE id = src_id;
88
+ RETURN FOUND;
89
+ END IF;
90
+ END; $$ LANGUAGE plpgsql;}
91
+
92
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{table_name}_delete(src_id BIGINT) RETURNS BOOLEAN AS $$
93
+ BEGIN
94
+ DELETE FROM #{table_name} WHERE id = src_id;
95
+ RETURN FOUND;
96
+ END; $$ LANGUAGE plpgsql;}
97
+
98
+ table.source_schema.attach_listener! conn, table.source_table, table_name, listener_fields
99
+
100
+ conn.exec "ANALYZE #{table_name}"
101
+ end
102
+
103
+ def drop_table!(db, conn, table)
104
+ table_name = "#{table_prefix}_#{table.name}"
105
+
106
+ table.source_schema.detach_listener! conn, table.source_table, table_name, listener_fields
107
+
108
+ conn.exec "DROP TABLE IF EXISTS #{table_name}"
109
+ end
110
+
111
+ def listener_fields
112
+ [:tags, :geometry]
113
+ end
114
+
115
+ class Dsl < Osmer::Schema::Base::Dsl
116
+
117
+ def multipolygons(name, options = {}, &block)
118
+ table name, :multipolygons, options, &block
119
+ end
120
+
121
+ def multilines(name, options = {}, &block)
122
+ table name, :multilines, options, &block
123
+ end
124
+
125
+ def polygons(name, options = {}, &block)
126
+ table name, :polygons, options, &block
127
+ end
128
+
129
+ def lines(name, options = {}, &block)
130
+ table name, :lines, options, &block
131
+ end
132
+
133
+ def points(name, options = {}, &block)
134
+ table name, :points, options, &block
135
+ end
136
+
137
+ def table(name, type, options, &block)
138
+ schema.tables << Table.new(schema, name, type, options).configure(&block)
139
+ end
140
+
141
+ end
142
+
143
+ class Table
144
+
145
+ include Osmer::Configurable
146
+
147
+ attr_reader :schema, :name, :type, :source_schema, :source_table, :mappers
148
+
149
+ def initialize(schema, name, type, options)
150
+ require 'osmer/mapper/type'
151
+ require 'osmer/mapper/name'
152
+
153
+ @schema, @name, @type = schema, name, type
154
+
155
+ @source_table = type.to_s.gsub(/\Amulti/,'')
156
+ @source_schema = schema.ns.find_schema(options[:source] || schema.source || :source) or raise StandardError.new("Source schema not found")
157
+
158
+ @mappers = {
159
+ :type => Osmer::Mapper::Type.new(self, :type),
160
+ :name => Osmer::Mapper::Name.new(self, :name)
161
+ }
162
+ end
163
+
164
+ def projection
165
+ schema.projection
166
+ end
167
+
168
+ class Dsl < Struct.new(:table)
169
+
170
+ include Osmer::Utils
171
+
172
+ def map(*args)
173
+ table.mappers[:type].add_args(*args)
174
+ end
175
+
176
+ def with(*args)
177
+ args.each do |arg|
178
+ if arg.is_a? Hash
179
+ arg.each(&method(:add_mapper))
180
+ else
181
+ add_mapper arg, arg
182
+ end
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ def add_mapper(key, type)
189
+ require "osmer/mapper/#{type}"
190
+
191
+ table.mappers[key.to_s] = Osmer::Mapper.const_get(camelize type).new table, key
192
+ end
193
+
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -0,0 +1,154 @@
1
+ require 'osmer/schema/base'
2
+
3
+ class Osmer::Schema::Osm2pgsql < Osmer::Schema::Base
4
+
5
+ attr_accessor :osm2pgsql_binary, :bbox, :updater
6
+
7
+ def initialize(*args)
8
+ @osm2pgsql_binary = 'osm2pgsql'
9
+ super
10
+ end
11
+
12
+ # Create schema in given database
13
+ def create!(db)
14
+ db.in_transaction do |conn|
15
+ raise StandardError.new("Schema #{name} already created!") unless schema_tables(conn).empty?
16
+ end
17
+
18
+ osm2pgsql_exec db, "'#{empty_file}'", "creating osm2pgsql schema"
19
+ end
20
+
21
+ # Drop schema in given database
22
+ def drop!(db)
23
+ db.in_transaction do |conn|
24
+ schema_tables(conn).each do |table|
25
+ conn.exec "DROP TABLE IF EXISTS #{table}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def import_data!(db, file = nil)
31
+ raise StandardError.new("No file or updater specified") unless file or updater
32
+
33
+ db.in_transaction do |conn|
34
+ schema_tables(conn).each do |table|
35
+ conn.exec "DELETE FROM #{table}"
36
+ end
37
+ end
38
+
39
+ if file
40
+ osm2pgsql_exec db, "-a '#{file}'", "importing data with osm2pgsql from #{file}"
41
+ else
42
+ updater.load_dump db, self do |f|
43
+ osm2pgsql_exec db, "-a '#{f}'", "importing data with osm2pgsql from #{f}"
44
+ end
45
+ end
46
+ end
47
+
48
+ def update_data!(db, file = nil)
49
+ raise StandardError.new("No file or updater specified") unless file or updater
50
+
51
+ if file
52
+ osm2pgsql_exec db, "-a '#{file}'", "importing data with osm2pgsql from #{file}"
53
+ else
54
+ updater.load_diffs db, self do |f|
55
+ osm2pgsql_exec db, "-a '#{f}'", "importing data with osm2pgsql from #{f}"
56
+ end
57
+ end
58
+ end
59
+
60
+ def attach_listener!(conn, collection, name, fields)
61
+ table = collection_table collection
62
+ args = fields.map{|f| collection_field(collection, f) }.join(', ')
63
+
64
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{name}_insert_proxy() RETURNS trigger AS $$
65
+ BEGIN
66
+ PERFORM #{name}_insert(NEW.osm_id, #{args});
67
+ RETURN NULL;
68
+ END; $$ LANGUAGE plpgsql}
69
+
70
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{name}_update_proxy() RETURNS trigger AS $$
71
+ BEGIN
72
+ PERFORM #{name}_update(NEW.osm_id, #{args});
73
+ RETURN NULL;
74
+ END; $$ LANGUAGE plpgsql}
75
+
76
+ conn.exec %Q{CREATE OR REPLACE FUNCTION #{name}_delete_proxy() RETURNS trigger AS $$
77
+ BEGIN
78
+ PERFORM #{name}_delete(OLD.osm_id);
79
+ RETURN NULL;
80
+ END; $$ LANGUAGE plpgsql}
81
+
82
+ # Create new triggers
83
+ conn.exec "CREATE TRIGGER #{name}_insert_trigger AFTER INSERT ON #{table} FOR EACH ROW EXECUTE PROCEDURE #{name}_insert_proxy()"
84
+ conn.exec "CREATE TRIGGER #{name}_update_trigger AFTER UPDATE ON #{table} FOR EACH ROW EXECUTE PROCEDURE #{name}_update_proxy()"
85
+ conn.exec "CREATE TRIGGER #{name}_delete_trigger AFTER DELETE ON #{table} FOR EACH ROW EXECUTE PROCEDURE #{name}_delete_proxy()"
86
+
87
+ # Prepopulate dependency
88
+ conn.exec "SELECT #{name}_insert(NEW.osm_id, #{args}) FROM #{table} NEW"
89
+ end
90
+
91
+ def detach_listener!(conn, collection, name, fields)
92
+ table = collection_table collection
93
+
94
+ # Drop triggers
95
+ conn.exec "DROP TRIGGER IF EXISTS #{name}_insert_trigger ON #{table}"
96
+ conn.exec "DROP TRIGGER IF EXISTS #{name}_update_trigger ON #{table}"
97
+ conn.exec "DROP TRIGGER IF EXISTS #{name}_delete_trigger ON #{table}"
98
+ end
99
+
100
+ private
101
+
102
+ def osm2pgsql_exec(db, tail, desc)
103
+ cmd = "'#{osm2pgsql_binary}' -j -m -G --slim -U #{db[:username]} -d #{db[:database]} -H #{db[:host]} -p '#{table_prefix}'"
104
+ cmd << " --bbox #{bbox.join(',')}" if bbox # Restrict import if bbox specified
105
+
106
+ case projection.to_i
107
+ when 4326 then cmd << ' --latlong'
108
+ when 900913 then cmd << ' --merc'
109
+ else raise StandardError.new("Unsupported projection #{projection}")
110
+ end
111
+
112
+ puts "#{cmd} #{tail}"
113
+ system "#{cmd} #{tail}" or raise StandardError.new("Error #{desc}")
114
+ end
115
+
116
+ def schema_tables(conn)
117
+ res = conn.exec "select table_name from information_schema.tables where table_schema = 'public' AND table_name LIKE '#{table_prefix}_%'"
118
+ tables = res.values.flatten
119
+ res.clear
120
+ tables
121
+ end
122
+
123
+ def collection_table(collection)
124
+ case collection.to_s
125
+ when 'polygons' then "#{table_prefix}_polygon"
126
+ when 'lines' then "#{table_prefix}_line"
127
+ when 'points' then "#{table_prefix}_point"
128
+ else raise StandardError.new("Unsupported collection '#{collection}'")
129
+ end
130
+ end
131
+
132
+ def collection_field(collection, field)
133
+ case field.to_s
134
+ when 'geometry' then 'NEW.way'
135
+ when 'tags' then 'NEW.tags'
136
+ else raise StandardError.new("Unsupported field '#{field}' in collection '#{collection}'")
137
+ end
138
+ end
139
+
140
+ def empty_file
141
+ File.expand_path("../../../../data/empty.osm", __FILE__)
142
+ end
143
+
144
+ class Dsl < Osmer::Schema::Base::Dsl
145
+
146
+ def updates(interval = nil, &block)
147
+ require 'osmer/updater'
148
+
149
+ schema.updater = Osmer::Updater.new(interval).configure(&block)
150
+ end
151
+
152
+ end
153
+
154
+ end
@@ -0,0 +1,61 @@
1
+ require 'osmer'
2
+
3
+ require 'pg'
4
+
5
+ class Osmer::Target::Pg
6
+
7
+ attr_accessor :options
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def [](key)
14
+ @options[key]
15
+ end
16
+
17
+ def in_transaction
18
+ conn = Connection.new connection_options
19
+
20
+ begin
21
+ conn.exec "BEGIN TRANSACTION"
22
+
23
+ yield conn
24
+
25
+ conn.exec "COMMIT"
26
+ ensure
27
+ conn.close
28
+ end
29
+ end
30
+
31
+ GEOTYPES = { 'point' => 'POINT', 'line' => 'LINESTRING', 'polygon' => 'POLYGON', 'multiline' => 'MULTILINESTRING', 'multipolygon' => 'MULTIPOLYGON' }
32
+
33
+ def geometry_type(type)
34
+ GEOTYPES[type.to_s.gsub(/s\z/,'')] or raise StandardError.new("Unknown geometry type #{type.inspect}")
35
+ end
36
+
37
+ private
38
+
39
+ def connection_options
40
+ { :host => options[:host], :user => options[:username], :password => options[:password], :dbname => options[:database] }
41
+ end
42
+
43
+ class Connection < Struct.new(:options)
44
+
45
+ def initialize(*args)
46
+ super
47
+ @conn = PG.connect options
48
+ end
49
+
50
+ def exec(q, *args)
51
+ puts " #{q}"
52
+ @conn.exec q, *args
53
+ end
54
+
55
+ def close
56
+ @conn.close
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,30 @@
1
+ require 'osmer'
2
+ require 'thor'
3
+
4
+ class Osmer::ThorBase < Thor
5
+
6
+ class_option :config, :aliases => '-c', :default => 'Osmfile', :desc => 'Config file location'
7
+
8
+ class_option :dbhost, :aliases => '-h', :default => 'localhost', :desc => 'Database host'
9
+ class_option :dbport, :aliases => '-p', :default => '5432', :desc => 'Database port'
10
+ class_option :dbuser, :aliases => '-U', :desc => 'Database user'
11
+ class_option :dbpass, :aliases => '-P', :desc => 'Database password'
12
+ class_option :dbname, :aliases => '-d', :desc => 'Database name'
13
+
14
+ private
15
+
16
+ def app_options
17
+ @app_options = parent_options.merge(options)
18
+ end
19
+
20
+ def db
21
+ require 'osmer/target/pg'
22
+
23
+ @db ||= Osmer::Target::Pg.new :username => app_options[:dbuser], :password => app_options[:dbpass], :host => app_options[:dbhost], :port => app_options[:dbport], :database => app_options[:dbname]
24
+ end
25
+
26
+ def osmer
27
+ @osmer ||= Osmer.new.configure app_options[:config]
28
+ end
29
+
30
+ end
@@ -0,0 +1,123 @@
1
+ require 'osmer'
2
+
3
+ class Osmer::Updater
4
+ include Osmer::Configurable
5
+
6
+ attr_accessor :interval, :dump, :diff
7
+
8
+ def initialize(interval = nil)
9
+ @interval = interval
10
+ end
11
+
12
+ def load_diffs(db, schema)
13
+ db.in_transaction do |conn|
14
+ init_meta_table conn, schema
15
+ cur = get_current_version conn, schema
16
+
17
+ puts "Requesting diff versions"
18
+ versions = get_diff_versions
19
+
20
+ while version = versions.detect{|d| d[0] == cur }
21
+ puts "Downloading diff #{version.join('-')}"
22
+ file = download_file diff, version
23
+
24
+ puts "Applying diff #{file}"
25
+ yield file
26
+
27
+ set_current_version conn, schema, version[1]
28
+ cur = version[1]
29
+ end
30
+ end
31
+ end
32
+
33
+ def load_dump(db, schema)
34
+ db.in_transaction do |conn|
35
+ init_meta_table conn, schema
36
+
37
+ puts "Requesting last dump version"
38
+ version = get_dump_version
39
+
40
+ puts "Downloading dump #{version.join('-')}"
41
+ file = download_file dump, version
42
+
43
+ puts "Loading dump #{file}"
44
+ yield file
45
+
46
+ set_current_version conn, schema, version[0]
47
+ end
48
+ end
49
+
50
+ def reset(db, schema)
51
+ db.in_transaction do |conn|
52
+ init_meta_table conn, schema
53
+ reset_current_version conn, schema
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def meta_table(schema)
60
+ "#{schema.ns.prefix}_schema_versions"
61
+ end
62
+
63
+ def init_meta_table(conn, schema)
64
+ conn.exec "CREATE TABLE IF NOT EXISTS #{meta_table(schema)}(schema VARCHAR(255) NOT NULL PRIMARY KEY, version INT8 NOT NULL)"
65
+ end
66
+
67
+ def get_current_version(conn, schema)
68
+ conn.exec("SELECT version FROM #{meta_table(schema)} WHERE schema = $1", [schema.name]).values.first.first
69
+ end
70
+
71
+ def reset_current_version(conn, schema)
72
+ conn.exec "DELETE FROM #{meta_table(schema)} WHERE schema = $1", [schema.name]
73
+ end
74
+
75
+ def set_current_version(conn, schema, version)
76
+ if conn.exec("UPDATE #{meta_table(schema)} SET version = $2 WHERE schema = $1", [schema.name, version]).cmdtuples == 0
77
+ conn.exec "INSERT INTO #{meta_table(schema)}(schema, version) VALUES ($1, $2)", [schema.name, version]
78
+ end
79
+ end
80
+
81
+ def get_diff_versions
82
+ get_available_versions(diff).uniq
83
+ end
84
+
85
+ def get_dump_version
86
+ get_available_versions(dump).max
87
+ end
88
+
89
+ def get_available_versions(url)
90
+ require 'open-uri'
91
+
92
+ dir_content = open(url.gsub(/\/[^\/]+\z/, '/')).read
93
+ file_regexp = Regexp.compile Regexp.quote(url.gsub(/.*\//, '')).gsub(/\\\{\w+\\\}/,'(\d+)')
94
+
95
+ dir_content.scan(file_regexp).uniq
96
+ end
97
+
98
+ def download_file(template, subst)
99
+ require 'fileutils'
100
+
101
+ FileUtils.mkdir_p "/tmp/osmer"
102
+
103
+ url = template.split(/\{\w+\}/).zip(subst).flatten.compact.join
104
+ file = "/tmp/osmer/#{url.gsub(/.*\//, '')}"
105
+
106
+ unless File.exists? file
107
+ system "wget -O '#{file}' '#{url}'" or StandardError.new("Error downloading file '#{file}' from '#{url}'")
108
+ end
109
+
110
+ file
111
+ end
112
+
113
+ class Dsl < Struct.new(:updater)
114
+
115
+ [:interval, :dump, :diff].each do |method|
116
+ define_method method do |value|
117
+ updater.send "#{method}=", value
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -0,0 +1,9 @@
1
+ class Osmer
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: osmer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 0
10
+ version: 0.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Alexey Noskov
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-26 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: pg
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 51
29
+ segments:
30
+ - 0
31
+ - 11
32
+ - 0
33
+ version: 0.11.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 43
45
+ segments:
46
+ - 0
47
+ - 14
48
+ - 6
49
+ version: 0.14.6
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 2
63
+ - 0
64
+ version: "2.0"
65
+ type: :development
66
+ version_requirements: *id003
67
+ description: Gem for managing OpenStreetMap data in PostgreSQL database
68
+ email:
69
+ - alexey.noskov@gmail.com
70
+ executables:
71
+ - osmer
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - README.rdoc
76
+ - MIT-LICENSE
77
+ files:
78
+ - lib/osmer/updater.rb
79
+ - lib/osmer/schema/app.rb
80
+ - lib/osmer/schema/custom.rb
81
+ - lib/osmer/schema/osm2pgsql.rb
82
+ - lib/osmer/schema/base.rb
83
+ - lib/osmer/app.rb
84
+ - lib/osmer/mapper/string.rb
85
+ - lib/osmer/mapper/type.rb
86
+ - lib/osmer/mapper/area.rb
87
+ - lib/osmer/mapper/base.rb
88
+ - lib/osmer/mapper/name.rb
89
+ - lib/osmer/mapper/length.rb
90
+ - lib/osmer/mapper/address.rb
91
+ - lib/osmer/data/app.rb
92
+ - lib/osmer/thor_base.rb
93
+ - lib/osmer/version.rb
94
+ - lib/osmer/target/pg.rb
95
+ - lib/osmer.rb
96
+ - bin/osmer
97
+ - data/empty.osm
98
+ - MIT-LICENSE
99
+ - README.rdoc
100
+ homepage: http://github.com/alno/osmer
101
+ licenses: []
102
+
103
+ post_install_message:
104
+ rdoc_options:
105
+ - --line-numbers
106
+ - --inline-source
107
+ - --title
108
+ - Osmer
109
+ - --main
110
+ - README.rdoc
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ hash: 3
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ hash: 23
128
+ segments:
129
+ - 1
130
+ - 3
131
+ - 6
132
+ version: 1.3.6
133
+ requirements: []
134
+
135
+ rubyforge_project:
136
+ rubygems_version: 1.8.11
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: Gem for managing OpenStreetMap data in PostgreSQL database
140
+ test_files: []
141
+