osmer 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+