geonames_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ Geonames-Rails
2
+ ===
3
+ Update 2
4
+ ---
5
+ Converted the plugin into a gem
6
+
7
+ Copy this into your application's Rakefile:
8
+
9
+ Dir["#{Gem.searcher.find('geonames_rails').full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
10
+
11
+ And to generate the migration and models, use
12
+
13
+ rails generate geonames_rails:migration
14
+ rails generate geonames_rails:models
15
+
16
+ Thanks to Garrett Davis for converting it into Rails 3 and John Barton for making the thing in the first place.
17
+
18
+
19
+ Update
20
+ ---
21
+
22
+ Updated the plugin for Rails 3 (v3.0.7 clean install)
23
+
24
+ * Recoded Generators
25
+ * Moved tasks/geonames_rails.rake to lib/tasks/geonames_rails.rake
26
+ * Added force_encoding to correct encoding errors in puller.rb
27
+
28
+ Thanks to John Barton (joho) for saving me some time, hopefully this fork does the same.
29
+
30
+
31
+ Getting Started
32
+ ---
33
+
34
+ I need a decent plugin that can do the following things with [the geonames database](http://www.geonames.org/)
35
+
36
+ * generate a standard db migration
37
+ * pull down the required geonames files from the web
38
+ * load those geonames files in the db (assuming a schema from the migration)
39
+ * generate me my models for free (but leave them in models so i can hack them up later)
40
+
41
+ Install the plugin by doing the following in your rails app root dir
42
+
43
+ script/plugin install git://github.com/joho/geonames-rails.git
44
+
45
+ You next step is to run the generators to give you the bare bones country/city models and the db migration
46
+
47
+ script/generate geonames_migration
48
+ script/generate geonames_models
49
+
50
+ Once you've run that migration and got your models set up how you like you're right to pull down the data straight from the geonames server and into your database. It's as easy as one little command
51
+
52
+ rake geonames_rails:pull_and_load_data
53
+
54
+ Advanced Usage
55
+ ---
56
+
57
+ OK, so you've had a bit of a play and decided one of a couple of things
58
+
59
+ * the default fields i've chosen suck and you want more/less of them
60
+ * you don't like the storage method i've chosen. maybe you want to use a document store - or something else
61
+
62
+ Well, you're covered. The method for writing out the country/city data is fully pluggable. All you need is a class that implements the following two methods
63
+
64
+ class MyCustomGeonamesWriter
65
+ def write_country(country_mapping)
66
+ end
67
+
68
+ def write_cities(country_code, city_mappings)
69
+ end
70
+ end
71
+
72
+ and then you can pass an instance of that class as the second argument to the geonames loader. See the rake task for an example.
73
+
74
+ ---
75
+
76
+ Outstanding Tasks
77
+ ---
78
+
79
+ TODO
80
+
81
+ * add regions? (maybe, i'm not sure they're worth it)
82
+
83
+ DONE
84
+
85
+ * rake task that will pull the latest geonames data into temp files
86
+ * generators for the db migration and models
87
+ * actually put something in the db migrations
88
+ * allow you to declare which fields you're using from geonames
89
+ - currently store the mappings between field names and column of data in classing in the Mappings module
90
+ - pulled out the writing of the records into a class
91
+ - changed the loader so you can plug in whatever writer you want
92
+ * write the text to AR converter
93
+
94
+ ---
95
+
96
+ Copyright John Barton 2009
@@ -0,0 +1,28 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/base'
3
+ require 'rails/generators/migration'
4
+
5
+ module GeonamesRails
6
+ module Generators
7
+ class MigrationGenerator < Rails::Generators::Base
8
+ source_root File.expand_path(File.join(File.dirname(__FILE__), 'migration_templates'))
9
+ include Rails::Generators::Migration
10
+ desc "add the geonames migrations"
11
+
12
+ def generate_migration
13
+ migration_template 'geonames_tables.rb',"db/migrate/create_geonames_tables.rb"
14
+ end
15
+
16
+ def self.next_migration_number(path)
17
+ unless @prev_migration_nr
18
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
19
+ else
20
+ @prev_migration_nr += 1
21
+ end
22
+ @prev_migration_nr.to_s
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,79 @@
1
+ class CreateGeonamesTables < ActiveRecord::Migration
2
+ def self.up
3
+ # blah
4
+
5
+ # # create countries
6
+ create_table :countries do |t|
7
+ # [0] iso alpha2
8
+ t.string :iso_code_two_letter, :null => false
9
+ # [1] iso alpha3
10
+ t.string :iso_code_three_letter, :null => false
11
+ # [2] iso numeric
12
+ t.integer :iso_number, :null => false
13
+ # [3] fips code
14
+ # [4] name
15
+ t.string :name, :null => false
16
+ # [5] capital
17
+ t.string :capital
18
+ # [6] areaInSqKm
19
+ # [7] population
20
+ t.integer :population
21
+ # [8] continent
22
+ t.string :continent
23
+ # [9] top level domain
24
+ # [10] Currency code
25
+ t.string :currency_code
26
+ # [11] Currency name
27
+ t.string :currency_name
28
+ # [12] Phone
29
+ # [13] Postal Code Format
30
+ # [14] Postal Code Regex
31
+ # [15] Languages
32
+ # [16] Geoname id
33
+ t.integer :geonames_id #, :null => false
34
+ # [17] Neighbours
35
+ # [18] Equivalent Fips Code
36
+ end
37
+
38
+ add_index :countries, :iso_code_two_letter, :unique => true
39
+ add_index :countries, :geonames_id, :null => false
40
+
41
+ # create cities
42
+ create_table :cities do |t|
43
+ t.integer :country_id, :null => false
44
+ # [0] geonameid : integer id of record in geonames database
45
+ t.integer :geonames_id, :null => false
46
+ # [1] name : name of geographical point (utf8) varchar(200)
47
+ t.string :name, :null => false
48
+ # [2] asciiname : name of geographical point in plain ascii characters, varchar(200)
49
+ # [3] alternatenames : alternatenames, comma separated varchar(4000)
50
+ # [4] latitude : latitude in decimal degrees (wgs84)
51
+ t.decimal :latitude, :precision => 14, :scale => 8, :null => false
52
+ # [5] longitude : longitude in decimal degrees (wgs84)
53
+ t.decimal :longitude, :precision => 14, :scale => 8, :null => false
54
+ # [6] feature class : see http://www.geonames.org/export/codes.html, char(1)
55
+ # [7] feature code : see http://www.geonames.org/export/codes.html, varchar(10)
56
+ # [8] country code : ISO-3166 2-letter country code, 2 characters
57
+ t.string :country_iso_code_two_letters
58
+ # [9] cc2 : alternate country codes, comma separated, ISO-3166 2-letter country code, 60 characters
59
+ # [10] admin1 code : fipscode (subject to change to iso code), isocode for the us and ch, see file admin1Codes.txt for display names of this code; varchar(20)
60
+ # [11] admin2 code : code for the second administrative division, a county in the US, see file admin2Codes.txt; varchar(80)
61
+ # [12] admin3 code : code for third level administrative division, varchar(20)
62
+ # [13] admin4 code : code for fourth level administrative division, varchar(20)
63
+ # [14] population : integer
64
+ t.integer :population
65
+ # [15] elevation : in meters, integer
66
+ # [16] gtopo30 : average elevation of 30'x30' (ca 900mx900m) area in meters, integer
67
+ # [17] timezone : the timezone id (see file timeZone.txt)
68
+ t.integer :geonames_timezone_id
69
+ # [18] modification date : date of last modification in yyyy-MM-dd format
70
+ end
71
+
72
+ add_index :cities, :geonames_id, :unique => true
73
+ end
74
+
75
+ def self.down
76
+ # drop all the tables
77
+ %w(countries cities).each { |t| drop_table t }
78
+ end
79
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails/generators'
2
+
3
+ module GeonamesRails
4
+ module Generators
5
+ class ModelsGenerator < Rails::Generators::Base
6
+ source_root File.expand_path(File.join(File.dirname(__FILE__), 'models_templates'))
7
+ include Rails::Generators::Migration
8
+ desc "add the geonames models (city and country)"
9
+
10
+ def generate_models
11
+ %w(country city).each do |model_name|
12
+ copy_file "models/#{model_name}.rb", "app/models/#{model_name}.rb"
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class City < ActiveRecord::Base
2
+ belongs_to :country
3
+ end
@@ -0,0 +1,3 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :cities
3
+ end
@@ -0,0 +1,13 @@
1
+ Dir["tasks/**/*.rake"].each { |ext| load ext } if defined?(Rake)
2
+
3
+ require 'open-uri'
4
+ require 'fileutils'
5
+
6
+ require 'geonames_rails/loader'
7
+ require 'geonames_rails/puller'
8
+ require 'geonames_rails/writers/dry_run'
9
+ require 'geonames_rails/writers/active_record'
10
+
11
+ require 'generators/geonames_rails/migration_generator'
12
+ require 'generators/geonames_rails/models_generator'
13
+ #puts 'this code runs'
@@ -0,0 +1,73 @@
1
+ require 'geonames_rails/mappings/base'
2
+ require 'geonames_rails/mappings/city'
3
+ require 'geonames_rails/mappings/country'
4
+
5
+ module GeonamesRails
6
+
7
+ class Loader
8
+
9
+ def initialize(puller, writer, logger = nil)
10
+ @logger = logger || STDOUT
11
+ @puller = puller
12
+ @writer = writer
13
+ end
14
+
15
+ def load_data
16
+ @puller.pull if @puller # pull geonames files down
17
+
18
+ #load_countries
19
+
20
+ load_cities
21
+
22
+ @puller.cleanup if @puller # cleanup the geonames files
23
+ end
24
+
25
+ protected
26
+ def load_countries
27
+ log_message "opening countries file"
28
+ File.open(File.join(Rails.root, 'tmp', 'countryInfo.txt'), 'r') do |f|
29
+ f.each_line do |line|
30
+ # skip comments
31
+ next if line.match(/^#/) || line.match(/^iso/i)
32
+
33
+ country_mapping = Mappings::Country.new(line)
34
+ result = @writer.write_country(country_mapping)
35
+
36
+ log_message result
37
+ end
38
+ end
39
+ end
40
+
41
+ def load_cities
42
+ %w(cities1000 cities5000 cities15000).each do |city_file|
43
+ #%w(cities15000).each do |city_file|
44
+ load_cities_file(city_file)
45
+ end
46
+ end
47
+
48
+ def load_cities_file(city_file)
49
+ log_message "Loading city file #{city_file}"
50
+ cities = []
51
+ File.open(File.join(Rails.root, 'tmp', "#{city_file}.txt"), 'r') do |f|
52
+ f.each_line { |line| cities << Mappings::City.new(line) }
53
+ end
54
+
55
+ log_message "#{cities.length} cities to process"
56
+
57
+ cities_by_country_code = cities.group_by { |city_mapping| city_mapping[:country_iso_code_two_letters] }
58
+
59
+ cities_by_country_code.keys.each do |country_code|
60
+ cities = cities_by_country_code[country_code]
61
+
62
+ result = @writer.write_cities(country_code, cities)
63
+
64
+ log_message result
65
+ end
66
+ end
67
+
68
+ def log_message(message)
69
+ @logger << message
70
+ @logger << "\n"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
1
+ module GeonamesRails
2
+ module Mappings
3
+ class Base < Hash
4
+ def initialize(line = nil)
5
+ if line
6
+ fields_from_line = line.split("\t")
7
+ mappings.each do |k,v|
8
+ self[k] = fields_from_line[v]
9
+ end
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ module GeonamesRails
2
+ module Mappings
3
+ class City < Base
4
+ protected
5
+ def mappings
6
+ {
7
+ # [0] geonameid : integer id of record in geonames database
8
+ :geonames_id => 0,
9
+ # [1] name : name of geographical point (utf8) varchar(200)
10
+ :name => 1,
11
+ # [2] asciiname : name of geographical point in plain ascii characters, varchar(200)
12
+ :ascii_name => 2,
13
+ # [3] alternatenames : alternatenames, comma separated varchar(4000)
14
+ :alternate_name => 3,
15
+ # [4] latitude : latitude in decimal degrees (wgs84)
16
+ :latitude => 4,
17
+ # [5] longitude : longitude in decimal degrees (wgs84)
18
+ :longitude => 5,
19
+ # [6] feature class : see http://www.geonames.org/export/codes.html, char(1)
20
+ :feature_class => 6,
21
+ # [7] feature code : see http://www.geonames.org/export/codes.html, varchar(10)
22
+ :feature_code => 7,
23
+ # [8] country code : ISO-3166 2-letter country code, 2 characters,
24
+ :country_iso_code_two_letters => 8,
25
+ # [9] cc2 : alternate country codes, comma separated, ISO-3166 2-letter country code, 60 characters
26
+ :alternate_country_codes => 9,
27
+ # [10] admin1 code : fipscode (subject to change to iso code), isocode for the us and ch, see file admin1Codes.txt for display names of this code; varchar(20)
28
+ :admin_1_code => 10,
29
+ # [11] admin2 code : code for the second administrative division, a county in the US, see file admin2Codes.txt; varchar(80)
30
+ :admin_2_code => 11,
31
+ # [12] admin3 code : code for third level administrative division, varchar(20)
32
+ :admin_3_code => 12,
33
+ # [13] admin4 code : code for fourth level administrative division, varchar(20)
34
+ :admin_4_code => 13,
35
+ # [14] population : integer
36
+ :population => 14,
37
+ # [15] elevation : in meters, integer
38
+ :elevation => 15,
39
+ # [16] gtopo30 : average elevation of 30'x30' (ca 900mx900m) area in meters, integer,
40
+ :average_elevation => 16,
41
+ # [17] timezone : the timezone id (see file timeZone.txt)
42
+ :geonames_timezone_id => 17,
43
+ # [18] modification date : date of last modification in yyyy-MM-dd format
44
+ :geonames_modification_date => 18
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ module GeonamesRails
2
+ module Mappings
3
+ class Country < Base
4
+ protected
5
+ def mappings
6
+ {
7
+ # [0] iso alpha2
8
+ :iso_code_two_letter => 0,
9
+ # [1] iso alpha3
10
+ :iso_code_three_letter => 1,
11
+ # [2] iso numeric
12
+ :iso_number => 2,
13
+ # [3] fips code
14
+ :fips_code => 3,
15
+ # [4] name
16
+ :name => 4,
17
+ # [5] capital
18
+ :capital => 5,
19
+ # [6] areaInSqKm,
20
+ :area_in_square_km => 6,
21
+ # [7] population
22
+ :population => 7,
23
+ # [8] continent
24
+ :continent => 8,
25
+ # [9] top level domain
26
+ :top_level_domain => 9,
27
+ # [10] Currency code
28
+ :currency_code => 10,
29
+ # [11] Currency name
30
+ :currency_name => 11,
31
+ # [12] Phone
32
+ :phone => 12,
33
+ # [13] Postal Code Format
34
+ :postal_code_format => 13,
35
+ # [14] Postal Code Regex
36
+ :postal_code_regex => 14,
37
+ # [15] Languages
38
+ :languages => 15,
39
+ # [16] Geoname id
40
+ :geonames_id => 16,
41
+ # [17] Neighbours
42
+ :neighbours => 17,
43
+ # [18] Equivalent Fips Code
44
+ :equivalent_fips_code => 18
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ module GeonamesRails
2
+ class Puller
3
+ def pull
4
+ @temp_geonames_files = []
5
+ target_dir = File.join(Rails.root, 'tmp')
6
+
7
+ file_names = %w(cities1000.zip cities5000.zip cities15000.zip admin1CodesASCII.txt countryInfo.txt)
8
+ file_names.each do |file_name|
9
+ url = "http://download.geonames.org/export/dump/#{file_name}"
10
+
11
+ remote_file = open(url)
12
+
13
+ target_file_name = File.join(target_dir, file_name)
14
+ File.open target_file_name, 'w' do |f|
15
+ f.write(remote_file.read.force_encoding("UTF-8"))
16
+ end
17
+ remote_file.close
18
+
19
+ @temp_geonames_files << target_file_name
20
+
21
+ file_base_name, file_extension = file_name.split('.')
22
+ if file_extension == 'zip'
23
+ `unzip #{target_file_name} -d #{target_dir}`
24
+ @temp_geonames_files << File.join(target_dir, "#{file_base_name}.txt")
25
+ end
26
+
27
+ end
28
+ end
29
+
30
+ def cleanup
31
+ @temp_geonames_files.each do |f|
32
+ FileUtils.rm f
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module GeonamesRails
2
+ module Writers
3
+ class ActiveRecord
4
+
5
+ def write_country(country_mapping)
6
+ iso_code = country_mapping[:iso_code_two_letter]
7
+ c = Country.find_or_initialize_by_iso_code_two_letter(iso_code)
8
+
9
+ created_or_updating = c.new_record? ? 'Creating' : 'Updating'
10
+
11
+ c.attributes = country_mapping.slice(:iso_code_two_letter,
12
+ :iso_code_three_letter,
13
+ :iso_number,
14
+ :name,
15
+ :capital,
16
+ :continent,
17
+ :geonames_id)
18
+ c.save!
19
+
20
+ "#{created_or_updating} db record for #{iso_code}"
21
+ end
22
+
23
+
24
+
25
+ def write_cities(country_code, city_mappings)
26
+ country = Country.find_by_iso_code_two_letter(country_code)
27
+
28
+ #puts country
29
+
30
+ city_mappings.each do |city_mapping|
31
+ city = City.find_or_initialize_by_geonames_id(city_mapping[:geonames_id])
32
+ city.country_id = country.id
33
+
34
+ city.attributes = city_mapping.slice(:name,
35
+ :latitude,
36
+ :longitude,
37
+ :country_iso_code_two_letters,
38
+ :geonames_timezone_id)
39
+
40
+ city.save!
41
+ end
42
+
43
+ "Processed #{country.name}(#{country_code}) with #{city_mappings.length} cities"
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ module GeonamesRails
2
+ module Writers
3
+ class DryRun
4
+ def write_country(country_mapping)
5
+ raise "must have a of country mapping" unless country_mapping
6
+
7
+ "Dry run of country #{country_mapping[:name]} should have been OK"
8
+ end
9
+
10
+ def write_cities(country_code, city_mappings)
11
+ raise "can't create cities without a country" unless country_code
12
+ raise "must have a set of city mappings" unless city_mappings
13
+
14
+ raise "i'm sure there should be at least 1 city in this country" if city_mappings.empty?
15
+
16
+ "Dry run of country #{country_code} would have written out #{city_mappings.length} cities"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ require 'geonames_rails'
2
+
3
+ namespace :geonames_rails do
4
+ desc 'pull down the geonames data from the server'
5
+ task :pull_data => :environment do
6
+ GeonamesRails::Puller.new.pull
7
+ end
8
+
9
+ desc 'pull geonames data, load into db, then clean up after itself'
10
+ task :pull_and_load_data => :environment do
11
+ puller = GeonamesRails::Puller.new
12
+ writer = ENV['DRY_RUN'] ? GeonamesRails::Writers::DryRun.new : GeonamesRails::Writers::ActiveRecord.new
13
+ GeonamesRails::Loader.new(puller, writer).load_data
14
+ end
15
+
16
+ desc 'load the data from files you already have laying about'
17
+ task :load_data => :environment do
18
+ writer = ENV['DRY_RUN'] ? GeonamesRails::Writers::DryRun.new : GeonamesRails::Writers::ActiveRecord.new
19
+ GeonamesRails::Loader.new(nil, writer).load_data
20
+ end
21
+
22
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: geonames_rails
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Marius Andra
13
+ - Garrett Davis
14
+ - John Barton
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-02-18 00:00:00 +01:00
20
+ default_executable:
21
+ dependencies: []
22
+
23
+ description: Fetch data from geonames.org and make the required models
24
+ email: marius.andra@gmail.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - lib/geonames_rails/writers/active_record.rb
33
+ - lib/geonames_rails/writers/dry_run.rb
34
+ - lib/geonames_rails/mappings/city.rb
35
+ - lib/geonames_rails/mappings/base.rb
36
+ - lib/geonames_rails/mappings/country.rb
37
+ - lib/geonames_rails/puller.rb
38
+ - lib/geonames_rails/loader.rb
39
+ - lib/geonames_rails.rb
40
+ - lib/generators/geonames_rails/migration_generator.rb
41
+ - lib/generators/geonames_rails/migration_templates/geonames_tables.rb
42
+ - lib/generators/geonames_rails/models_templates/models/city.rb
43
+ - lib/generators/geonames_rails/models_templates/models/country.rb
44
+ - lib/generators/geonames_rails/models_generator.rb
45
+ - lib/tasks/geonames_rails.rake
46
+ - README.md
47
+ has_rdoc: true
48
+ homepage: http://rubygems.org/gems/geonames-rails
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Geonames.org support for Rails applications
79
+ test_files: []
80
+