Merged 14009:14059 from trunk.
authorTom Hughes <tom@compton.nu>
Thu, 12 Mar 2009 00:13:27 +0000 (00:13 +0000)
committerTom Hughes <tom@compton.nu>
Thu, 12 Mar 2009 00:13:27 +0000 (00:13 +0000)
347 files changed:
app/controllers/amf_controller.rb
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/browse_controller.rb
app/controllers/changeset_controller.rb [new file with mode: 0644]
app/controllers/changeset_tag_controller.rb [new file with mode: 0644]
app/controllers/diary_entry_controller.rb
app/controllers/message_controller.rb
app/controllers/node_controller.rb
app/controllers/old_node_controller.rb
app/controllers/old_relation_controller.rb
app/controllers/old_way_controller.rb
app/controllers/relation_controller.rb
app/controllers/trace_controller.rb
app/controllers/user_controller.rb
app/controllers/user_preference_controller.rb
app/controllers/way_controller.rb
app/helpers/browse_helper.rb
app/models/changeset.rb [new file with mode: 0644]
app/models/changeset_tag.rb [new file with mode: 0644]
app/models/diary_entry.rb
app/models/message.rb
app/models/node.rb
app/models/node_tag.rb [new file with mode: 0644]
app/models/old_node.rb
app/models/old_node_tag.rb [new file with mode: 0644]
app/models/old_relation.rb
app/models/old_relation_member.rb
app/models/old_relation_tag.rb
app/models/old_way.rb
app/models/old_way_tag.rb
app/models/relation.rb
app/models/relation_member.rb
app/models/relation_tag.rb
app/models/trace.rb
app/models/tracetag.rb
app/models/user.rb
app/models/user_preference.rb
app/models/way.rb
app/models/way_tag.rb
app/views/browse/_changeset_details.rhtml [new file with mode: 0644]
app/views/browse/_common_details.rhtml
app/views/browse/_paging_nav.rhtml [new file with mode: 0644]
app/views/browse/changeset.rhtml [new file with mode: 0644]
app/views/browse/index.rhtml
app/views/browse/not_found.rhtml [new file with mode: 0644]
app/views/browse/start.rjs
app/views/changeset/_changeset.rhtml [new file with mode: 0644]
app/views/changeset/_changeset_paging_nav.rhtml [new file with mode: 0644]
app/views/changeset/list.rhtml [new file with mode: 0644]
app/views/diary_entry/edit.rhtml
app/views/diary_entry/list.rhtml
app/views/diary_entry/no_such_entry.rhtml [new file with mode: 0644]
app/views/layouts/site.rhtml
app/views/message/_message_summary.rhtml
app/views/message/_sent_message_summary.rhtml
app/views/message/new.rhtml
app/views/message/no_such_user.rhtml [new file with mode: 0644]
app/views/user/account.rhtml
app/views/user/login.rhtml
app/views/user/new.rhtml
app/views/user/view.rhtml
config/application.yml
config/database.yml
config/environment.rb
config/environments/development.rb
config/initializers/composite_primary_keys.rb [deleted file]
config/initializers/libxml.rb
config/potlatch/colours.txt
config/routes.rb
db/README
db/functions/Makefile
db/functions/maptile.c
db/migrate/001_create_osm_db.rb
db/migrate/002_cleanup_osm_db.rb
db/migrate/003_sql_session_store_setup.rb
db/migrate/004_user_enhancements.rb
db/migrate/005_tile_tracepoints.rb
db/migrate/006_tile_nodes.rb
db/migrate/007_add_relations.rb
db/migrate/010_diary_comments.rb
db/migrate/013_add_email_valid.rb
db/migrate/015_add_user_visible.rb
db/migrate/019_add_timestamp_indexes.rb [new file with mode: 0644]
db/migrate/020_populate_node_tags_and_remove.rb [new file with mode: 0644]
db/migrate/020_populate_node_tags_and_remove_helper.c [new file with mode: 0644]
db/migrate/021_move_to_innodb.rb [new file with mode: 0644]
db/migrate/022_key_constraints.rb [new file with mode: 0644]
db/migrate/023_add_changesets.rb [new file with mode: 0644]
db/migrate/024_order_relation_members.rb [new file with mode: 0644]
db/migrate/025_add_end_time_to_changesets.rb [new file with mode: 0644]
doc/README_FOR_APP
lib/consistency_validations.rb [new file with mode: 0644]
lib/diff_reader.rb [new file with mode: 0644]
lib/geo_record.rb
lib/map_boundary.rb
lib/migrate.rb
lib/osm.rb
lib/tags.rb [deleted file]
lib/tasks/populate_node_tags.rake [deleted file]
lib/validators.rb [new file with mode: 0644]
public/404.html
public/500.html
public/images/new.png [new file with mode: 0644]
public/javascripts/map.js
public/potlatch/beep.mp3
public/potlatch/potlatch.swf
public/stylesheets/site.css
test/fixtures/changeset_tags.yml [new file with mode: 0644]
test/fixtures/changesets.yml [new file with mode: 0644]
test/fixtures/current_node_tags.yml [new file with mode: 0644]
test/fixtures/current_nodes.yml
test/fixtures/current_relation_members.yml
test/fixtures/current_relation_tags.yml
test/fixtures/current_relations.yml
test/fixtures/current_way_nodes.yml
test/fixtures/current_way_tags.yml
test/fixtures/current_ways.yml
test/fixtures/diary_comments.yml [new file with mode: 0644]
test/fixtures/diary_entries.yml [new file with mode: 0644]
test/fixtures/friends.yml [new file with mode: 0644]
test/fixtures/gps_points.yml [new file with mode: 0644]
test/fixtures/gpx_file_tags.yml [new file with mode: 0644]
test/fixtures/gpx_files.yml [new file with mode: 0644]
test/fixtures/messages.yml
test/fixtures/node_tags.yml [new file with mode: 0644]
test/fixtures/nodes.yml
test/fixtures/relation_tags.yml
test/fixtures/relations.yml
test/fixtures/user_preferences.yml
test/fixtures/users.yml
test/fixtures/way_nodes.yml
test/fixtures/way_tags.yml
test/fixtures/ways.yml
test/functional/amf_controller_test.rb [new file with mode: 0644]
test/functional/api_controller_test.rb
test/functional/browse_controller_test.rb [new file with mode: 0644]
test/functional/changeset_controller_test.rb [new file with mode: 0644]
test/functional/changeset_tag_controller_test.rb [new file with mode: 0644]
test/functional/diary_entry_controller_test.rb [new file with mode: 0644]
test/functional/export_controller_test.rb [new file with mode: 0644]
test/functional/friend_controller_test.rb [new file with mode: 0644]
test/functional/geocoder_controller_test.rb
test/functional/message_controller_test.rb
test/functional/node_controller_test.rb
test/functional/old_node_controller_test.rb [new file with mode: 0644]
test/functional/old_relation_controller_test.rb
test/functional/old_way_controller_test.rb
test/functional/relation_controller_test.rb
test/functional/search_controller_test.rb [new file with mode: 0644]
test/functional/site_controller_test.rb [new file with mode: 0644]
test/functional/swf_controller_test.rb [new file with mode: 0644]
test/functional/trace_controller_test.rb [new file with mode: 0644]
test/functional/user_controller_test.rb [new file with mode: 0644]
test/functional/user_preference_controller_test.rb
test/functional/way_controller_test.rb
test/integration/user_diaries_test.rb [new file with mode: 0644]
test/test_helper.rb
test/unit/changeset_tag_test.rb [new file with mode: 0644]
test/unit/changeset_test.rb [new file with mode: 0644]
test/unit/diary_comment_test.rb [new file with mode: 0644]
test/unit/diary_entry_test.rb [new file with mode: 0644]
test/unit/friend_test.rb [new file with mode: 0644]
test/unit/message_test.rb
test/unit/node_tag_test.rb [new file with mode: 0644]
test/unit/node_test.rb
test/unit/old_node_tag_test.rb [new file with mode: 0644]
test/unit/old_node_test.rb [new file with mode: 0644]
test/unit/old_relation_tag_test.rb [new file with mode: 0644]
test/unit/old_way_tag_test.rb [new file with mode: 0644]
test/unit/relation_member_test.rb [new file with mode: 0644]
test/unit/relation_tag_test.rb [new file with mode: 0644]
test/unit/relation_test.rb [new file with mode: 0644]
test/unit/trace_test.rb [new file with mode: 0644]
test/unit/tracepoint_test.rb [new file with mode: 0644]
test/unit/tracetag_test.rb [new file with mode: 0644]
test/unit/user_preference_test.rb
test/unit/user_test.rb
test/unit/user_token_test.rb [new file with mode: 0644]
test/unit/way_node_test.rb [new file with mode: 0644]
test/unit/way_tag_test.rb [new file with mode: 0644]
test/unit/way_test.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/History.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/Manifest.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/README.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/Rakefile [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/init.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/install.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/loader.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb [new file with mode: 0755]
vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tmp/test.db [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/index.html [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/index.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/template.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version.txt [new file with mode: 0644]
vendor/plugins/classic_pagination/lib/pagination.rb
vendor/plugins/deadlock_retry/README [new file with mode: 0644]
vendor/plugins/deadlock_retry/Rakefile [new file with mode: 0644]
vendor/plugins/deadlock_retry/init.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/lib/deadlock_retry.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb [new file with mode: 0644]
vendor/plugins/file_column/CHANGELOG [new file with mode: 0644]
vendor/plugins/file_column/README [new file with mode: 0644]
vendor/plugins/file_column/Rakefile [new file with mode: 0644]
vendor/plugins/file_column/TODO [new file with mode: 0644]
vendor/plugins/file_column/init.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column_helper.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_compat.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/magick_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/rails_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/test_case.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/validations.rb [new file with mode: 0644]
vendor/plugins/file_column/test/abstract_unit.rb [new file with mode: 0644]
vendor/plugins/file_column/test/connection.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_helper_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_test.rb [new file with mode: 0755]
vendor/plugins/file_column/test/fixtures/entry.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/invalid-image.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/kerb.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/mysql.sql [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/schema.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/skanthak.png [new file with mode: 0644]
vendor/plugins/file_column/test/magick_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/magick_view_only_test.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/LICENSE [new file with mode: 0644]
vendor/plugins/sql_session_store/README [new file with mode: 0755]
vendor/plugins/sql_session_store/Rakefile [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/USAGE [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/init.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/install.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/mysql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/oracle_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/postgresql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sql_session.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/lib/sql_session_store.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sqlite_session.rb [new file with mode: 0755]

index febc4db..331487a 100644 (file)
@@ -3,7 +3,7 @@
 # OSM database takes place using this controller. Messages are 
 # encoded in the Actionscript Message Format (AMF).
 #
-# Helper functions are in /lib/potlatch.
+# Helper functions are in /lib/potlatch.rb
 #
 # Author::     editions Systeme D / Richard Fairhurst 2004-2008
 # Licence::    public domain.
 # from the AMF message), each method generally takes arguments in the order 
 # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
 # Methods all return an array to the SWF.
+#
+# == API 0.6
+#
+# Note that this requires a patched version of composite_primary_keys 1.1.0
+# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) 
+# if you are to run with POTLATCH_USE_SQL=false .
 # 
 # == Debugging
 # 
 #      return(-1,"message")            <-- just puts up a dialogue
 #      return(-2,"message")            <-- also asks the user to e-mail me
 # 
-# To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message").
+# To write to the Rails log, use logger.info("message").
+
+# Remaining issues:
+# * version conflict when POIs and ways are reverted
 
 class AmfController < ApplicationController
   require 'stringio'
 
   include Potlatch
 
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
   session :off
   before_filter :check_write_availability
 
@@ -36,235 +48,396 @@ class AmfController < ApplicationController
   # ** FIXME: refactor to reduce duplication of code across read/write
   
   def amf_read
-       req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
-                                                                                       # (cf http://www.ruby-forum.com/topic/122163)
-       req.read(2)                                                             # Skip version indicator and client ID
-       results={}                                                              # Results of each body
-
-       # Parse request
-
-       headers=AMF.getint(req)                                 # Read number of headers
-
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'getpresets';                      results[index]=AMF.putdata(index,getpresets())
-               when 'whichways';                       results[index]=AMF.putdata(index,whichways(*args))
-               when 'whichways_deleted';       results[index]=AMF.putdata(index,whichways_deleted(*args))
-               when 'getway';                          results[index]=AMF.putdata(index,getway(args[0].to_i))
-               when 'getrelation';                     results[index]=AMF.putdata(index,getrelation(args[0].to_i))
-               when 'getway_old';                      results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
-               when 'getway_history';          results[index]=AMF.putdata(index,getway_history(args[0].to_i))
-               when 'getnode_history';         results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
-               when 'findrelations';           results[index]=AMF.putdata(index,findrelations(*args))
-               when 'getpoi';                          results[index]=AMF.putdata(index,getpoi(*args))
-         end
-       end
+    req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
+                              # (cf http://www.ruby-forum.com/topic/122163)
+    req.read(2)                                                                # Skip version indicator and client ID
+    results={}                                                         # Results of each body
+
+    # Parse request
+
+    headers=AMF.getint(req)                                    # Read number of headers
+
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+      logger.info("Executing AMF #{message}:#{index}")
+
+      case message
+        when 'getpresets';                     results[index]=AMF.putdata(index,getpresets())
+        when 'whichways';                      results[index]=AMF.putdata(index,whichways(*args))
+        when 'whichways_deleted';      results[index]=AMF.putdata(index,whichways_deleted(*args))
+        when 'getway';                         results[index]=AMF.putdata(index,getway(args[0].to_i))
+        when 'getrelation';                    results[index]=AMF.putdata(index,getrelation(args[0].to_i))
+        when 'getway_old';                     results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1]))
+        when 'getway_history';         results[index]=AMF.putdata(index,getway_history(args[0].to_i))
+        when 'getnode_history';                results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
+        when 'findgpx';                                results[index]=AMF.putdata(index,findgpx(*args))
+        when 'findrelations';          results[index]=AMF.putdata(index,findrelations(*args))
+        when 'getpoi';                         results[index]=AMF.putdata(index,getpoi(*args))
+      end
+    end
+    logger.info("encoding AMF results")
     sendresponse(results)
   end
 
   def amf_write
-       req=StringIO.new(request.raw_post+0.chr)
-       req.read(2)
-       results={}
-       renumberednodes={}                                              # Shared across repeated putways
-       renumberedways={}                                               # Shared across repeated putways
-
-       headers=AMF.getint(req)                                 # Read number of headers
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'putway';                          r=putway(renumberednodes,*args)
+    req=StringIO.new(request.raw_post+0.chr)
+    req.read(2)
+    results={}
+    renumberednodes={}                                         # Shared across repeated putways
+    renumberedways={}                                          # Shared across repeated putways
+
+    headers=AMF.getint(req)                                    # Read number of headers
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+
+      logger.info("Executing AMF #{message}:#{index}")
+      case message
+        when 'putway';                         r=putway(renumberednodes,*args)
                                                                        renumberednodes=r[3]
-                                                                       if r[1] != r[2]
-                                                                         renumberedways[r[1]] = r[2]
-                                                                       end
+                                                                       if r[1] != r[2] then renumberedways[r[1]] = r[2] end
                                                                        results[index]=AMF.putdata(index,r)
-               when 'putrelation';                     results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
-               when 'deleteway';                       results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
-               when 'putpoi';                          results[index]=AMF.putdata(index,putpoi(*args))
-         end
-       end
+        when 'putrelation';                    results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
+        when 'deleteway';                      results[index]=AMF.putdata(index,deleteway(*args))
+        when 'putpoi';                         r=putpoi(*args)
+                                                                       if r[1] != r[2] then renumberednodes[r[1]] = r[2] end
+                                                               results[index]=AMF.putdata(index,r)
+        when 'startchangeset';         results[index]=AMF.putdata(index,startchangeset(*args))
+      end
+    end
+    logger.info("encoding AMF results")
     sendresponse(results)
   end
 
   private
 
+  # Start new changeset
+  
+  def startchangeset(usertoken, cstags, closeid, closecomment)
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
+
+    # close previous changeset and add comment
+    if closeid
+      cs = Changeset.find(closeid)
+      cs.set_closed_time_now
+      if cs.user_id!=user.id
+        return -2,"You cannot close that changeset because you're not the person who opened it."
+      elsif closecomment.empty?
+        cs.save!
+      else
+        cs.tags['comment']=closecomment
+        cs.save_with_tags!
+      end
+    end
+       
+    # open a new changeset
+    cs = Changeset.new
+    cs.tags = cstags
+    cs.user_id = user.id
+    # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better
+    cs.created_at = Time.now
+    cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT
+    cs.save_with_tags!
+    return [0,cs.id]
+  end
+
   # Return presets (default tags, localisation etc.):
   # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
 
   def getpresets() #:doc:
-       return POTLATCH_PRESETS
+    return POTLATCH_PRESETS
   end
 
+  ##
   # Find all the ways, POI nodes (i.e. not part of ways), and relations
   # in a given bounding box. Nodes are returned in full; ways and relations 
   # are IDs only. 
-
+  #
+  # return is of the form: 
+  # [error_code, 
+  #  [[way_id, way_version], ...],
+  #  [[node_id, lat, lon, [tags, ...], node_version], ...],
+  #  [[rel_id, rel_version], ...]]
+  # where the ways are any visible ways which refer to any visible
+  # nodes in the bbox, nodes are any visible nodes in the bbox but not
+  # used in any way, rel is any relation which refers to either a way
+  # or node that we're returning.
   def whichways(xmin, ymin, xmax, ymax) #:doc:
-       enlarge = [(xmax-xmin)/8,0.01].min
-       xmin -= enlarge; ymin -= enlarge
-       xmax += enlarge; ymax += enlarge
-
-       if POTLATCH_USE_SQL then
-         way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax)
-         points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
-         relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, way_ids)
-       else
-         # find the way ids in an area
-         nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 1", :include => :ways)
-         way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
-
-         # find the node ids in an area that aren't part of ways
-         nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
-         points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
-
-         # find the relations used by those nodes and ways
-         relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => "visible = 1") +
-                  Relation.find_for_ways(way_ids, :conditions => "visible = 1")
-         relation_ids = relations.collect { |relation| relation.id }.uniq
-       end
-
-       [way_ids, points, relation_ids]
+    enlarge = [(xmax-xmin)/8,0.01].min
+    xmin -= enlarge; ymin -= enlarge
+    xmax += enlarge; ymax += enlarge
+    
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    check_boundaries(xmin, ymin, xmax, ymax)
+
+    if POTLATCH_USE_SQL then
+      ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
+      points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
+      relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
+    else
+      # find the way ids in an area
+      nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
+      ways = nodes_in_area.inject([]) { |sum, node| 
+        visible_ways = node.ways.select { |w| w.visible? }
+        sum + visible_ways.collect { |w| [w.id,w.version] }
+      }.uniq
+      ways.delete([])
+
+      # find the node ids in an area that aren't part of ways
+      nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
+      points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq
+
+      # find the relations used by those nodes and ways
+      relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
+                  Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true})
+      relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
+    end
+
+    [0, ways, points, relations]
+
+  rescue Exception => err
+    [-2,"Sorry - I can't get the map for that area."]
   end
 
   # Find deleted ways in current bounding box (similar to whichways, but ways
   # with a deleted node only - not POIs or relations).
 
   def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
-       xmin -= 0.01; ymin -= 0.01
-       xmax += 0.01; ymax += 0.01
-
-       nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 0 AND current_ways.visible = 0", :include => :ways_via_history)
-       way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
-
-       [way_ids]
+    enlarge = [(xmax-xmin)/8,0.01].min
+    xmin -= enlarge; ymin -= enlarge
+    xmax += enlarge; ymax += enlarge
+
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    begin
+      check_boundaries(xmin, ymin, xmax, ymax)
+    rescue Exception => err
+      return [-2,"Sorry - I can't get the map for that area."]
+    end
+
+    nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
+    way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
+
+    [0,way_ids]
   end
 
   # Get a way including nodes and tags.
-  # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
+  # Returns the way id, a Potlatch-style array of points, a hash of tags, and the version number.
 
   def getway(wayid) #:doc:
-       if POTLATCH_USE_SQL then
-         points = sql_get_nodes_in_way(wayid)
-         tags = sql_get_tags_in_way(wayid)
-       else
-         # Ideally we would do ":include => :nodes" here but if we do that
-         # then rails only seems to return the first copy of a node when a
-         # way includes a node more than once
-         way = Way.find(wayid)
-         points = way.nodes.collect do |node|
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               [node.lon, node.lat, node.id, nodetags]
-         end
-         tags = way.tags
-       end
-
-       [wayid, points, tags]
+    if POTLATCH_USE_SQL then
+        points = sql_get_nodes_in_way(wayid)
+        tags = sql_get_tags_in_way(wayid)
+        version = sql_get_way_version(wayid)
+      else
+        # Ideally we would do ":include => :nodes" here but if we do that
+        # then rails only seems to return the first copy of a node when a
+        # way includes a node more than once
+        begin
+          way = Way.find(wayid)
+        rescue ActiveRecord::RecordNotFound
+          return [wayid,[],{}]
+        end
+
+        # check case where way has been deleted or doesn't exist
+        return [wayid,[],{}] if way.nil? or !way.visible
+
+        points = way.nodes.collect do |node|
+        nodetags=node.tags
+        nodetags.delete('created_by')
+        [node.lon, node.lat, node.id, nodetags, node.version]
+      end
+      tags = way.tags
+      version = way.version
+    end
+
+    [wayid, points, tags, version]
   end
 
   # Get an old version of a way, and all constituent nodes.
   #
-  # For undelete (version=0), always uses the most recent version of each node, 
-  # even if it's moved.  For revert (version=1+), uses the node in existence 
+  # For undelete (version<0), always uses the most recent version of each node, 
+  # even if it's moved.  For revert (version >= 0), uses the node in existence 
   # at the time, generating a new id if it's still visible and has been moved/
   # retagged.
-
-  def getway_old(id, version) #:doc:
-       if version < 0
-         old_way = OldWay.find(:first, :conditions => ['visible = 1 AND id = ?', id], :order => 'version DESC')
-         points = old_way.get_nodes_undelete
-       else
-         old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
-         points = old_way.get_nodes_revert
-       end
-
-       old_way.tags['history'] = "Retrieved from v#{old_way.version}"
-
-       [0, id, points, old_way.tags, old_way.version]
+  #
+  # Returns:
+  # 0. success code, 
+  # 1. id, 
+  # 2. array of points, 
+  # 3. hash of tags, 
+  # 4. version, 
+  # 5. is this the current, visible version? (boolean)
+  
+  def getway_old(id, timestamp) #:doc:
+    if timestamp == ''
+      # undelete
+      old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
+      points = old_way.get_nodes_undelete unless old_way.nil?
+    else
+      # revert
+      timestamp = DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")
+      old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC')
+      points = old_way.get_nodes_revert(timestamp) unless old_way.nil?
+      if !old_way.visible
+        return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."]
+      end
+    end
+
+    if old_way.nil?
+      # *** FIXME: shouldn't this be returning an error?
+      return [-1, id, [], {}, -1,0]
+    else
+      curway=Way.find(id)
+      old_way.tags['history'] = "Retrieved from v#{old_way.version}"
+      return [0, id, points, old_way.tags, curway.version, (curway.version==old_way.version and curway.visible)]
+    end
   end
   
-  # Find history of a way. Returns 'way', id, and 
-  # an array of previous versions.
+  # Find history of a way.
+  # Returns 'way', id, and an array of previous versions:
+  # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
+  # - now [timestamp,user,uid]
+  #
+  # Heuristic: Find all nodes that have ever been part of the way; 
+  # get a list of their revision dates; add revision dates of the way;
+  # sort and collapse list (to within 2 seconds); trim all dates before the 
+  # start date of the way.
 
   def getway_history(wayid) #:doc:
-       history = Way.find(wayid).old_ways.reverse.collect do |old_way|
-         user = old_way.user.data_public? ? old_way.user.display_name : 'anonymous'
-         uid  = old_way.user.data_public? ? old_way.user.id : 0
-         [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
-       end
 
-       ['way',wayid,history]
+    begin
+      # Find list of revision dates for way and all constituent nodes
+      revdates=[]
+      revusers={}
+      Way.find(wayid).old_ways.collect do |a|
+        revdates.push(a.timestamp)
+        unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end
+        a.nds.each do |n|
+          Node.find(n).old_nodes.collect do |o|
+            revdates.push(o.timestamp)
+            unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end
+          end
+        end
+      end
+      waycreated=revdates[0]
+      revdates.uniq!
+      revdates.sort!
+         revdates.reverse!
+
+      # Remove any dates (from nodes) before first revision date of way
+      revdates.delete_if { |d| d<waycreated }
+      # Remove any elements where 2 seconds doesn't elapse before next one
+      revdates.delete_if { |d| revdates.include?(d+1) or revdates.include?(d+2) }
+      # Collect all in one nested array
+      revdates.collect! {|d| [d.strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
+
+      return ['way',wayid,revdates]
+    rescue ActiveRecord::RecordNotFound
+      return ['way', wayid, []]
+    end
   end
-
-  # Find history of a node. Returns 'node', id, and 
-  # an array of previous versions.
+  
+  # Find history of a node. Returns 'node', id, and an array of previous versions as above.
 
   def getnode_history(nodeid) #:doc:
-       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
-         user = old_node.user.data_public? ? old_node.user.display_name : 'anonymous'
-         uid  = old_node.user.data_public? ? old_node.user.id : 0
-         [old_node.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
-       end
+    begin 
+      history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
+        [old_node.timestamp.strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
+      end
+      return ['node', nodeid, history]
+    rescue ActiveRecord::RecordNotFound
+      return ['node', nodeid, []]
+    end
+  end
 
-       ['node',nodeid,history]
+  def change_user(obj)
+    user_object = obj.changeset.user
+    user = user_object.data_public? ? user_object.display_name : 'anonymous'
+    uid  = user_object.data_public? ? user_object.id : 0
+    [user,uid]
+  end
+
+  # Find GPS traces with specified name/id.
+  # Returns array listing GPXs, each one comprising id, name and description.
+  
+  def findgpx(searchterm, usertoken)
+    user = getuser(usertoken)
+    if !uid then return -1,"You must be logged in to search for GPX traces." end
+
+    gpxs = []
+    if searchterm.to_i>0 then
+      gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,user.id] )
+      if gpx then
+        gpxs.push([gpx.id, gpx.name, gpx.description])
+      end
+    else
+      Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,user.id,searchterm] ).each do |gpx|
+      gpxs.push([gpx.id, gpx.name, gpx.description])
+         end
+       end
+    gpxs
   end
 
   # Get a relation with all tags and members.
   # Returns:
   # 0. relation id,
   # 1. hash of tags,
-  # 2. list of members.
+  # 2. list of members,
+  # 3. version.
   
   def getrelation(relid) #:doc:
-       rel = Relation.find(relid)
-
-       [relid, rel.tags, rel.members]
+    begin
+      rel = Relation.find(relid)
+    rescue ActiveRecord::RecordNotFound
+      return [relid, {}, []]
+    end
+
+    return [relid, {}, [], nil] if rel.nil? or !rel.visible
+    [relid, rel.tags, rel.members, rel.version]
   end
 
   # Find relations with specified name/id.
   # Returns array of relations, each in same form as getrelation.
   
   def findrelations(searchterm)
-       rels = []
-       if searchterm.to_i>0 then
-         rel = Relation.find(searchterm.to_i)
-         if rel and rel.visible then
-           rels.push([rel.id, rel.tags, rel.members])
-         end
-       else
-         RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
-               if t.relation.visible then
+    rels = []
+    if searchterm.to_i>0 then
+      rel = Relation.find(searchterm.to_i)
+      if rel and rel.visible then
+        rels.push([rel.id, rel.tags, rel.members])
+      end
+    else
+      RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
+      if t.relation.visible then
              rels.push([t.relation.id, t.relation.tags, t.relation.members])
            end
          end
        end
-       rels
+    rels
   end
 
   # Save a relation.
@@ -273,226 +446,335 @@ class AmfController < ApplicationController
   # 1. original relation id (unchanged),
   # 2. new relation id.
 
-  def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the relation could not be saved." end
-
-       relid = relid.to_i
-       visible = visible.to_i
-
-       # create a new relation, or find the existing one
-       if relid <= 0
-         rel = Relation.new
-       else
-         rel = Relation.find(relid)
-       end
-
-       # check the members are all positive, and correctly type
-       typedmembers = []
-       members.each do |m|
-         mid = m[1].to_i
-         if mid < 0
-               mid = renumberednodes[mid] if m[0] == 'node'
-               mid = renumberedways[mid] if m[0] == 'way'
-         end
-      if mid
-           typedmembers << [m[0], mid, m[2]]
-         end
-       end
-
-       # assign new contents
-       rel.members = typedmembers
-       rel.tags = tags
-       rel.visible = visible
-       rel.user_id = uid
-
-       # check it then save it
-       # BUG: the following is commented out because it always fails on my
-       #  install. I think it's a Rails bug.
-
-       #if !rel.preconditions_ok?
-       #  return -2, "Relation preconditions failed"
-       #else
-         rel.save_with_history!
-       #end
-
-       [0, relid, rel.id]
+  def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) #:doc:
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the relation could not be saved." end
+
+    relid = relid.to_i
+    visible = (visible.to_i != 0)
+
+    new_relation = nil
+    relation = nil
+    Relation.transaction do
+      # create a new relation, or find the existing one
+      if relid > 0
+        relation = Relation.find(relid)
+      end
+      # We always need a new node, based on the data that has been sent to us
+      new_relation = Relation.new
+
+      # check the members are all positive, and correctly type
+      typedmembers = []
+      members.each do |m|
+        mid = m[1].to_i
+        if mid < 0
+          mid = renumberednodes[mid] if m[0] == 'node'
+          mid = renumberedways[mid] if m[0] == 'way'
+        end
+        if mid
+          typedmembers << [m[0], mid, m[2]]
+        end
+      end
+
+      # assign new contents
+      new_relation.members = typedmembers
+      new_relation.tags = tags
+      new_relation.visible = visible
+      new_relation.changeset_id = changeset_id
+      new_relation.version = version
+
+
+      if id <= 0
+        # We're creating the node
+        new_relation.create_with_history(user)
+      elsif visible
+        # We're updating the node
+        relation.update_from(new_relation, user)
+      else
+        # We're deleting the node
+        relation.delete_with_history!(new_relation, user)
+      end
+    end # transaction
+      
+    if id <= 0
+      return [0, relid, new_relation.id, new_relation.version]
+    else
+      return [0, relid, relation.id, relation.version]
+    end
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}."]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this relation since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The relation has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-( ."]
   end
 
   # Save a way to the database, including all nodes. Any nodes in the previous
   # version and no longer used are deleted.
   # 
+  # Parameters:
+  # 0. hash of renumbered nodes (added by amf_controller)
+  # 1. current user token (for authentication)
+  # 2. current changeset
+  # 3. new way version
+  # 4. way ID
+  # 5. list of nodes in way
+  # 6. hash of way tags
+  # 7. array of nodes to change (each one is [lon,lat,id,version,tags])
+  # 
   # Returns:
   # 0. '0' (code for success),
   # 1. original way id (unchanged),
   # 2. new way id,
-  # 3. hash of renumbered nodes (old id=>new id)
+  # 3. hash of renumbered nodes (old id=>new id),
+  # 4. way version,
+  # 5. hash of node versions (node=>version)
 
-  def putway(renumberednodes, usertoken, originalway, points, attributes) #:doc:
+  def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes) #:doc:
 
-       # -- Initialise and carry out checks
+    # -- Initialise
        
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be saved." end
-
-       originalway = originalway.to_i
-
-       points.each do |a|
-         if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
-         if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
-       end
-
-       if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
-
-       # -- Get unique nodes
-
-       if originalway < 0
-         way = Way.new
-         uniques = []
-       else
-         way = Way.find(originalway)
-         uniques = way.unshared_node_ids
-       end
-
-       # -- Compare nodes and save changes to any that have changed
-
-       nodes = []
-
-       points.each do |n|
-         lon = n[0].to_f
-         lat = n[1].to_f
-         id = n[2].to_i
-         savenode = false
-
-         if renumberednodes[id]
-           id = renumberednodes[id]
-         elsif id < 0
-               # Create new node
-               node = Node.new
-               savenode = true
-         else
-               node = Node.find(id)
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
-                  n[4] != nodetags or !node.visible?
-                 savenode = true
-               end
-         end
-
-         if savenode
-               node.user_id = uid
-           node.lat = lat
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the way could not be saved." end
+    if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
+
+    originalway = originalway.to_i
+       pointlist.collect! {|a| a.to_i }
+
+    way=nil    # this is returned, so scope it outside the transaction
+    nodeversions = {}
+    Way.transaction do
+
+      # -- Get unique nodes
+
+      if originalway <= 0
+        uniques = []
+      else
+        way = Way.find(originalway)
+        uniques = way.unshared_node_ids
+      end
+
+      #-- Update each changed node
+
+      nodes.each do |a|
+        lon = a[0].to_f
+        lat = a[1].to_f
+        id = a[2].to_i
+        version = a[3].to_i
+        if id == 0  then return -2,"Server error - node with id 0 found in way #{originalway}." end
+        if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end
+        if renumberednodes[id] then id = renumberednodes[id] end
+
+        node = Node.new
+        node.changeset_id = changeset_id
+        node.lat = lat
         node.lon = lon
-           node.tags = Tags.join(n[4])
-           node.visible = true
-           node.save_with_history!
-
-               if id != node.id
-                 renumberednodes[id] = node.id
-                 id = node.id
-           end
-         end
-
-         uniques = uniques - [id]
-         nodes.push(id)
-       end
-
-       # -- Save revised way
-
-       way.tags = attributes
-       way.nds = nodes
-       way.user_id = uid
-       way.visible = true
-       way.save_with_history!
-
-       # -- Delete any unique nodes
-       
-       uniques.each do |n|
-         deleteitemrelations(n, 'node')
-
-         node = Node.find(n)
-         node.user_id = uid
-         node.visible = false
-         node.save_with_history!
-       end
-
-       [0, originalway, way.id, renumberednodes]
+        node.tags = a[4]
+        node.tags.delete('created_by')
+        node.version = version
+        if id <= 0
+          # We're creating the node
+          node.create_with_history(user)
+          renumberednodes[id] = node.id
+          nodeversions[node.id] = node.version
+        else
+          # We're updating an existing node
+          previous=Node.find(id)
+          previous.update_from(node, user)
+          nodeversions[previous.id] = previous.version
+        end
+      end
+
+      # -- Save revised way
+
+         pointlist.collect! {|a|
+               renumberednodes[a] ? renumberednodes[a]:a
+         } # renumber nodes
+      new_way = Way.new
+      new_way.tags = attributes
+      new_way.nds = pointlist
+      new_way.changeset_id = changeset_id
+      new_way.version = wayversion
+      if originalway <= 0
+        new_way.create_with_history(user)
+        way=new_way    # so we can get way.id and way.version
+      elsif way.tags!=attributes or way.nds!=pointlist or !way.visible?
+        way.update_from(new_way, user)
+      end
+
+      # -- Delete any unique nodes no longer used
+
+      uniques=uniques-pointlist
+      uniques.each do |n|
+        node = Node.find(n)
+        deleteitemrelations(user, changeset_id, id, 'node', node.version)
+        new_node = Node.new
+        new_node.changeset_id = changeset_id
+        new_node.version = node.version
+        node.delete_with_history!(new_node, user)
+      end
+
+    end # transaction
+
+    [0, originalway, way.id, renumberednodes, way.version, nodeversions]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-2, "Sorry, your changeset #{ex.changeset.id} has been closed (at #{ex.changeset.closed_at})."]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APITooManyWayNodesError => ex
+    return [-1, "You have tried to upload a really long way with #{ex.provided} points: only #{ex.max} are allowed."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The point has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-(."]
   end
 
   # Save POI to the database.
   # Refuses save if the node has since become part of a way.
-  # Returns:
+  # Returns array with:
   # 0. 0 (success),
   # 1. original node id (unchanged),
-  # 2. new node id.
-
-  def putpoi(usertoken, id, lon, lat, tags, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the point could not be saved." end
-
-       id = id.to_i
-       visible = (visible.to_i == 1)
-
-       if id > 0 then
-         node = Node.find(id)
-
-         if !visible then
-           unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
-           deleteitemrelations(id, 'node')
-         end
-       else
-         node = Node.new
-       end
-
-       node.user_id = uid
-       node.lat = lat
-       node.lon = lon
-       node.tags = Tags.join(tags)
-       node.visible = visible
-       node.save_with_history!
-
-       [0, id, node.id]
+  # 2. new node id,
+  # 3. version.
+
+  def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) #:doc:
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the point could not be saved." end
+
+    id = id.to_i
+    visible = (visible.to_i == 1)
+    node = nil
+    new_node = nil
+    Node.transaction do
+      if id > 0 then
+        node = Node.find(id)
+
+        if !visible then
+          unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
+        end
+      end
+      # We always need a new node, based on the data that has been sent to us
+      new_node = Node.new
+
+      new_node.changeset_id = changeset_id
+      new_node.version = version
+      new_node.lat = lat
+      new_node.lon = lon
+      new_node.tags = tags
+      if id <= 0 
+        # We're creating the node
+        new_node.create_with_history(user)
+      elsif visible
+        # We're updating the node
+        node.update_from(new_node, user)
+      else
+        # We're deleting the node
+        node.delete_with_history!(new_node, user)
+      end
+     end # transaction
+
+    if id <= 0
+      return [0, id, new_node.id, new_node.version]
+    else
+      return [0, id, node.id, node.version]
+    end 
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this point since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The point has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
   # Read POI from database
   # (only called on revert: POIs are usually read by whichways).
   #
-  # Returns array of id, long, lat, hash of tags.
+  # Returns array of id, long, lat, hash of tags, version.
 
   def getpoi(id,timestamp) #:doc:
-       if timestamp>0 then
-         n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp])
-       else
-         n = Node.find(id)
-       end
-
-       if n
-         return [n.id, n.lon, n.lat, n.tags_as_hash]
-       else
-         return [nil, nil, nil, '']
-       end
+    if timestamp == '' then
+      n = Node.find(id)
+    else
+      n = OldNode.find(id, :conditions=>['timestamp=?',DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")])
+    end
+
+    if n
+      return [n.id, n.lon, n.lat, n.tags, n.version]
+    else
+      return [nil, nil, nil, {}, nil]
+    end
   end
 
   # Delete way and all constituent nodes. Also removes from any relations.
+  # Params:
+  # * The user token
+  # * the changeset id
+  # * the id of the way to change
+  # * the version of the way that was downloaded
+  # * a hash of the id and versions of all the nodes that are in the way, if any 
+  # of the nodes have been changed by someone else then, there is a problem!
   # Returns 0 (success), unchanged way id.
 
-  def deleteway(usertoken, way_id) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be deleted." end
-
-       # FIXME: would be good not to make two history entries when removing
-       #                two nodes from the same relation
-       user = User.find(uid)
-       way = Way.find(way_id)
-       way.unshared_node_ids.each do |n|
-         deleteitemrelations(n, 'node')
-       end
-       deleteitemrelations(way_id, 'way')
-
-       way.delete_with_relations_and_nodes_and_history(user)  
-
-       [0, way_id]
+  def deleteway(usertoken, changeset_id, way_id, way_version, node_id_version) #:doc:
+    user = getuser(usertoken)
+    unless user then return -1,"You are not logged in, so the way could not be deleted." end
+      
+    way_id = way_id.to_i
+    # Need a transaction so that if one item fails to delete, the whole delete fails.
+    Way.transaction do
+
+      # delete the way
+      old_way = Way.find(way_id)
+      delete_way = Way.new
+      delete_way.version = way_version
+      delete_way.changeset_id = changeset_id
+      old_way.delete_with_history!(delete_way, user)
+
+      old_way.unshared_node_ids.each do |node_id|
+        # delete the node
+        node = Node.find(node_id)
+        delete_node = Node.new
+        delete_node.changeset_id = changeset_id
+        if node_id_version[node_id.to_s]
+          delete_node.version = node_id_version[node_id.to_s]
+        else
+          # in case the node wasn't passed (i.e. if it was previously removed
+          # from the way in Potlatch)
+          deleteitemrelations(user, changeset_id, node_id, 'node', node.version)
+             delete_node.version = node.version
+           end
+        node.delete_with_history!(delete_node, user)
+      end
+    end # transaction
+    [0, way_id]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The way has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-( ."]
   end
 
 
@@ -500,141 +782,147 @@ class AmfController < ApplicationController
   # Support functions
 
   # Remove a node or way from all relations
+  # This is only used by putway and deleteway when deleting nodes removed 
+  # from a way (because Potlatch itself doesn't keep track of these - 
+  # possible FIXME).
 
-  def deleteitemrelations(objid, type) #:doc:
-       relations = RelationMember.find(:all, 
+  def deleteitemrelations(user, changeset_id, objid, type, version) #:doc:
+    relations = RelationMember.find(:all, 
                                                                        :conditions => ['member_type = ? and member_id = ?', type, objid], 
                                                                        :include => :relation).collect { |rm| rm.relation }.uniq
 
-       relations.each do |rel|
-         rel.members.delete_if { |x| x[0] == type and x[1] == objid }
-         rel.save_with_history!
-       end
-  end
-
-  # Break out node tags into a hash
-  # (should become obsolete as of API 0.6)
-
-  def tagstring_to_hash(a) #:doc:
-       tags={}
-       Tags.split(a) do |k, v|
-         tags[k]=v
-       end
-       tags
+    relations.each do |rel|
+      rel.members.delete_if { |x| x[0] == type and x[1] == objid }
+      new_rel = Relation.new
+      new_rel.tags = rel.tags
+      new_rel.visible = rel.visible
+      new_rel.version = rel.version
+      new_rel.members = rel.members
+      new_rel.changeset_id = changeset_id
+      rel.update_from(new_rel, user)
+    end
   end
 
   # Authenticate token
-  # (could be removed if no-one uses the username+password form)
-
-  def getuserid(token) #:doc:
-       if (token =~ /^(.+)\+(.+)$/) then
-         user = User.authenticate(:username => $1, :password => $2)
-       else
-         user = User.authenticate(:token => token)
-       end
-
-       return user ? user.id : nil;
-  end
-
-  # Compare two floating-point numbers to within 0.0000001
-
-  def fpcomp(a,b) #:doc:
-       return ((a/0.0000001).round==(b/0.0000001).round)
+  # (can also be of form user:pass)
+  # When we are writing to the api, we need the actual user model, 
+  # not just the id, hence this abstraction
+
+  def getuser(token) #:doc:
+    if (token =~ /^(.+)\:(.+)$/) then
+      user = User.authenticate(:username => $1, :password => $2)
+    else
+      user = User.authenticate(:token => token)
+    end
+    return user
   end
 
   # Send AMF response
   
   def sendresponse(results)
-       a,b=results.length.divmod(256)
-       render :content_type => "application/x-amf", :text => proc { |response, output| 
-         output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
-         results.each do |k,v|
-               output.write(v)
-         end
-       }
+    a,b=results.length.divmod(256)
+    render :content_type => "application/x-amf", :text => proc { |response, output| 
+      # ** move amf writing loop into here - 
+      # basically we read the messages in first (into an array of some sort),
+      # then iterate through that array within here, and do all the AMF writing
+      output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
+      results.each do |k,v|
+        output.write(v)
+      end
+    }
   end
 
 
   # ====================================================================
   # Alternative SQL queries for getway/whichways
 
-  def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-  SELECT DISTINCT current_way_nodes.id AS wayid
-               FROM current_way_nodes
-  INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
-  INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
-          WHERE current_nodes.visible=1 
-                AND current_ways.visible=1 
-                AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
+  def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
+    sql=<<-EOF
+    SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
+      FROM current_way_nodes
+    INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
+    INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
+       WHERE current_nodes.visible=TRUE 
+       AND current_ways.visible=TRUE 
+       AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
+    EOF
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
   end
        
   def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags 
+    pois=[]
+    sql=<<-EOF
+                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
                        FROM current_nodes 
- LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
-                  WHERE current_nodes.visible=1
      LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
+                  WHERE current_nodes.visible=TRUE
                         AND cwn.id IS NULL
                         AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |n| [n['id'].to_i,n['lon'].to_f,n['lat'].to_f,tagstring_to_hash(n['tags'])] }
+    EOF
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      poitags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        poitags[n['k']]=n['v']
+      end
+      pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
+    end
+    pois
   end
        
   def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
-       # ** It would be more Potlatchy to get relations for nodes within ways
-       #    during 'getway', not here
-       sql=<<-EOF
-         SELECT DISTINCT cr.id AS relid 
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id 
-  INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
-          WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
-       EOF
-       unless way_ids.empty?
-         sql+=<<-EOF
-          UNION
-         SELECT DISTINCT cr.id AS relid
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id
-          WHERE crm.member_type='way' 
-                AND crm.member_id IN (#{way_ids.join(',')})
-         EOF
-       end
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
+    # ** It would be more Potlatchy to get relations for nodes within ways
+    #    during 'getway', not here
+    sql=<<-EOF
+      SELECT DISTINCT cr.id AS relid,cr.version AS version 
+      FROM current_relations cr
+      INNER JOIN current_relation_members crm ON crm.id=cr.id 
+      INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
+       WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
+      EOF
+    unless way_ids.empty?
+      sql+=<<-EOF
+       UNION
+        SELECT DISTINCT cr.id AS relid,cr.version AS version
+        FROM current_relations cr
+        INNER JOIN current_relation_members crm ON crm.id=cr.id
+         WHERE crm.member_type='way' 
+         AND crm.member_id IN (#{way_ids.join(',')})
+        EOF
+    end
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
   end
        
   def sql_get_nodes_in_way(wayid)
-       points=[]
-       sql=<<-EOF
-               SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags 
-                 FROM current_way_nodes,current_nodes 
-                WHERE current_way_nodes.id=#{wayid.to_i} 
+    points=[]
+    sql=<<-EOF
+      SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id 
+      FROM current_way_nodes,current_nodes 
+       WHERE current_way_nodes.id=#{wayid.to_i} 
                   AND current_way_nodes.node_id=current_nodes.id 
-                  AND current_nodes.visible=1
-         ORDER BY sequence_id
+                  AND current_nodes.visible=TRUE
+      ORDER BY sequence_id
          EOF
-       ActiveRecord::Base.connection.select_all(sql).each do |row|
-         nodetags=tagstring_to_hash(row['tags'])
-         nodetags.delete('created_by')
-         points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
-       end
-       points
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      nodetags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        nodetags[n['k']]=n['v']
+      end
+      nodetags.delete('created_by')
+      points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
+    end
+    points
   end
        
   def sql_get_tags_in_way(wayid)
-       tags={}
-       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
-         tags[row['k']]=row['v']
-       end
-       tags
+    tags={}
+    ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
+      tags[row['k']]=row['v']
+    end
+    tags
   end
 
+  def sql_get_way_version(wayid)
+    ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")
+  end
 end
 
-# Local Variables:
-# indent-tabs-mode: t
-# tab-width: 4
-# End:
index 7a0a45f..cae5102 100644 (file)
@@ -11,12 +11,13 @@ class ApiController < ApplicationController
   @@count = COUNT
 
   # The maximum area you're allowed to request, in square degrees
-  MAX_REQUEST_AREA = 0.25
+  MAX_REQUEST_AREA = APP_CONFIG['max_request_area']
 
   # Number of GPS trace/trackpoints returned per-page
-  TRACEPOINTS_PER_PAGE = 5000
+  TRACEPOINTS_PER_PAGE = APP_CONFIG['tracepoints_per_page']
 
-  
+  # Get an XML response containing a list of tracepoints that have been uploaded
+  # within the specified bounding box, and in the specified page.
   def trackpoints
     @@count+=1
     #retrieve the page number
@@ -84,6 +85,15 @@ class ApiController < ApplicationController
     render :text => doc.to_s, :content_type => "text/xml"
   end
 
+  # This is probably the most common call of all. It is used for getting the 
+  # OSM data for a specified bounding box, usually for editing. First the
+  # bounding box (bbox) is checked to make sure that it is sane. All nodes 
+  # are searched, then all the ways that reference those nodes are found.
+  # All Nodes that are referenced by those ways are fetched and added to the list
+  # of nodes.
+  # Then all the relations that reference the already found nodes and ways are
+  # fetched. All the nodes and ways that are referenced by those ways are then 
+  # fetched. Finally all the xml is returned.
   def map
     GC.start
     @@count+=1
@@ -109,18 +119,19 @@ class ApiController < ApplicationController
       return
     end
 
-    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1", :limit => APP_CONFIG['max_number_of_nodes']+1)
+    # FIXME um why is this area using a different order for the lat/lon from above???
+    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => {:visible => true}, :limit => APP_CONFIG['max_number_of_nodes']+1)
     # get all the nodes, by tag not yet working, waiting for change from NickB
     # need to be @nodes (instance var) so tests in /spec can be performed
     #@nodes = Node.search(bbox, params[:tag])
 
     node_ids = @nodes.collect(&:id)
     if node_ids.length > APP_CONFIG['max_number_of_nodes']
-      report_error("You requested too many nodes (limit is 50,000). Either request a smaller area, or use planet.osm")
+      report_error("You requested too many nodes (limit is #{APP_CONFIG['max_number_of_nodes']}). Either request a smaller area, or use planet.osm")
       return
     end
     if node_ids.length == 0
-      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
+      render :text => "<osm version='#{API_VERSION}' generator='#{GENERATOR}'></osm>", :content_type => "text/xml"
       return
     end
 
@@ -176,15 +187,15 @@ class ApiController < ApplicationController
       end
     end 
 
-    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => "visible = 1") +
-                Relation.find_for_ways(way_ids, :conditions => "visible = 1")
+    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => {:visible => true}) +
+                Relation.find_for_ways(way_ids, :conditions => {:visible => true})
 
     # we do not normally return the "other" partners referenced by an relation, 
     # e.g. if we return a way A that is referenced by relation X, and there's 
     # another way B also referenced, that is not returned. But we do make 
     # an exception for cases where an relation references another *relation*; 
     # in that case we return that as well (but we don't go recursive here)
-    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => "visible = 1")
+    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => {:visible => true})
 
     # this "uniq" may be slightly inefficient; it may be better to first collect and output
     # all node-related relations, then find the *not yet covered* way-related ones etc.
@@ -204,6 +215,8 @@ class ApiController < ApplicationController
     end
   end
 
+  # Get a list of the tiles that have changed within a specified time
+  # period
   def changes
     zoom = (params[:zoom] || '12').to_i
 
@@ -217,7 +230,7 @@ class ApiController < ApplicationController
     end
 
     if zoom >= 1 and zoom <= 16 and
-       endtime >= starttime and endtime - starttime <= 24.hours
+       endtime > starttime and endtime - starttime <= 24.hours
       mask = (1 << zoom) - 1
 
       tiles = Node.count(:conditions => ["timestamp BETWEEN ? AND ?", starttime, endtime],
@@ -245,21 +258,32 @@ class ApiController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", :status => :bad_request
     end
   end
 
+  # External apps that use the api are able to query the api to find out some 
+  # parameters of the API. It currently returns: 
+  # * minimum and maximum API versions that can be used.
+  # * maximum area that can be requested in a bbox request in square degrees
+  # * number of tracepoints that are returned in each tracepoints page
   def capabilities
     doc = OSM::API.new.get_xml_doc
 
     api = XML::Node.new 'api'
     version = XML::Node.new 'version'
-    version['minimum'] = '0.5';
-    version['maximum'] = '0.5';
+    version['minimum'] = "#{API_VERSION}";
+    version['maximum'] = "#{API_VERSION}";
     api << version
     area = XML::Node.new 'area'
     area['maximum'] = MAX_REQUEST_AREA.to_s;
     api << area
+    tracepoints = XML::Node.new 'tracepoints'
+    tracepoints['per_page'] = APP_CONFIG['tracepoints_per_page'].to_s
+    api << tracepoints
+    waynodes = XML::Node.new 'waynodes'
+    waynodes['maximum'] = APP_CONFIG['max_number_of_way_nodes'].to_s
+    api << waynodes
     
     doc.root << api
 
index ce13a6a..f5ea006 100644 (file)
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
 
   def authorize_web
     if session[:user]
-      @user = User.find(session[:user], :conditions => "visible = 1")
+      @user = User.find(session[:user], :conditions => {:visible => true})
     elsif session[:token]
       @user = User.authenticate(:token => session[:token])
       session[:user] = @user.id
@@ -22,7 +22,11 @@ class ApplicationController < ActionController::Base
     redirect_to :controller => 'user', :action => 'login', :referer => request.request_uri unless @user
   end
 
-  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+  ##
+  # sets up the @user object for use by other methods. this is mostly called
+  # from the authorize method, but can be called elsewhere if authorisation
+  # is optional.
+  def setup_user_auth
     username, passwd = get_auth_data # parse from headers
     # authenticate per-scheme
     if username.nil?
@@ -32,6 +36,11 @@ class ApplicationController < ActionController::Base
     else
       @user = User.authenticate(:username => username, :password => passwd) # basic auth
     end
+  end
+
+  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+    # make the @user object from any auth sources we have
+    setup_user_auth
 
     # handle authenticate pass/fail
     unless @user
@@ -71,7 +80,7 @@ class ApplicationController < ActionController::Base
   #  phrase from that, we can also put the error message into the status
   #  message. For now, rails won't let us)
   def report_error(message)
-    render :nothing => true, :status => :bad_request
+    render :text => message, :status => :bad_request
     # Todo: some sort of escaping of problem characters in the message
     response.headers['Error'] = message
   end
@@ -82,6 +91,8 @@ private
   def get_auth_data 
     if request.env.has_key? 'X-HTTP_AUTHORIZATION'          # where mod_rewrite might have put it 
       authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split 
+    elsif request.env.has_key? 'REDIRECT_X_HTTP_AUTHORIZATION'          # mod_fcgi 
+      authdata = request.env['REDIRECT_X_HTTP_AUTHORIZATION'].to_s.split 
     elsif request.env.has_key? 'HTTP_AUTHORIZATION'         # regular location
       authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
     end 
index f3a0451..237c57a 100644 (file)
@@ -8,7 +8,7 @@ class BrowseController < ApplicationController
   end
   
   def index
-    @nodes = Node.find(:all, :order => "timestamp DESC", :limit=> 20)  
+    @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20)
   end
   
   def relation 
@@ -17,14 +17,15 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation | ' + (@name)
       @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
       @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -34,12 +35,13 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -49,14 +51,15 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way | ' + (@name)
       @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
       @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -66,12 +69,13 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
 
@@ -81,14 +85,15 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node | ' + (@name)
       @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
       @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "node"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -98,12 +103,29 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
+    end
+  end
+  
+  def changeset
+    begin
+      @changeset = Changeset.find(params[:id])
+      @node_pages, @nodes = paginate(:old_nodes, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'node_page')
+      @way_pages, @ways = paginate(:old_ways, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'way_page')
+      @relation_pages, @relations = paginate(:old_relations, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'relation_page')
+      
+      @title = "Changeset | #{@changeset.id}"
+      @next = Changeset.find(:first, :order => "id ASC", :conditions => [ "id > :id", { :id => @changeset.id }] ) 
+      @prev = Changeset.find(:first, :order => "id DESC", :conditions => [ "id < :id", { :id => @changeset.id }] ) 
+    rescue ActiveRecord::RecordNotFound
+      @type = "changeset"
+      render :action => "not_found", :status => :not_found
     end
   end
 end
diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb
new file mode 100644 (file)
index 0000000..b2ff427
--- /dev/null
@@ -0,0 +1,408 @@
+# The ChangesetController is the RESTful interface to Changeset objects
+
+class ChangesetController < ApplicationController
+  layout 'site'
+  require 'xml/libxml'
+
+  session :off, :except => [:list]
+  before_filter :authorize_web, :only => [:list]
+  before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close]
+  before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include]
+  before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download, :query]
+  after_filter :compress_output
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  # Helper methods for checking consistency
+  include ConsistencyValidations
+
+  # Create a changeset from XML.
+  def create
+    if request.put?
+      cs = Changeset.from_xml(request.raw_post, true)
+
+      if cs
+        cs.user_id = @user.id
+        cs.save_with_tags!
+        render :text => cs.id.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+  end
+
+  ##
+  # Return XML giving the basic info about the changeset. Does not 
+  # return anything about the nodes, ways and relations in the changeset.
+  def read
+    begin
+      changeset = Changeset.find(params[:id])
+      render :text => changeset.to_xml.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    end
+  end
+  
+  ##
+  # marks a changeset as closed. this may be called multiple times
+  # on the same changeset, so is idempotent.
+  def close 
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])    
+    check_changeset_consistency(changeset, @user)
+
+    # to close the changeset, we'll just set its closed_at time to
+    # now. this might not be enough if there are concurrency issues, 
+    # but we'll have to wait and see.
+    changeset.set_closed_time_now
+
+    changeset.save!
+    render :nothing => true
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # insert a (set of) points into a changeset bounding box. this can only
+  # increase the size of the bounding box. this is a hint that clients can
+  # set either before uploading a large number of changes, or changes that
+  # the client (but not the server) knows will affect areas further away.
+  def expand_bbox
+    # only allow POST requests, because although this method is
+    # idempotent, there is no "document" to PUT really...
+    if request.post?
+      cs = Changeset.find(params[:id])
+      check_changeset_consistency(cs, @user)
+
+      # keep an array of lons and lats
+      lon = Array.new
+      lat = Array.new
+
+      # the request is in pseudo-osm format... this is kind-of an
+      # abuse, maybe should change to some other format?
+      doc = XML::Parser.string(request.raw_post).parse
+      doc.find("//osm/node").each do |n|
+        lon << n['lon'].to_f * GeoRecord::SCALE
+        lat << n['lat'].to_f * GeoRecord::SCALE
+      end
+
+      # add the existing bounding box to the lon-lat array
+      lon << cs.min_lon unless cs.min_lon.nil?
+      lat << cs.min_lat unless cs.min_lat.nil?
+      lon << cs.max_lon unless cs.max_lon.nil?
+      lat << cs.max_lat unless cs.max_lat.nil?
+
+      # collapse the arrays to minimum and maximum
+      cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
+        lon.min, lat.min, lon.max, lat.max
+
+      # save the larger bounding box and return the changeset, which
+      # will include the bigger bounding box.
+      cs.save!
+      render :text => cs.to_xml.to_s, :content_type => "text/xml"
+
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # Upload a diff in a single transaction.
+  #
+  # This means that each change within the diff must succeed, i.e: that
+  # each version number mentioned is still current. Otherwise the entire
+  # transaction *must* be rolled back.
+  #
+  # Furthermore, each element in the diff can only reference the current
+  # changeset.
+  #
+  # Returns: a diffResult document, as described in 
+  # http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
+  def upload
+    # only allow POST requests, as the upload method is most definitely
+    # not idempotent, as several uploads with placeholder IDs will have
+    # different side-effects.
+    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
+    unless request.post?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+
+    changeset = Changeset.find(params[:id])
+    check_changeset_consistency(changeset, @user)
+    
+    diff_reader = DiffReader.new(request.raw_post, changeset)
+    Changeset.transaction do
+      result = diff_reader.commit
+      render :text => result.to_s, :content_type => "text/xml"
+    end
+    
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # download the changeset as an osmChange document.
+  #
+  # to make it easier to revert diffs it would be better if the osmChange
+  # format were reversible, i.e: contained both old and new versions of 
+  # modified elements. but it doesn't at the moment...
+  #
+  # this method cannot order the database changes fully (i.e: timestamp and
+  # version number may be too coarse) so the resulting diff may not apply
+  # to a different database. however since changesets are not atomic this 
+  # behaviour cannot be guaranteed anyway and is the result of a design
+  # choice.
+  def download
+    changeset = Changeset.find(params[:id])
+    
+    # get all the elements in the changeset and stick them in a big array.
+    elements = [changeset.old_nodes, 
+                changeset.old_ways, 
+                changeset.old_relations].flatten
+    
+    # sort the elements by timestamp and version number, as this is the 
+    # almost sensible ordering available. this would be much nicer if 
+    # global (SVN-style) versioning were used - then that would be 
+    # unambiguous.
+    elements.sort! do |a, b| 
+      if (a.timestamp == b.timestamp)
+        a.version <=> b.version
+      else
+        a.timestamp <=> b.timestamp 
+      end
+    end
+    
+    # create an osmChange document for the output
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "osmChange"
+
+    # generate an output element for each operation. note: we avoid looking
+    # at the history because it is simpler - but it would be more correct to 
+    # check these assertions.
+    elements.each do |elt|
+      result.root <<
+        if (elt.version == 1) 
+          # first version, so it must be newly-created.
+          created = XML::Node.new "create"
+          created << elt.to_xml_node
+        else
+          # get the previous version from the element history
+          prev_elt = elt.class.find(:first, :conditions => 
+                                    ['id = ? and version = ?',
+                                     elt.id, elt.version])
+          unless elt.visible
+            # if the element isn't visible then it must have been deleted, so
+            # output the *previous* XML
+            deleted = XML::Node.new "delete"
+            deleted << prev_elt.to_xml_node
+          else
+            # must be a modify, for which we don't need the previous version
+            # yet...
+            modified = XML::Node.new "modify"
+            modified << elt.to_xml_node
+          end
+        end
+    end
+
+    render :text => result.to_s, :content_type => "text/xml"
+            
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # query changesets by bounding box, time, user or open/closed status.
+  def query
+    # create the conditions that the user asked for. some or all of
+    # these may be nil.
+    conditions = conditions_bbox(params['bbox'])
+    conditions = cond_merge conditions, conditions_user(params['user'])
+    conditions = cond_merge conditions, conditions_time(params['time'])
+    conditions = cond_merge conditions, conditions_open(params['open'])
+
+    # create the results document
+    results = OSM::API.new.get_xml_doc
+
+    # add all matching changesets to the XML results document
+    Changeset.find(:all, 
+                   :conditions => conditions, 
+                   :limit => 100,
+                   :order => 'created_at desc').each do |cs|
+      results.root << cs.to_xml_node
+    end
+
+    render :text => results.to_s, :content_type => "text/xml"
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+  
+  ##
+  # updates a changeset's tags. none of the changeset's attributes are
+  # user-modifiable, so they will be ignored.
+  #
+  # changesets are not (yet?) versioned, so we don't have to deal with
+  # history tables here. changesets are locked to a single user, however.
+  #
+  # after succesful update, returns the XML of the changeset.
+  def update
+    # request *must* be a PUT.
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])
+    new_changeset = Changeset.from_xml(request.raw_post)
+
+    unless new_changeset.nil?
+      check_changeset_consistency(changeset, @user)
+      changeset.update_from(new_changeset, @user)
+      render :text => changeset.to_xml, :mime_type => "text/xml"
+    else
+      
+      render :nothing => true, :status => :bad_request
+    end
+      
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # list edits belonging to a user
+  def list
+    user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]])
+    @edit_pages, @edits = paginate(:changesets,
+                                   :include => [:user, :changeset_tags],
+                                   :conditions => ["changesets.user_id = ? AND min_lat IS NOT NULL", user.id],
+                                   :order => "changesets.created_at DESC",
+                                   :per_page => 20)
+    
+    @action = 'list'
+    @display_name = user.display_name
+    # FIXME needs rescues in here
+  end
+
+private
+  #------------------------------------------------------------
+  # utility functions below.
+  #------------------------------------------------------------  
+
+  ##
+  # merge two conditions
+  def cond_merge(a, b)
+    if a and b
+      a_str = a.shift
+      b_str = b.shift
+      return [ a_str + " and " + b_str ] + a + b
+    elsif a 
+      return a
+    else b
+      return b
+    end
+  end
+
+  ##
+  # if a bounding box was specified then parse it and do some sanity 
+  # checks. this is mostly the same as the map call, but without the 
+  # area restriction.
+  def conditions_bbox(bbox)
+    unless bbox.nil?
+      raise OSM::APIBadUserInput.new("Bounding box should be min_lon,min_lat,max_lon,max_lat") unless bbox.count(',') == 3
+      bbox = sanitise_boundaries(bbox.split(/,/))
+      raise OSM::APIBadUserInput.new("Minimum longitude should be less than maximum.") unless bbox[0] <= bbox[2]
+      raise OSM::APIBadUserInput.new("Minimum latitude should be less than maximum.") unless bbox[1] <= bbox[3]
+      return ['min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?',
+              bbox[2] * GeoRecord::SCALE, bbox[0] * GeoRecord::SCALE, bbox[3]* GeoRecord::SCALE, bbox[1] * GeoRecord::SCALE]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changesets to those by a particular user
+  def conditions_user(user)
+    unless user.nil?
+      # user input checking, we don't have any UIDs < 1
+      raise OSM::APIBadUserInput.new("invalid user ID") if user.to_i < 1
+
+      u = User.find(user.to_i)
+      # should be able to get changesets of public users only, or 
+      # our own changesets regardless of public-ness.
+      unless u.data_public?
+        # get optional user auth stuff so that users can see their own
+        # changesets if they're non-public
+        setup_user_auth
+        
+        raise OSM::APINotFoundError if @user.nil? or @user.id != u.id
+      end
+      return ['user_id = ?', u.id]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changes to those during a particular time period
+  def conditions_time(time) 
+    unless time.nil?
+      # if there is a range, i.e: comma separated, then the first is 
+      # low, second is high - same as with bounding boxes.
+      if time.count(',') == 1
+        # check that we actually have 2 elements in the array
+        times = time.split(/,/)
+        raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 
+
+        from, to = times.collect { |t| DateTime.parse(t) }
+        return ['closed_at >= ? and created_at <= ?', from, to]
+      else
+        # if there is no comma, assume its a lower limit on time
+        return ['closed_at >= ?', DateTime.parse(time)]
+      end
+    else
+      return nil
+    end
+    # stupid DateTime seems to throw both of these for bad parsing, so
+    # we have to catch both and ensure the correct code path is taken.
+  rescue ArgumentError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  rescue RuntimeError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  end
+
+  ##
+  # restrict changes to those which are open
+  #
+  # at the moment this code assumes we're only interested in open
+  # changesets and gives no facility to query closed changesets. this
+  # would be reasonably simple to implement if anyone actually wants
+  # it?
+  def conditions_open(open)
+    return open.nil? ? nil : ['closed_at >= ? and num_changes <= ?', 
+                              DateTime.now, Changeset::MAX_ELEMENTS]
+  end
+
+end
diff --git a/app/controllers/changeset_tag_controller.rb b/app/controllers/changeset_tag_controller.rb
new file mode 100644 (file)
index 0000000..3e8db3f
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangesetTagController < ApplicationController
+  layout 'site'
+
+  def search
+    @tags = ChangesetTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", params[:query][:query].to_s] )
+  end
+
+
+end
index e0d6e44..3592ccb 100644 (file)
@@ -38,6 +38,8 @@ class DiaryEntryController < ApplicationController
         redirect_to :controller => 'diary_entry', :action => 'view', :id => params[:id]
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => "no_such_entry", :status => :not_found
   end
 
   def comment
@@ -54,7 +56,7 @@ class DiaryEntryController < ApplicationController
 
   def list
     if params[:display_name]
-      @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if @this_user
         @title = @this_user.display_name + "'s diary"
@@ -70,7 +72,7 @@ class DiaryEntryController < ApplicationController
     else
       @title = "Users' diaries"
       @entry_pages, @entries = paginate(:diary_entries, :include => :user,
-                                        :conditions => "users.visible = 1",
+                                        :conditions => ["users.visible = ?", true],
                                         :order => 'created_at DESC',
                                         :per_page => 20)
     end
@@ -78,13 +80,13 @@ class DiaryEntryController < ApplicationController
 
   def rss
     if params[:display_name]
-      user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if user
         @entries = DiaryEntry.find(:all, :conditions => ['user_id = ?', user.id], :order => 'created_at DESC', :limit => 20)
         @title = "OpenStreetMap diary entries for #{user.display_name}"
         @description = "Recent OpenStreetmap diary entries from #{user.display_name}"
-        @link = "http://www.openstreetmap.org/user/#{user.display_name}/diary"
+        @link = "http://#{SERVER_URL}/user/#{user.display_name}/diary"
 
         render :content_type => Mime::RSS
       else
@@ -92,21 +94,22 @@ class DiaryEntryController < ApplicationController
       end
     else
       @entries = DiaryEntry.find(:all, :include => :user,
-                                 :conditions => "users.visible = 1",
+                                 :conditions => ["users.visible = ?", true],
                                  :order => 'created_at DESC', :limit => 20)
       @title = "OpenStreetMap diary entries"
       @description = "Recent diary entries from users of OpenStreetMap"
-      @link = "http://www.openstreetmap.org/diary"
+      @link = "http://#{SERVER_URL}/diary"
 
       render :content_type => Mime::RSS
     end
   end
 
   def view
-    user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if user
       @entry = DiaryEntry.find(:first, :conditions => ['user_id = ? AND id = ?', user.id, params[:id]])
+      @title = "Users' diaries | #{params[:display_name]}"
     else
       @not_found_user = params[:display_name]
 
index 85c0ac3..c039fe6 100644 (file)
@@ -4,11 +4,16 @@ class MessageController < ApplicationController
   before_filter :authorize_web
   before_filter :require_user
 
+  # Allow the user to write a new message to another user. This action also 
+  # deals with the sending of that message to the other user when the user
+  # clicks send.
+  # The user_id param is the id of the user that the message is being sent to.
   def new
     @title = 'send message'
+    @to_user = User.find(params[:user_id])
     if params[:message]
       @message = Message.new(params[:message])
-      @message.to_user_id = params[:user_id]
+      @message.to_user_id = @to_user.id
       @message.from_user_id = @user.id
       @message.sent_on = Time.now
    
@@ -20,27 +25,33 @@ class MessageController < ApplicationController
     else
       @title = params[:title]
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Allow the user to reply to another message.
   def reply
     message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
     @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" 
     @title = "Re: #{message.title.sub(/^Re:\s*/, '')}"
     @user_id = message.from_user_id
+    @to_user = User.find(message.to_user_id)
     render :action => 'new'
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Show a message
   def read
     @title = 'read message'
     @message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
-    @message.message_read = 1 if @message.to_user_id == @user.id
+    @message.message_read = true if @message.to_user_id == @user.id
     @message.save
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Display the list of messages that have been sent to the user.
   def inbox
     @title = 'inbox'
     if @user and params[:display_name] == @user.display_name
@@ -49,6 +60,7 @@ class MessageController < ApplicationController
     end
   end
 
+  # Display the list of messages that the user has sent to other users.
   def outbox
     @title = 'outbox'
     if @user and params[:display_name] == @user.display_name
@@ -57,15 +69,16 @@ class MessageController < ApplicationController
     end
   end
 
+  # Set the message as being read or unread.
   def mark
     if params[:message_id]
       id = params[:message_id]
       message = Message.find_by_id(id)
       if params[:mark] == 'unread'
-        message_read = 0 
+        message_read = false 
         mark_type = 'unread'
       else
-        message_read = 1
+        message_read = true
         mark_type = 'read'
       end
       message.message_read = message_read
@@ -74,5 +87,7 @@ class MessageController < ApplicationController
         redirect_to :controller => 'message', :action => 'inbox', :display_name => @user.display_name
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 end
index edc3675..c03f3c4 100644 (file)
@@ -11,20 +11,21 @@ class NodeController < ApplicationController
 
   # Create a node from XML.
   def create
-    if request.put?
-      node = Node.from_xml(request.raw_post, true)
-
-      if node
-        node.user_id = @user.id
-        node.visible = true
-        node.save_with_history!
+    begin
+      if request.put?
+        node = Node.from_xml(request.raw_post, true)
 
-        render :text => node.id.to_s, :content_type => "text/plain"
+        if node
+          node.create_with_history @user
+          render :text => node.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -32,7 +33,7 @@ class NodeController < ApplicationController
   def read
     begin
       node = Node.find(params[:id])
-      if node.visible
+      if node.visible?
         response.headers['Last-Modified'] = node.timestamp.rfc822
         render :text => node.to_xml.to_s, :content_type => "text/xml"
        else
@@ -42,7 +43,7 @@ class NodeController < ApplicationController
       render :nothing => true, :status => :not_found
     end
   end
-
+  
   # Update a node from given XML
   def update
     begin
@@ -50,49 +51,40 @@ class NodeController < ApplicationController
       new_node = Node.from_xml(request.raw_post)
 
       if new_node and new_node.id == node.id
-        node.user_id = @user.id
-        node.latitude = new_node.latitude 
-        node.longitude = new_node.longitude
-        node.tags = new_node.tags
-        node.visible = true
-        node.save_with_history!
-
-        render :nothing => true
+        node.update_from(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
   end
 
-  # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way.
-  # FIXME remove all the fricking SQL
+  # Delete a node. Doesn't actually delete it, but retains its history 
+  # in a wiki-like way. We therefore treat it like an update, so the delete
+  # method returns the new version number.
   def delete
     begin
       node = Node.find(params[:id])
-
-      if node.visible
-        if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = 1 AND current_way_nodes.node_id = ?", node.id ])
-          render :text => "", :status => :precondition_failed
-        elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='node' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          node.user_id = @user.id
-          node.visible = 0
-          node.save_with_history!
-
-          render :nothing => true
-        end
+      new_node = Node.from_xml(request.raw_post)
+      
+      if new_node and new_node.id == node.id
+        node.delete_with_history!(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
-  # WTF does this do?
+  # Dump the details on many nodes whose ids are given in the "nodes" parameter.
   def nodes
     ids = params['nodes'].split(',').collect { |n| n.to_i }
 
index e278983..5639762 100644 (file)
@@ -22,4 +22,21 @@ class OldNodeController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_node = OldNode.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_node.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_node.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 0b5aa89..84d0b0c 100644 (file)
@@ -2,6 +2,7 @@ class OldRelationController < ApplicationController
   require 'xml/libxml'
 
   session :off
+  before_filter :check_read_availability
   after_filter :compress_output
 
   def history
@@ -20,4 +21,21 @@ class OldRelationController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_relation = OldRelation.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_relation.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_relation.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internetal_service_error
+    end
+  end
 end
index e72c97a..da4e26d 100644 (file)
@@ -13,7 +13,7 @@ class OldWayController < ApplicationController
 
       way.old_ways.each do |old_way|
         doc.root << old_way.to_xml_node
-     end
+      end
 
       render :text => doc.to_s, :content_type => "text/xml"
     rescue ActiveRecord::RecordNotFound
@@ -22,4 +22,21 @@ class OldWayController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_way = OldWay.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_way.timestamp.rfc822
+      
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_way.to_xml_node
+      
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 2b1ba6c..93573b9 100644 (file)
@@ -8,23 +8,23 @@ class RelationController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      relation = Relation.from_xml(request.raw_post, true)
-
-      if relation
-        if !relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.save_with_history!
-
-         render :text => relation.id.to_s, :content_type => "text/plain"
-        end
+    begin
+      if request.put?
+        relation = Relation.from_xml(request.raw_post, true)
+
+        # We assume that an exception has been thrown if there was an error 
+        # generating the relation
+        #if relation
+          relation.create_with_history @user
+          render :text => relation.id.to_s, :content_type => "text/plain"
+        #else
+         # render :text => "Couldn't get turn the input into a relation.", :status => :bad_request
+        #end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -45,56 +45,38 @@ class RelationController < ApplicationController
   end
 
   def update
+    logger.debug request.raw_post
     begin
       relation = Relation.find(params[:id])
       new_relation = Relation.from_xml(request.raw_post)
 
       if new_relation and new_relation.id == relation.id
-        if !new_relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = new_relation.tags
-          relation.members = new_relation.members
-          relation.visible = true
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+        relation.update_from new_relation, @user
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
   def delete
-#XXX check if member somewhere!
     begin
       relation = Relation.find(params[:id])
-
-      if relation.visible
-        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = []
-          relation.members = []
-          relation.visible = false
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+      new_relation = Relation.from_xml(request.raw_post)
+      if new_relation and new_relation.id == relation.id
+        relation.delete_with_history!(new_relation, @user)
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
     end
   end
 
@@ -160,8 +142,7 @@ class RelationController < ApplicationController
         render :text => doc.to_s, :content_type => "text/xml"
 
       else
-
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :gone
       end
 
     rescue ActiveRecord::RecordNotFound
@@ -184,8 +165,10 @@ class RelationController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "You need to supply a comma separated list of ids.", :status => :bad_request
     end
+  rescue ActiveRecord::RecordNotFound
+    render :text => "Could not find one of the relations", :status => :not_found
   end
 
   def relations_for_way
@@ -199,12 +182,12 @@ class RelationController < ApplicationController
   end
 
   def relations_for_object(objtype)
-    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id }.uniq
+    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Relation.find(relationids).each do |relation|
-      doc.root << relation.to_xml_node
+      doc.root << relation.to_xml_node if relation.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index bcac118..6528dff 100644 (file)
@@ -1,8 +1,8 @@
 class TraceController < ApplicationController
   layout 'site'
 
-  before_filter :authorize_web  
-  before_filter :require_user, :only => [:mine, :edit, :delete, :make_public]
+  before_filter :authorize_web
+  before_filter :require_user, :only => [:mine, :create, :edit, :delete, :make_public]
   before_filter :authorize, :only => [:api_details, :api_data, :api_create]
   before_filter :check_database_availability, :except => [:api_details, :api_data, :api_create]
   before_filter :check_read_availability, :only => [:api_details, :api_data, :api_create]
@@ -13,7 +13,7 @@ class TraceController < ApplicationController
     # from display name, pick up user id if one user's traces only
     display_name = params[:display_name]
     if target_user.nil? and !display_name.blank?
-      target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
+      target_user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, display_name])
     end
 
     # set title
@@ -34,15 +34,15 @@ class TraceController < ApplicationController
     # 4 - user's traces, not logged in as that user = all user's public traces
     if target_user.nil? # all traces
       if @user
-        conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
+        conditions = ["(gpx_files.public = ? OR gpx_files.user_id = ?)", true, @user.id] #1
       else
-        conditions  = ["gpx_files.public = 1"] #2
+        conditions  = ["gpx_files.public = ?", true] #2
       end
     else
       if @user and @user == target_user
         conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
       else
-        conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
+        conditions = ["gpx_files.public = ? AND gpx_files.user_id = ?", true, target_user.id] #4
       end
     end
     
@@ -53,7 +53,8 @@ class TraceController < ApplicationController
       conditions[0] += " AND gpx_files.id IN (#{files.join(',')})"
     end
     
-    conditions[0] += " AND gpx_files.visible = 1"
+    conditions[0] += " AND gpx_files.visible = ?"
+    conditions << true
 
     @trace_pages, @traces = paginate(:traces,
                                      :include => [:user, :tags],
@@ -98,26 +99,28 @@ class TraceController < ApplicationController
   end
 
   def create
-    logger.info(params[:trace][:gpx_file].class.name)
-    if params[:trace][:gpx_file].respond_to?(:read)
-      do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
-                params[:trace][:description], params[:trace][:public])
+    if params[:trace]
+      logger.info(params[:trace][:gpx_file].class.name)
+      if params[:trace][:gpx_file].respond_to?(:read)
+        do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
+                  params[:trace][:description], params[:trace][:public])
 
-      if @trace.id
-        logger.info("id is #{@trace.id}")
-        flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
+        if @trace.id
+          logger.info("id is #{@trace.id}")
+          flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
 
-        redirect_to :action => 'mine'
+          redirect_to :action => 'mine'
+        end
+      else
+        @trace = Trace.new({:name => "Dummy",
+                            :tagstring => params[:trace][:tagstring],
+                            :description => params[:trace][:description],
+                            :public => params[:trace][:public],
+                            :inserted => false, :user => @user,
+                            :timestamp => Time.now})
+        @trace.valid?
+        @trace.errors.add(:gpx_file, "can't be blank")
       end
-    else
-      @trace = Trace.new({:name => "Dummy",
-                          :tagstring => params[:trace][:tagstring],
-                          :description => params[:trace][:description],
-                          :public => params[:trace][:public],
-                          :inserted => false, :user => @user,
-                          :timestamp => Time.now})
-      @trace.valid?
-      @trace.errors.add(:gpx_file, "can't be blank")
     end
   end
 
@@ -194,7 +197,7 @@ class TraceController < ApplicationController
   end
 
   def georss
-    conditions = ["gpx_files.public = 1"]
+    conditions = ["gpx_files.public = ?", true]
 
     if params[:display_name]
       conditions[0] += " AND users.display_name = ?"
@@ -276,12 +279,20 @@ class TraceController < ApplicationController
 
   def api_create
     if request.post?
-      do_create(params[:file], params[:tags], params[:description], params[:public])
-
-      if @trace.id
-        render :text => @trace.id.to_s, :content_type => "text/plain"
-      elsif @trace.valid?
-        render :nothing => true, :status => :internal_server_error
+      tags = params[:tags] || ""
+      description = params[:description] || ""
+      pub = params[:public] || false
+      
+      if params[:file].respond_to?(:read)
+        do_create(params[:file], tags, description, pub)
+
+        if @trace.id
+          render :text => @trace.id.to_s, :content_type => "text/plain"
+        elsif @trace.valid?
+          render :nothing => true, :status => :internal_server_error
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
         render :nothing => true, :status => :bad_request
       end
@@ -309,6 +320,17 @@ private
     else
       FileUtils.rm_f(filename)
     end
+    
+    # Finally save whether the user marked the trace as being public
+    if @trace.public?
+      if @user.trace_public_default.nil?
+        @user.preferences.create(:k => "gps.trace.public", :v => "default")
+      end
+    else
+      pref = @user.trace_public_default
+      pref.destroy unless pref.nil?
+    end
+    
   end
 
 end
index 6a60917..47c1cff 100644 (file)
@@ -82,7 +82,7 @@ class UserController < ApplicationController
   def lost_password
     @title = 'lost password'
     if params[:user] and params[:user][:email]
-      user = User.find_by_email(params[:user][:email], :conditions => "visible = 1")
+      user = User.find_by_email(params[:user][:email], :conditions => {:visible => true})
 
       if user
         token = user.tokens.create
@@ -122,6 +122,15 @@ class UserController < ApplicationController
   end
 
   def login
+    if session[:user]
+      # The user is logged in already, if the referer param exists, redirect them to that
+      if params[:referer]
+        redirect_to params[:referer]
+      else
+        redirect_to :controller => 'site', :action => 'index'
+      end
+      return
+    end
     @title = 'login'
     if params[:user]
       email_or_display_name = params[:user][:email]
@@ -222,7 +231,7 @@ class UserController < ApplicationController
   end
 
   def view
-    @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if @this_user
       @title = @this_user.display_name
@@ -235,7 +244,7 @@ class UserController < ApplicationController
   def make_friend
     if params[:display_name]     
       name = params[:display_name]
-      new_friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      new_friend = User.find_by_display_name(name, :conditions => {:visible => true})
       friend = Friend.new
       friend.user_id = @user.id
       friend.friend_user_id = new_friend.id
@@ -257,7 +266,7 @@ class UserController < ApplicationController
   def remove_friend
     if params[:display_name]     
       name = params[:display_name]
-      friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      friend = User.find_by_display_name(name, :conditions => {:visible => true})
       if @user.is_friends_with?(friend)
         Friend.delete_all "user_id = #{@user.id} AND friend_user_id = #{friend.id}"
         flash[:notice] = "#{friend.display_name} was removed from your friends."
index 3a48ee6..68ea88e 100644 (file)
@@ -5,11 +5,9 @@ class UserPreferenceController < ApplicationController
   def read_one
     pref = UserPreference.find(@user.id, params[:preference_key])
 
-    if pref
-      render :text => pref.v.to_s
-    else
-      render :text => 'OH NOES! PREF NOT FOUND!', :status => 404
-    end
+    render :text => pref.v.to_s
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => 'OH NOES! PREF NOT FOUND!', :status => :not_found
   end
 
   def update_one
@@ -32,6 +30,8 @@ class UserPreferenceController < ApplicationController
     UserPreference.delete(@user.id, params[:preference_key])
 
     render :nothing => true
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => "param: #{params[:preference_key]} not found", :status => :not_found
   end
 
   # print out all the preferences as a big xml block
@@ -52,48 +52,42 @@ class UserPreferenceController < ApplicationController
 
   # update the entire set of preferences
   def update
-    begin
-      p = XML::Parser.string(request.raw_post)
-      doc = p.parse
-
-      prefs = []
-
-      keyhash = {}
+    p = XML::Parser.string(request.raw_post)
+    doc = p.parse
 
-      doc.find('//preferences/preference').each do |pt|
-        pref = UserPreference.new
+    prefs = []
 
-        unless keyhash[pt['k']].nil? # already have that key
-          render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
-          return
-        end
+    keyhash = {}
 
-        keyhash[pt['k']] = 1
-
-        pref.k = pt['k']
-        pref.v = pt['v']
-        pref.user_id = @user.id
-        prefs << pref
-      end
+    doc.find('//preferences/preference').each do |pt|
+      pref = UserPreference.new
 
-      if prefs.size > 150
-        render :text => 'Too many preferences', :status => :request_entity_too_large
-        return
+      unless keyhash[pt['k']].nil? # already have that key
+        render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
       end
 
-      # kill the existing ones
-      UserPreference.delete_all(['user_id = ?', @user.id])
+      keyhash[pt['k']] = 1
 
-      # save the new ones
-      prefs.each do |pref|
-        pref.save!
-      end
+      pref.k = pt['k']
+      pref.v = pt['v']
+      pref.user_id = @user.id
+      prefs << pref
+    end
 
-    rescue Exception => ex
-      render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
-      return
+    if prefs.size > 150
+      render :text => 'Too many preferences', :status => :request_entity_too_large
     end
 
+    # kill the existing ones
+    UserPreference.delete_all(['user_id = ?', @user.id])
+
+    # save the new ones
+    prefs.each do |pref|
+      pref.save!
+    end
     render :nothing => true
+
+  rescue Exception => ex
+    render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
   end
 end
index 3b6491c..80c75d9 100644 (file)
@@ -8,23 +8,22 @@ class WayController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      way = Way.from_xml(request.raw_post, true)
-
-      if way
-        if !way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.save_with_history!
+    begin
+      if request.put?
+        way = Way.from_xml(request.raw_post, true)
 
+        if way
+          way.create_with_history @user
           render :text => way.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
         end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     end
   end
 
@@ -39,6 +38,8 @@ class WayController < ApplicationController
       else
         render :text => "", :status => :gone
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -50,20 +51,14 @@ class WayController < ApplicationController
       new_way = Way.from_xml(request.raw_post)
 
       if new_way and new_way.id == way.id
-        if !new_way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.tags = new_way.tags
-          way.nds = new_way.nds
-          way.visible = true
-          way.save_with_history!
-
-          render :nothing => true
-        end
+        way.update_from(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -73,14 +68,16 @@ class WayController < ApplicationController
   def delete
     begin
       way = Way.find(params[:id])
-      way.delete_with_relations_and_history(@user)
-
-      # if we get here, all is fine, otherwise something will catch below.  
-      render :nothing => true
-    rescue OSM::APIAlreadyDeletedError
-      render :text => "", :status => :gone
-    rescue OSM::APIPreconditionFailedError
-      render :text => "", :status => :precondition_failed
+      new_way = Way.from_xml(request.raw_post)
+
+      if new_way and new_way.id == way.id
+        way.delete_with_history!(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -92,7 +89,7 @@ class WayController < ApplicationController
 
       if way.visible
         nd_ids = way.nds + [-1]
-        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
+        nodes = Node.find(:all, :conditions => ["visible = ? AND id IN (#{nd_ids.join(',')})", true])
 
         # Render
         doc = OSM::API.new.get_xml_doc
@@ -130,13 +127,19 @@ class WayController < ApplicationController
     end
   end
 
+  ##
+  # returns all the ways which are currently using the node given in the 
+  # :id parameter. note that this used to return deleted ways as well, but
+  # this seemed not to be the expected behaviour, so it was removed.
   def ways_for_node
-    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id[0] }.uniq
+    wayids = WayNode.find(:all, 
+                          :conditions => ['node_id = ?', params[:id]]
+                          ).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Way.find(wayids).each do |way|
-      doc.root << way.to_xml_node
+      doc.root << way.to_xml_node if way.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index c86ad5b..34302a8 100644 (file)
@@ -1,2 +1,5 @@
 module BrowseHelper
+  def link_to_page(page, page_param)
+    return link_to(page, page_param => page)
+  end
 end
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
new file mode 100644 (file)
index 0000000..d420f53
--- /dev/null
@@ -0,0 +1,237 @@
+class Changeset < ActiveRecord::Base
+  require 'xml/libxml'
+
+  belongs_to :user
+
+  has_many :changeset_tags, :foreign_key => 'id'
+  
+  has_many :nodes
+  has_many :ways
+  has_many :relations
+  has_many :old_nodes
+  has_many :old_ways
+  has_many :old_relations
+  
+  validates_presence_of :id, :on => :update
+  validates_presence_of :user_id, :created_at, :closed_at, :num_changes
+  validates_uniqueness_of :id
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :min_lat, :max_lat, :min_lon, :max_lat, :allow_nil => true, :integer_only => true
+  validates_numericality_of :user_id,  :integer_only => true
+  validates_numericality_of :num_changes, :integer_only => true, :greater_than_or_equal_to => 0
+  validates_associated :user
+
+  # over-expansion factor to use when updating the bounding box
+  EXPAND = 0.1
+
+  # maximum number of elements allowed in a changeset
+  MAX_ELEMENTS = 50000
+
+  # maximum time a changeset is allowed to be open for.
+  MAX_TIME_OPEN = 1.day
+
+  # idle timeout increment, one hour seems reasonable.
+  IDLE_TIMEOUT = 1.hour
+
+  # Use a method like this, so that we can easily change how we
+  # determine whether a changeset is open, without breaking code in at 
+  # least 6 controllers
+  def is_open?
+    # a changeset is open (that is, it will accept further changes) when
+    # it has not yet run out of time and its capacity is small enough.
+    # note that this may not be a hard limit - due to timing changes and
+    # concurrency it is possible that some changesets may be slightly 
+    # longer than strictly allowed or have slightly more changes in them.
+    return ((closed_at > Time.now) and (num_changes <= MAX_ELEMENTS))
+  end
+
+  def set_closed_time_now
+    if is_open?
+      self.closed_at = Time.now
+    end
+  end
+  
+  def self.from_xml(xml, create=false)
+    begin
+      p = XML::Parser.string(xml)
+      doc = p.parse
+
+      cs = Changeset.new
+
+      doc.find('//osm/changeset').each do |pt|
+        if create
+          cs.created_at = Time.now
+          # initial close time is 1h ahead, but will be increased on each
+          # modification.
+          cs.closed_at = Time.now + IDLE_TIMEOUT
+          # initially we have no changes in a changeset
+          cs.num_changes = 0
+        end
+
+        pt.find('tag').each do |tag|
+          cs.add_tag_keyval(tag['k'], tag['v'])
+        end
+      end
+    rescue Exception => ex
+      cs = nil
+    end
+
+    return cs
+  end
+
+  ##
+  # returns the bounding box of the changeset. it is possible that some
+  # or all of the values will be nil, indicating that they are undefined.
+  def bbox
+    @bbox ||= [ min_lon, min_lat, max_lon, max_lat ]
+  end
+
+  ##
+  # expand the bounding box to include the given bounding box. also, 
+  # expand a little bit more in the direction of the expansion, so that
+  # further expansions may be unnecessary. this is an optimisation 
+  # suggested on the wiki page by kleptog.
+  def update_bbox!(array)
+    # ensure that bbox is cached and has no nils in it. if there are any
+    # nils, just use the bounding box update to write over them.
+    @bbox = bbox.zip(array).collect { |a, b| a.nil? ? b : a }
+
+    # FIXME - this looks nasty and violates DRY... is there any prettier 
+    # way to do this? 
+    @bbox[0] = array[0] + EXPAND * (@bbox[0] - @bbox[2]) if array[0] < @bbox[0]
+    @bbox[1] = array[1] + EXPAND * (@bbox[1] - @bbox[3]) if array[1] < @bbox[1]
+    @bbox[2] = array[2] + EXPAND * (@bbox[2] - @bbox[0]) if array[2] > @bbox[2]
+    @bbox[3] = array[3] + EXPAND * (@bbox[3] - @bbox[1]) if array[3] > @bbox[3]
+
+    # update active record. rails 2.1's dirty handling should take care of
+    # whether this object needs saving or not.
+    self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox
+  end
+
+  ##
+  # the number of elements is also passed in so that we can ensure that
+  # a single changeset doesn't contain too many elements. this, of course,
+  # destroys the optimisation described in the bbox method above.
+  def add_changes!(elements)
+    self.num_changes += elements
+  end
+
+  def tags_as_hash
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.changeset_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t
+  end
+
+  def add_tag_keyval(k, v)
+    @tags = Hash.new unless @tags
+    @tags[k] = v
+  end
+
+  def save_with_tags!
+    t = Time.now
+
+    # do the changeset update and the changeset tags update in the
+    # same transaction to ensure consistency.
+    Changeset.transaction do
+      # set the auto-close time to be one hour in the future unless
+      # that would make it more than 24h long, in which case clip to
+      # 24h, as this has been decided is a reasonable time limit.
+      if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
+        self.closed_at = created_at + MAX_TIME_OPEN
+      else
+        self.closed_at = Time.now + IDLE_TIMEOUT
+      end
+      self.save!
+
+      tags = self.tags
+      ChangesetTag.delete_all(['id = ?', self.id])
+
+      tags.each do |k,v|
+        tag = ChangesetTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+    end
+  end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+  
+  def to_xml_node(user_display_name_cache = nil)
+    el1 = XML::Node.new 'changeset'
+    el1['id'] = self.id.to_s
+
+    user_display_name_cache = {} if user_display_name_cache.nil?
+
+    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+      # use the cache if available
+    elsif self.user.data_public?
+      user_display_name_cache[self.user_id] = self.user.display_name
+    else
+      user_display_name_cache[self.user_id] = nil
+    end
+
+    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    el1['uid'] = self.user_id.to_s if self.user.data_public?
+
+    self.tags.each do |k,v|
+      el2 = XML::Node.new('tag')
+      el2['k'] = k.to_s
+      el2['v'] = v.to_s
+      el1 << el2
+    end
+    
+    el1['created_at'] = self.created_at.xmlschema
+    el1['closed_at'] = self.closed_at.xmlschema unless is_open?
+    el1['open'] = is_open?.to_s
+
+    el1['min_lon'] = (bbox[0].to_f / GeoRecord::SCALE).to_s unless bbox[0].nil?
+    el1['min_lat'] = (bbox[1].to_f / GeoRecord::SCALE).to_s unless bbox[1].nil?
+    el1['max_lon'] = (bbox[2].to_f / GeoRecord::SCALE).to_s unless bbox[2].nil?
+    el1['max_lat'] = (bbox[3].to_f / GeoRecord::SCALE).to_s unless bbox[3].nil?
+    
+    # NOTE: changesets don't include the XML of the changes within them,
+    # they are just structures for tagging. to get the osmChange of a
+    # changeset, see the download method of the controller.
+
+    return el1
+  end
+
+  ##
+  # update this instance from another instance given and the user who is
+  # doing the updating. note that this method is not for updating the
+  # bounding box, only the tags of the changeset.
+  def update_from(other, user)
+    # ensure that only the user who opened the changeset may modify it.
+    unless user.id == self.user_id 
+      raise OSM::APIUserChangesetMismatchError 
+    end
+    
+    # can't change a closed changeset
+    unless is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(self)
+    end
+
+    # copy the other's tags
+    self.tags = other.tags
+
+    save_with_tags!
+  end
+end
diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb
new file mode 100644 (file)
index 0000000..6a414a0
--- /dev/null
@@ -0,0 +1,8 @@
+class ChangesetTag < ActiveRecord::Base
+  belongs_to :changeset, :foreign_key => 'id'
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
+end
index dd1f988..8063372 100644 (file)
@@ -5,7 +5,11 @@ class DiaryEntry < ActiveRecord::Base
                             :order => "diary_comments.id"
 
   validates_presence_of :title, :body
-  validates_numericality_of :latitude, :allow_nil => true
-  validates_numericality_of :longitude, :allow_nil => true
+  validates_length_of :title, :within => 1..255
+  validates_length_of :language, :within => 2..3, :allow_nil => true
+  validates_numericality_of :latitude, :allow_nil => true,
+                            :greater_than_or_equal_to => -90, :less_than_or_equal_to => 90
+  validates_numericality_of :longitude, :allow_nil => true,
+                            :greater_than_or_equal_to => -180, :less_than_or_equal_to => 180
   validates_associated :user
 end
index 97e4111..464c550 100644 (file)
@@ -1,8 +1,12 @@
+require 'validators'
+
 class Message < ActiveRecord::Base
   belongs_to :sender, :class_name => "User", :foreign_key => :from_user_id
   belongs_to :recipient, :class_name => "User", :foreign_key => :to_user_id
 
-  validates_presence_of :title, :body, :sent_on
+  validates_presence_of :title, :body, :sent_on, :sender, :recipient
+  validates_length_of :title, :within => 1..255
   validates_inclusion_of :message_read, :in => [ true, false ]
   validates_associated :sender, :recipient
+  validates_as_utf8 :title
 end
index af88a11..d3e0a7e 100644 (file)
@@ -2,27 +2,34 @@ class Node < ActiveRecord::Base
   require 'xml/libxml'
 
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'current_nodes'
-  
-  validates_presence_of :user_id, :timestamp
-  validates_inclusion_of :visible, :in => [ true, false ]
-  validates_numericality_of :latitude, :longitude
-  validate :validate_position
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_nodes, :foreign_key => :id
 
   has_many :way_nodes
   has_many :ways, :through => :way_nodes
 
+  has_many :node_tags, :foreign_key => :id
+  
   has_many :old_way_nodes
   has_many :ways_via_history, :class_name=> "Way", :through => :old_way_nodes, :source => :way
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :latitude, :longitude, :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validate :validate_position
+  validates_associated :changeset
+
   # Sanity check the latitude and longitude and add an error if it's broken
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
@@ -50,7 +57,7 @@ class Node < ActiveRecord::Base
     #conditions = keys.join(' AND ')
  
     find_by_area(min_lat, min_lon, max_lat, max_lon,
-                    :conditions => 'visible = 1',
+                    :conditions => {:visible => true},
                     :limit => APP_CONFIG['max_number_of_nodes']+1)  
   end
 
@@ -59,83 +66,148 @@ class Node < ActiveRecord::Base
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
-  
-      node = Node.new
 
       doc.find('//osm/node').each do |pt|
-        node.lat = pt['lat'].to_f
-        node.lon = pt['lon'].to_f
+        return Node.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("node", xml, ex.message)
+    end
+  end
 
-        return nil unless node.in_world?
+  def self.from_xml_node(pt, create=false)
+    node = Node.new
+    
+    raise OSM::APIBadXMLError.new("node", pt, "lat missing") if pt['lat'].nil?
+    raise OSM::APIBadXMLError.new("node", pt, "lon missing") if pt['lon'].nil?
+    node.lat = pt['lat'].to_f
+    node.lon = pt['lon'].to_f
+    raise OSM::APIBadXMLError.new("node", pt, "changeset id missing") if pt['changeset'].nil?
+    node.changeset_id = pt['changeset'].to_i
 
-        unless create
-          if pt['id'] != '0'
-            node.id = pt['id'].to_i
-          end
-        end
+    raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world?
 
-        node.visible = pt['visible'] and pt['visible'] == 'true'
+    # version must be present unless creating
+    raise OSM::APIBadXMLError.new("node", pt, "Version is required when updating") unless create or not pt['version'].nil?
+    node.version = create ? 0 : pt['version'].to_i
 
-        if create
-          node.timestamp = Time.now
-        else
-          if pt['timestamp']
-            node.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+    unless create
+      if pt['id'] != '0'
+        node.id = pt['id'].to_i
+      end
+    end
 
-        tags = []
+    # visible if it says it is, or as the default if the attribute
+    # is missing.
+    # Don't need to set the visibility, when it is set explicitly in the create/update/delete
+    #node.visible = pt['visible'].nil? or pt['visible'] == 'true'
 
-        pt.find('tag').each do |tag|
-          tags << [tag['k'],tag['v']]
-        end
+    # We don't care about the time, as it is explicitly set on create/update/delete
 
-        node.tags = Tags.join(tags)
-      end
-    rescue
-      node = nil
+    tags = []
+
+    pt.find('tag').each do |tag|
+      node.add_tag_key_val(tag['k'],tag['v'])
     end
 
     return node
   end
 
-  # Save this node with the appropriate OldNode object to represent it's history.
-  def save_with_history!
+  ##
+  # the bounding box around a node, which is used for determining the changeset's
+  # bounding box
+  def bbox
+    [ longitude, latitude, longitude, latitude ]
+  end
+
+  # Should probably be renamed delete_from to come in line with update
+  def delete_with_history!(new_node, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
     Node.transaction do
-      self.timestamp = Time.now
-      self.save!
-      old_node = OldNode.from_node(self)
-      old_node.save!
+      check_consistency(self, new_node, user)
+      if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = ? AND current_way_nodes.node_id = ?", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new
+      elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='node' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new
+      else
+        self.changeset_id = new_node.changeset_id
+        self.visible = false
+        
+        # update the changeset with the deleted position
+        changeset.update_bbox!(bbox)
+        
+        save_with_history!
+      end
     end
   end
 
-  # Turn this Node in to a complete OSM XML object with <osm> wrapper
+  def update_from(new_node, user)
+    check_consistency(self, new_node, user)
+
+    # update changeset with *old* position first
+    changeset.update_bbox!(bbox);
+
+    # FIXME logic needs to be double checked
+    self.changeset_id = new_node.changeset_id
+    self.latitude = new_node.latitude 
+    self.longitude = new_node.longitude
+    self.tags = new_node.tags
+    self.visible = true
+
+    # update changeset with *new* position
+    changeset.update_bbox!(bbox);
+
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    self.id = nil
+    self.version = 0
+    self.visible = true
+
+    # update the changeset to include the new location
+    changeset.update_bbox!(bbox)
+
+    save_with_history!
+  end
+
   def to_xml
     doc = OSM::API.new.get_xml_doc
     doc.root << to_xml_node()
     return doc
   end
 
-  # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node(user_display_name_cache = nil)
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
+    
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -147,12 +219,79 @@ class Node < ActiveRecord::Base
     return el1
   end
 
-  # Return the node's tags as a Hash of keys and their values
   def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.node_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t 
+  end 
+
+  def add_tag_key_val(k,v)
+    @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("node", self.id, k) if @tags.include? k
+
+    @tags[k] = v
+  end
+
+  ##
+  # are the preconditions OK? this is mainly here to keep the duck
+  # typing interface the same between nodes, ways and relations.
+  def preconditions_ok?
+    in_world?
+  end
+
+  ##
+  # dummy method to make the interfaces of node, way and relation
+  # more consistent.
+  def fix_placeholders!(id_map)
+    # nodes don't refer to anything, so there is nothing to do here
+  end
+  
+  private
+
+  def save_with_history!
+    t = Time.now
+    Node.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      # Create a NodeTag
+      tags = self.tags
+      NodeTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = NodeTag.new
+        tag.k = k 
+        tag.v = v 
+        tag.id = self.id
+        tag.save!
+      end 
+
+      # Create an OldNode
+      old_node = OldNode.from_node(self)
+      old_node.timestamp = t
+      old_node.save_with_dependencies!
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the changeset in case of bounding box updates
+      changeset.save!
     end
-    hash
   end
+  
 end
diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb
new file mode 100644 (file)
index 0000000..4942601
--- /dev/null
@@ -0,0 +1,10 @@
+class NodeTag < ActiveRecord::Base
+  set_table_name 'current_node_tags'
+
+  belongs_to :node, :foreign_key => 'id'
+  
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
+end
index 76eab84..be115c5 100644 (file)
@@ -1,25 +1,21 @@
 class OldNode < ActiveRecord::Base
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'nodes'
   
-  validates_presence_of :user_id, :timestamp
+  validates_presence_of :changeset_id, :timestamp
   validates_inclusion_of :visible, :in => [ true, false ]
   validates_numericality_of :latitude, :longitude
   validate :validate_position
+  validates_associated :changeset
 
-  belongs_to :user
+  belongs_to :changeset
  
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
   end
 
-  def in_world?
-    return false if self.lat < -90 or self.lat > 90
-    return false if self.lon < -180 or self.lon > 180
-    return true
-  end
-
   def self.from_node(node)
     old_node = OldNode.new
     old_node.latitude = node.latitude
@@ -27,19 +23,30 @@ class OldNode < ActiveRecord::Base
     old_node.visible = node.visible
     old_node.tags = node.tags
     old_node.timestamp = node.timestamp
-    old_node.user_id = node.user_id
+    old_node.changeset_id = node.changeset_id
     old_node.id = node.id
+    old_node.version = node.version
     return old_node
   end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
 
   def to_xml_node
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-    el1['user'] = self.user.display_name if self.user.data_public?
+    el1['changeset'] = self.changeset.id.to_s
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -48,24 +55,54 @@ class OldNode < ActiveRecord::Base
 
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
     return el1
   end
-  
-  def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+
+  def save_with_dependencies!
+    save!
+    #not sure whats going on here
+    clear_aggregation_cache
+    clear_association_cache
+    #ok from here
+    @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ? AND version = ?', self.id, self.timestamp, self.version]).instance_variable_get('@attributes'))
+   
+    self.tags.each do |k,v|
+      tag = OldNodeTag.new
+      tag.k = k
+      tag.v = v
+      tag.id = self.id
+      tag.version = self.version
+      tag.save!
     end
-    hash
   end
 
-  # Pretend we're not in any ways
-  def ways
-    return []
+  def tags
+    unless @tags
+        @tags = Hash.new
+        OldNodeTag.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |tag|
+            @tags[tag.k] = tag.v
+        end
+    end
+    @tags = Hash.new unless @tags
+    @tags
   end
 
-  # Pretend we're not in any relations
-  def containing_relation_members
-    return []
+  def tags=(t)
+    @tags = t 
   end
+
+  def tags_as_hash 
+    return self.tags
+  end 
+  # Pretend we're not in any ways 
+  def ways 
+    return [] 
+  end 
+  # Pretend we're not in any relations 
+  def containing_relation_members 
+    return [] 
+  end 
 end
diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb
new file mode 100644 (file)
index 0000000..3fd4bf8
--- /dev/null
@@ -0,0 +1,10 @@
+class OldNodeTag < ActiveRecord::Base
+  set_table_name 'node_tags'
+  
+  belongs_to :user
+
+  validates_presence_of :id, :version
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
+end
index bac03c4..e2a6505 100644 (file)
@@ -1,14 +1,19 @@
 class OldRelation < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'relations'
 
-  belongs_to :user
+  belongs_to :changeset
+  
+  validates_associated :changeset
 
   def self.from_relation(relation)
     old_relation = OldRelation.new
     old_relation.visible = relation.visible
-    old_relation.user_id = relation.user_id
+    old_relation.changeset_id = relation.changeset_id
     old_relation.timestamp = relation.timestamp
     old_relation.id = relation.id
+    old_relation.version = relation.version
     old_relation.members = relation.members
     old_relation.tags = relation.tags
     return old_relation
@@ -33,14 +38,12 @@ class OldRelation < ActiveRecord::Base
       tag.save!
     end
 
-    i = 1
-    self.members.each do |m|
+    self.members.each_with_index do |m,i|
       member = OldRelationMember.new
-      member.id = self.id
+      member.id = [self.id, self.version, i]
       member.member_type = m[0]
       member.member_id = m[1]
       member.member_role = m[2]
-      member.version = self.version
       member.save!
     end
   end
@@ -48,7 +51,7 @@ class OldRelation < ActiveRecord::Base
   def members
     unless @members
         @members = Array.new
-        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |m|
+        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version], :order => "sequence_id").each do |m|
             @members += [[m.type,m.id,m.role]]
         end
     end
@@ -85,12 +88,23 @@ class OldRelation < ActiveRecord::Base
     OldRelationTag.find(:all, :conditions => ['id = ? AND version = ?', self.id, self.version])    
   end
 
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+
   def to_xml_node
     el1 = XML::Node.new 'relation'
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
     
     self.old_members.each do |member|
       e = XML::Node.new 'member'
index d8b6858..f0294d3 100644 (file)
@@ -1,3 +1,6 @@
 class OldRelationMember < ActiveRecord::Base
   set_table_name 'relation_members'
+
+  set_primary_keys :id, :version, :sequence_id
+  belongs_to :relation, :foreign_key=> :id
 end
index 7ce6f69..0fcb113 100644 (file)
@@ -1,3 +1,10 @@
 class OldRelationTag < ActiveRecord::Base
   set_table_name 'relation_tags'
+  
+  belongs_to :old_relation, :foreign_key => [:id, :version]
+  
+  validates_presence_of :id, :version
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index 63265d6..425478a 100644 (file)
@@ -1,14 +1,19 @@
 class OldWay < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'ways'
 
-  belongs_to :user
+  belongs_to :changeset
 
+  validates_associated :changeset
+  
   def self.from_way(way)
     old_way = OldWay.new
     old_way.visible = way.visible
-    old_way.user_id = way.user_id
+    old_way.changeset_id = way.changeset_id
     old_way.timestamp = way.timestamp
     old_way.id = way.id
+    old_way.version = way.version
     old_way.nds = way.nds
     old_way.tags = way.tags
     return old_way
@@ -93,7 +98,12 @@ class OldWay < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset.id.to_s
     
     self.old_nodes.each do |nd| # FIXME need to make sure they come back in the right order
       e = XML::Node.new 'nd'
@@ -114,27 +124,29 @@ class OldWay < ActiveRecord::Base
   # For get_nodes_undelete, uses same nodes, even if they've moved since
   # For get_nodes_revert,   allocates new ids 
   # Currently returns Potlatch-style array
-  
+  # where [5] indicates whether latest version is usable as is (boolean)
+  # (i.e. is it visible? are we actually reverting to an earlier version?)
+
   def get_nodes_undelete
        points = []
        self.nds.each do |n|
          node=Node.find(n)
-         points << [node.lon, node.lat, n, node.visible ? 1 : 0, node.tags_as_hash]
+         points << [node.lon, node.lat, n, node.version, node.tags_as_hash, node.visible]
     end
        points
   end
   
-  def get_nodes_revert
+  def get_nodes_revert(timestamp)
     points=[]
     self.nds.each do |n|
-      oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,self.timestamp], :order=>"timestamp DESC")
+      oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,timestamp], :order=>"timestamp DESC")
       curnode=Node.find(n)
-      id=n; v=curnode.visible ? 1 : 0
+      id=n; reuse=curnode.visible
       if oldnode.lat!=curnode.lat or oldnode.lon!=curnode.lon or oldnode.tags!=curnode.tags then
         # node has changed: if it's in other ways, give it a new id
-        if curnode.ways-[self.id] then id=-1; v=nil end
+        if curnode.ways-[self.id] then id=-1; reuse=false end
       end
-      points << [oldnode.lon, oldnode.lat, id, v, oldnode.tags_as_hash]
+      points << [oldnode.lon, oldnode.lat, id, curnode.version, oldnode.tags_as_hash, reuse]
     end
     points
   end
index b02fd45..801532d 100644 (file)
@@ -1,6 +1,10 @@
 class OldWayTag < ActiveRecord::Base
-  belongs_to :user
-
   set_table_name 'way_tags'
 
+  belongs_to :old_way, :foreign_key => [:id, :version]
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index d9dba30..64af4ec 100644 (file)
@@ -1,51 +1,83 @@
 class Relation < ActiveRecord::Base
   require 'xml/libxml'
   
+  include ConsistencyValidations
+  
   set_table_name 'current_relations'
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_relations, :foreign_key => 'id', :order => 'version'
 
-  has_many :relation_members, :foreign_key => 'id'
+  has_many :relation_members, :foreign_key => 'id', :order => 'sequence_id'
   has_many :relation_tags, :foreign_key => 'id'
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id 
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_associated :changeset
+  
+  TYPES = ["node", "way", "relation"]
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
 
-      relation = Relation.new
-
       doc.find('//osm/relation').each do |pt|
-        if !create and pt['id'] != '0'
-          relation.id = pt['id'].to_i
-        end
+        return Relation.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("relation", xml, ex.message)
+    end
+  end
 
-        if create
-          relation.timestamp = Time.now
-          relation.visible = true
-        else
-          if pt['timestamp']
-            relation.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    relation = Relation.new
 
-        pt.find('tag').each do |tag|
-          relation.add_tag_keyval(tag['k'], tag['v'])
-        end
+    if !create and pt['id'] != '0'
+      relation.id = pt['id'].to_i
+    end
 
-        pt.find('member').each do |member|
-          relation.add_member(member['type'], member['ref'], member['role'])
-        end
+    raise OSM::APIBadXMLError.new("relation", pt, "You are missing the required changeset in the relation") if pt['changeset'].nil?
+    relation.changeset_id = pt['changeset']
+
+    # The follow block does not need to be executed because they are dealt with 
+    # in create_with_history, update_from and delete_with_history
+    if create
+      relation.timestamp = Time.now
+      relation.visible = true
+      relation.version = 0
+    else
+      if pt['timestamp']
+        relation.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      relation = nil
+      relation.version = pt['version']
+    end
+
+    pt.find('tag').each do |tag|
+      relation.add_tag_keyval(tag['k'], tag['v'])
     end
 
+    pt.find('member').each do |member|
+      #member_type = 
+      logger.debug "each member"
+      raise OSM::APIBadXMLError.new("relation", pt, "The #{member['type']} is not allowed only, #{TYPES.inspect} allowed") unless TYPES.include? member['type']
+      logger.debug "after raise"
+      #member_ref = member['ref']
+      #member_role
+      member['role'] ||= "" # Allow  the upload to not include this, in which case we default to an empty string.
+      logger.debug member['role']
+      relation.add_member(member['type'], member['ref'], member['role'])
+    end
+    raise OSM::APIBadUserInput.new("Some bad xml in relation") if relation.nil?
+
     return relation
   end
 
@@ -60,18 +92,23 @@ class Relation < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
     
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     self.relation_members.each do |member|
       p=0
@@ -170,19 +207,164 @@ class Relation < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("relation", self.id, k) if @tags.include? k
+
     @tags[k] = v
   end
 
+  ##
+  # updates the changeset bounding box to contain the bounding box of 
+  # the element with given +type+ and +id+. this only works with nodes
+  # and ways at the moment, as they're the only elements to respond to
+  # the :bbox call.
+  def update_changeset_element(type, id)
+    element = Kernel.const_get(type.capitalize).find(id)
+    changeset.update_bbox! element.bbox
+  end    
+
+  def delete_with_history!(new_relation, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Relation.transaction do
+      check_consistency(self, new_relation, user)
+      # This will check to see if this relation is used by another relation
+      if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='relation' and member_id=? ", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new("The relation #{new_relation.id} is a used in another relation")
+      end
+      self.changeset_id = new_relation.changeset_id
+      self.tags = {}
+      self.members = []
+      self.visible = false
+      save_with_history!
+    end
+  end
+
+  def update_from(new_relation, user)
+    check_consistency(self, new_relation, user)
+    if !new_relation.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.changeset_id = new_relation.changeset_id
+    self.tags = new_relation.tags
+    self.members = new_relation.members
+    self.visible = true
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.version = 0
+    self.visible = true
+    save_with_history!
+  end
+
+  def preconditions_ok?
+    # These are hastables that store an id in the index of all 
+    # the nodes/way/relations that have already been added.
+    # If the member is valid and visible then we add it to the 
+    # relevant hash table, with the value true as a cache.
+    # Thus if you have nodes with the ids of 50 and 1 already in the
+    # relation, then the hash table nodes would contain:
+    # => {50=>true, 1=>true}
+    elements = { :node => Hash.new, :way => Hash.new, :relation => Hash.new }
+    self.members.each do |m|
+      # find the hash for the element type or die
+      hash = elements[m[0].to_sym] or return false
+
+      # unless its in the cache already
+      unless hash.key? m[1]
+        # use reflection to look up the appropriate class
+        model = Kernel.const_get(m[0].capitalize)
+
+        # get the element with that ID
+        element = model.find(m[1])
+
+        # and check that it is OK to use.
+        unless element and element.visible? and element.preconditions_ok?
+          return false
+        end
+        hash[m[1]] = true
+      end
+    end
+
+    return true
+  rescue
+    return false
+  end
+
+  # Temporary method to match interface to nodes
+  def tags_as_hash
+    return self.tags
+  end
+
+  ##
+  # if any members are referenced by placeholder IDs (i.e: negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.members.map! do |type, id, role|
+      old_id = id.to_i
+      if old_id < 0
+        new_id = id_map[type.to_sym][old_id]
+        raise "invalid placeholder" if new_id.nil?
+        [type, new_id, role]
+      else
+        [type, id, role]
+      end
+    end
+  end
+
+  private
+  
   def save_with_history!
     Relation.transaction do
+      # have to be a little bit clever here - to detect if any tags
+      # changed then we have to monitor their before and after state.
+      tags_changed = false
+
       t = Time.now
+      self.version += 1
       self.timestamp = t
       self.save!
 
       tags = self.tags
+      self.relation_tags.each do |old_tag|
+        key = old_tag.k
+        # if we can match the tags we currently have to the list
+        # of old tags, then we never set the tags_changed flag. but
+        # if any are different then set the flag and do the DB 
+        # update.
+        if tags.has_key? key 
+          # rails 2.1 dirty handling should take care of making this
+          # somewhat efficient... hopefully...
+          old_tag.v = tags[key]
+          tags_changed |= old_tag.changed?
+          old_tag.save!
+
+          # remove from the map, so that we can expect an empty map
+          # at the end if there are no new tags
+          tags.delete key
 
-      RelationTag.delete_all(['id = ?', self.id])
-
+        else
+          # this means a tag was deleted
+          tags_changed = true
+          RelationTag.delete_all ['id = ? and k = ?', self.id, old_tag.k]
+        end
+      end
+      # if there are left-over tags then they are new and will have to
+      # be added.
+      tags_changed |= (not tags.empty?)
       tags.each do |k,v|
         tag = RelationTag.new
         tag.k = k
@@ -191,80 +373,80 @@ class Relation < ActiveRecord::Base
         tag.save!
       end
 
-      members = self.members
-
-      RelationMember.delete_all(['id = ?', self.id])
+      # same pattern as before, but this time we're collecting the
+      # changed members in an array, as the bounding box updates for
+      # elements are per-element, not blanked on/off like for tags.
+      changed_members = Array.new
+      members = Hash.new
+      self.members.each do |m|
+        # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays
+        members[[m[1], m[0]]] = m[2]
+      end
+      relation_members.each do |old_member|
+        key = [old_member.member_id.to_s, old_member.member_type]
+        if members.has_key? key
+          members.delete key
+        else
+          changed_members << key
+        end
+      end
+      # any remaining members must be new additions
+      changed_members += members.keys
 
-      members.each do |n|
+      # update the members. first delete all the old members, as the new
+      # members may be in a different order and i don't feel like implementing
+      # a longest common subsequence algorithm to optimise this.
+      members = self.members
+      RelationMember.delete_all(:id => self.id)
+      members.each_with_index do |m,i|
         mem = RelationMember.new
-        mem.id = self.id
-        mem.member_type = n[0];
-        mem.member_id = n[1];
-        mem.member_role = n[2];
+        mem.id = [self.id, i]
+        mem.member_type = m[0]
+        mem.member_id = m[1]
+        mem.member_role = m[2]
         mem.save!
       end
 
       old_relation = OldRelation.from_relation(self)
       old_relation.timestamp = t
       old_relation.save_with_dependencies!
-    end
-  end
 
-  def preconditions_ok?
-    # These are hastables that store an id in the index of all 
-    # the nodes/way/relations that have already been added.
-    # Once we know the id of the node/way/relation exists
-    # we check to see if it is already existing in the hashtable
-    # if it does, then we return false. Otherwise
-    # we add it to the relevant hash table, with the value true..
-    # Thus if you have nodes with the ids of 50 and 1 already in the
-    # relation, then the hash table nodes would contain:
-    # => {50=>true, 1=>true}
-    nodes = Hash.new
-    ways = Hash.new
-    relations = Hash.new
-    self.members.each do |m|
-      if (m[0] == "node")
-        n = Node.find(:first, :conditions => ["id = ?", m[1]])
-        unless n and n.visible 
-          return false
-        end
-        if nodes[m[1]]
-          return false
-        else
-          nodes[m[1]] = true
-        end
-      elsif (m[0] == "way")
-        w = Way.find(:first, :conditions => ["id = ?", m[1]])
-        unless w and w.visible and w.preconditions_ok?
-          return false
-        end
-        if ways[m[1]]
-          return false
-        else
-          ways[m[1]] = true
-        end
-      elsif (m[0] == "relation")
-        e = Relation.find(:first, :conditions => ["id = ?", m[1]])
-        unless e and e.visible and e.preconditions_ok?
-          return false
-        end
-        if relations[m[1]]
-          return false
-        else
-          relations[m[1]] = true
+      # update the bbox of the changeset and save it too.
+      # discussion on the mailing list gave the following definition for
+      # the bounding box update procedure of a relation:
+      #
+      # adding or removing nodes or ways from a relation causes them to be
+      # added to the changeset bounding box. adding a relation member or
+      # changing tag values causes all node and way members to be added to the
+      # bounding box. this is similar to how the map call does things and is
+      # reasonable on the assumption that adding or removing members doesn't
+      # materially change the rest of the relation.
+      any_relations = 
+        changed_members.collect { |id,type| type == "relation" }.
+        inject(false) { |b,s| b or s }
+
+      if tags_changed or any_relations
+        # add all non-relation bounding boxes to the changeset
+        # FIXME: check for tag changes along with element deletions and
+        # make sure that the deleted element's bounding box is hit.
+        self.members.each do |type, id, role|
+          if type != "relation"
+            update_changeset_element(type, id)
+          end
         end
       else
-        return false
+        # add only changed members to the changeset
+        changed_members.each do |id, type|
+          update_changeset_element(type, id)
+        end
       end
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the (maybe updated) changeset bounding box
+      changeset.save!
     end
-    return true
-  rescue
-    return false
   end
 
-  # Temporary method to match interface to nodes
-  def tags_as_hash
-    return self.tags
-  end
 end
index 9ff4f46..f3033d1 100644 (file)
@@ -1,6 +1,7 @@
 class RelationMember < ActiveRecord::Base
   set_table_name 'current_relation_members'
   
+  set_primary_keys :id, :sequence_id
   belongs_to :member, :polymorphic => true, :foreign_type => :member_class
   belongs_to :relation, :foreign_key => :id
 
@@ -9,7 +10,7 @@ class RelationMember < ActiveRecord::Base
   end
 
   def after_initialize
-    self[:member_class] = self.member_type.capitalize
+    self[:member_class] = self.member_type.capitalize unless self.member_type.nil?
   end
 
   def before_save
index 939165e..812b2ec 100644 (file)
@@ -3,4 +3,8 @@ class RelationTag < ActiveRecord::Base
 
   belongs_to :relation, :foreign_key => 'id'
 
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
index 10e867b..03dbeb0 100644 (file)
@@ -3,6 +3,8 @@ class Trace < ActiveRecord::Base
 
   validates_presence_of :user_id, :name, :timestamp
   validates_presence_of :description, :on => :create
+  validates_length_of :name, :maximum => 255
+  validates_length_of :description, :maximum => 255
 #  validates_numericality_of :latitude, :longitude
   validates_inclusion_of :public, :inserted, :in => [ true, false]
   
index f1d5967..f9833e1 100644 (file)
@@ -2,6 +2,7 @@ class Tracetag < ActiveRecord::Base
   set_table_name 'gpx_file_tags'
 
   validates_format_of :tag, :with => /^[^\/;.,?]*$/
+  validates_length_of :tag, :within => 1..255
 
   belongs_to :trace, :foreign_key => 'gpx_id'
 end
index fae0371..ce244fe 100644 (file)
@@ -4,19 +4,21 @@ class User < ActiveRecord::Base
   has_many :traces
   has_many :diary_entries, :order => 'created_at DESC'
   has_many :messages, :foreign_key => :to_user_id, :order => 'sent_on DESC'
-  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => "message_read = 0", :order => 'sent_on DESC'
+  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => {:message_read => false}, :order => 'sent_on DESC'
   has_many :sent_messages, :class_name => "Message", :foreign_key => :from_user_id, :order => 'sent_on DESC'
-  has_many :friends, :include => :befriendee, :conditions => "users.visible = 1"
+  has_many :friends, :include => :befriendee, :conditions => ["users.visible = ?", true]
   has_many :tokens, :class_name => "UserToken"
   has_many :preferences, :class_name => "UserPreference"
+  has_many :changesets
 
   validates_presence_of :email, :display_name
   validates_confirmation_of :email, :message => 'Email addresses must match'
   validates_confirmation_of :pass_crypt, :message => 'Password must match the confirmation password'
   validates_uniqueness_of :display_name, :allow_nil => true
   validates_uniqueness_of :email
-  validates_length_of :pass_crypt, :minimum => 8
-  validates_length_of :display_name, :minimum => 3, :allow_nil => true
+  validates_length_of :pass_crypt, :within => 8..255
+  validates_length_of :display_name, :within => 3..255, :allow_nil => true
+  validates_length_of :email, :within => 6..255
   validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
   validates_format_of :display_name, :with => /^[^\/;.,?]*$/
   validates_numericality_of :home_lat, :allow_nil => true
@@ -80,7 +82,7 @@ class User < ActiveRecord::Base
     if self.home_lon and self.home_lat 
       gc = OSM::GreatCircle.new(self.home_lat, self.home_lon)
       bounds = gc.bounds(radius)
-      nearby = User.find(:all, :conditions => "visible = 1 and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = 1 and id != #{self.id}")
+      nearby = User.find(:all, :conditions => ["visible = ? and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = ? and id != #{self.id}", true, true])
       nearby.delete_if { |u| gc.distance(u.home_lat, u.home_lon) > radius }
       nearby.sort! { |u1,u2| gc.distance(u1.home_lat, u1.home_lon) <=> gc.distance(u2.home_lat, u2.home_lon) }
     else
@@ -104,6 +106,10 @@ class User < ActiveRecord::Base
     return false
   end
 
+  def trace_public_default
+    return self.preferences.find(:first, :conditions => {:k => "gps.trace.public", :v => "default"})
+  end
+
   def delete
     self.active = false
     self.display_name = "user_#{self.id}"
index 3985a52..28ef40f 100644 (file)
@@ -1,6 +1,9 @@
 class UserPreference < ActiveRecord::Base
   set_primary_keys :user_id, :k
   belongs_to :user
+  
+  validates_length_of :k, :within => 1..255
+  validates_length_of :v, :within => 1..255
 
   # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node
index 6c3ea9e..dbc1197 100644 (file)
@@ -1,9 +1,11 @@
 class Way < ActiveRecord::Base
   require 'xml/libxml'
+  
+  include ConsistencyValidations
 
   set_table_name 'current_ways'
-
-  belongs_to :user
+  
+  belongs_to :changeset
 
   has_many :old_ways, :foreign_key => 'id', :order => 'version'
 
@@ -15,37 +17,56 @@ class Way < ActiveRecord::Base
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :changeset_id,:version,  :timestamp
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_associated :changeset
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
 
-      way = Way.new
-
       doc.find('//osm/way').each do |pt|
-        if !create and pt['id'] != '0'
-          way.id = pt['id'].to_i
-        end
-
-        if create
-          way.timestamp = Time.now
-          way.visible = true
-        else
-          if pt['timestamp']
-            way.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+        return Way.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("way", xml, ex.message)
+    end
+  end
 
-        pt.find('tag').each do |tag|
-          way.add_tag_keyval(tag['k'], tag['v'])
-        end
+  def self.from_xml_node(pt, create=false)
+    way = Way.new
 
-        pt.find('nd').each do |nd|
-          way.add_nd_num(nd['ref'])
-        end
+    if !create and pt['id'] != '0'
+      way.id = pt['id'].to_i
+    end
+    
+    way.version = pt['version']
+    raise OSM::APIBadXMLError.new("node", pt, "Changeset is required") if pt['changeset'].nil?
+    way.changeset_id = pt['changeset']
+
+    # This next section isn't required for the create, update, or delete of ways
+    if create
+      way.timestamp = Time.now
+      way.visible = true
+    else
+      if pt['timestamp']
+        way.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      way = nil
+      # if visible isn't present then it defaults to true
+      way.visible = (pt['visible'] or true)
+    end
+
+    pt.find('tag').each do |tag|
+      way.add_tag_keyval(tag['k'], tag['v'])
+    end
+
+    pt.find('nd').each do |nd|
+      way.add_nd_num(nd['ref'])
     end
 
     return way
@@ -73,18 +94,23 @@ class Way < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     # make sure nodes are output in sequence_id order
     ordered_nodes = []
@@ -96,7 +122,7 @@ class Way < ActiveRecord::Base
         end
       else
         # otherwise, manually go to the db to check things
-        if nd.node.visible? and nd.node.visible?
+        if nd.node and nd.node.visible?
           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
         end
       end
@@ -154,99 +180,82 @@ class Way < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
-    @tags[k] = v
-  end
 
-  def save_with_history!
-    t = Time.now
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("way", self.id, k) if @tags.include? k
 
-    Way.transaction do
-      self.timestamp = t
-      self.save!
-    end
-
-    WayTag.transaction do
-      tags = self.tags
+    @tags[k] = v
+  end
 
-      WayTag.delete_all(['id = ?', self.id])
+  ##
+  # the integer coords (i.e: unscaled) bounding box of the way, assuming
+  # straight line segments.
+  def bbox
+    lons = nodes.collect { |n| n.longitude }
+    lats = nodes.collect { |n| n.latitude }
+    [ lons.min, lats.min, lons.max, lats.max ]
+  end
 
-      tags.each do |k,v|
-        tag = WayTag.new
-        tag.k = k
-        tag.v = v
-        tag.id = self.id
-        tag.save!
-      end
+  def update_from(new_way, user)
+    check_consistency(self, new_way, user)
+    if !new_way.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
+    self.changeset_id = new_way.changeset_id
+    self.tags = new_way.tags
+    self.nds = new_way.nds
+    self.visible = true
+    save_with_history!
+  end
 
-    WayNode.transaction do
-      nds = self.nds
-
-      WayNode.delete_all(['id = ?', self.id])
-
-      sequence = 1
-      nds.each do |n|
-        nd = WayNode.new
-        nd.id = [self.id, sequence]
-        nd.node_id = n
-        nd.save!
-        sequence += 1
-      end
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
-
-    old_way = OldWay.from_way(self)
-    old_way.timestamp = t
-    old_way.save_with_dependencies!
+    self.version = 0
+    self.visible = true
+    save_with_history!
   end
 
   def preconditions_ok?
     return false if self.nds.empty?
+    if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
+      raise OSM::APITooManyWayNodesError.new(self.nds.count, APP_CONFIG['max_number_of_way_nodes'])
+    end
     self.nds.each do |n|
       node = Node.find(:first, :conditions => ["id = ?", n])
       unless node and node.visible
-        return false
+        raise OSM::APIPreconditionFailedError.new("The node with id #{n} either does not exist, or is not visible")
       end
     end
     return true
   end
 
-  # Delete the way and it's relations, but don't really delete it - set its visibility to false and update the history etc to maintain wiki-like functionality.
-  def delete_with_relations_and_history(user)
-    if self.visible
-         # FIXME
-         # this should actually delete the relations,
-         # not just throw a PreconditionFailed if it's a member of a relation!!
+  def delete_with_history!(new_way, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError
+    end
+    
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Way.transaction do
+      check_consistency(self, new_way, user)
       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
-                             :conditions => [ "visible = 1 AND member_type='way' and member_id=?", self.id])
-        raise OSM::APIPreconditionFailedError
-      # end FIXME
+                             :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new("You need to make sure that this way is not a member of a relation.")
       else
-        self.user_id = user.id
+        self.changeset_id = new_way.changeset_id
         self.tags = []
         self.nds = []
         self.visible = false
-        self.save_with_history!
+        save_with_history!
       end
-    else
-      raise OSM::APIAlreadyDeletedError
     end
   end
 
-  # delete a way and it's nodes that aren't part of other ways, with history
-  def delete_with_relations_and_nodes_and_history(user)
-    # delete the nodes not used by other ways
-    self.unshared_node_ids.each do |node_id|
-      n = Node.find(node_id)
-      n.user_id = user.id
-      n.visible = false
-      n.save_with_history!
-    end
-    
-    self.user_id = user.id
-
-    self.delete_with_relations_and_history(user)
-  end
-
   # Find nodes that belong to this way only
   def unshared_node_ids
     node_ids = self.nodes.collect { |node| node.id }
@@ -263,4 +272,73 @@ class Way < ActiveRecord::Base
   def tags_as_hash
     return self.tags
   end
+
+  ##
+  # if any referenced nodes are placeholder IDs (i.e: are negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.nds.map! do |node_id|
+      if node_id < 0
+        new_id = id_map[:node][node_id]
+        raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
+        new_id
+      else
+        node_id
+      end
+    end
+  end
+
+  private
+  
+  def save_with_history!
+    t = Time.now
+
+    # update the bounding box, but don't save it as the controller knows the 
+    # lifetime of the change better. note that this has to be done both before 
+    # and after the save, so that nodes from both versions are included in the 
+    # bbox.
+    changeset.update_bbox!(bbox) unless nodes.empty?
+
+    Way.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      tags = self.tags
+      WayTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = WayTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+
+      nds = self.nds
+      WayNode.delete_all(['id = ?', self.id])
+      sequence = 1
+      nds.each do |n|
+        nd = WayNode.new
+        nd.id = [self.id, sequence]
+        nd.node_id = n
+        nd.save!
+        sequence += 1
+      end
+
+      old_way = OldWay.from_way(self)
+      old_way.timestamp = t
+      old_way.save_with_dependencies!
+
+      # update and commit the bounding box, now that way nodes 
+      # have been updated and we're in a transaction.
+      changeset.update_bbox!(bbox) unless nodes.empty?
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      changeset.save!
+    end
+  end
+
 end
index 4548674..fa9b433 100644 (file)
@@ -6,4 +6,9 @@ class WayTag < ActiveRecord::Base
   # FIXME add a real multipart key to waytags so that we can do eager loadin
 
   belongs_to :way, :foreign_key => 'id'
+  
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml
new file mode 100644 (file)
index 0000000..2b71161
--- /dev/null
@@ -0,0 +1,100 @@
+<table>
+
+  <tr>
+    <th>Created at:</th>
+    <td><%= h(changeset_details.created_at) %></td>
+  </tr>
+  
+  <tr>
+    <th>Closed at:</th>
+    <td><%= h(changeset_details.closed_at) %></td>
+  </tr>
+  
+  <% if changeset_details.user.data_public? %>
+    <tr>
+      <th>Belongs to:</th>
+      <td><%= link_to h(changeset_details.user.display_name), :controller => "user", :action => "view", :display_name => changeset_details.user.display_name %></td>
+    </tr>
+  <% end %>
+  
+  <% unless changeset_details.tags_as_hash.empty? %>
+    <tr valign="top">
+      <th>Tags:</th>
+      <td>
+        <table padding="0">
+          <%= render :partial => "tag", :collection => changeset_details.tags_as_hash %>
+        </table>
+      </td>
+    </tr>
+  <% else %>
+    <tr>
+      <th>Tags</th>
+      <td>There are no tags for this changeset</td>
+    </tr>
+  <% end %>
+
+  <% if changeset_details.max_lat.nil? or changeset_details.min_lat.nil? or changeset_details.max_lon.nil? or changeset_details.min_lon.nil? %>
+    <tr>
+      <td>No bounding box has been stored for this changeset.</td>
+    </tr>
+  <% else %>
+    <table>
+      <tr>
+        <td colspan="2" style="text-align:center"><b>Max Latitude: </b><%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+      <tr>
+        <td><b>Min Longitude: </b><%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%></td>
+        <td><b>Max Longitude: </b><%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+      <tr>
+        <td colspan="2" style="text-align:center"><b>Min Latitude: </b><%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+    </table>
+  <% end %>
+
+  <% unless @nodes.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @node_pages.item_count %> nodes:</th>
+      <td>
+        <table padding="0">
+          <% @nodes.each do |node| %>
+            <tr><td><%= link_to "Node #{node.id.to_s}, version #{node.version.to_s}", :action => "node", :id => node.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @node_pages, :page_param => "node_page"} %>
+  <% end %>
+  
+  <% unless @ways.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @way_pages.item_count %> ways:</th>
+      <td>
+        <table padding="0">
+          <% @ways.each do |way| %>
+            <tr><td><%= link_to "Way #{way.id.to_s}, version #{way.version.to_s}", :action => "way", :id => way.id.to_s %></td></tr>
+          <% end %>
+          <%=
+          #render :partial => "containing_relation", :collection => changeset_details.containing_relation_members 
+          %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @way_pages, :page_param => "way_page" } %>
+  <% end %>
+  
+  <% unless @relations.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @relation_pages.item_count %> relations:</th>
+      <td>
+        <table padding="0">
+          <% @relations.each do |relation| %>
+            <tr><td><%= link_to "Relation #{relation.id.to_s}, version #{relation.version.to_s}", :action => "relation", :id => relation.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @relation_pages, :page_param => "relation_page" } %>
+  <% end %>
+
+</table>
index ee5f22c..71a9dd3 100644 (file)
@@ -3,13 +3,23 @@
   <td><%= h(common_details.timestamp) %></td>
 </tr>
 
-<% if common_details.user.data_public %>
+<% if common_details.changeset.user.data_public? %>
   <tr>
     <th>Edited by:</th>
-    <td><%= link_to h(common_details.user.display_name), :controller => "user", :action => "view", :display_name => common_details.user.display_name %></td>
+    <td><%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %></td>
   </tr>
 <% end %>
 
+<tr>
+  <th>Version:</th>
+  <td><%= h(common_details.version) %></td>
+</tr>
+
+<tr>
+  <th>In changeset:</th>
+  <td><%= link_to common_details.changeset_id, :action => :changeset, :id => common_details.changeset_id %></td>
+</tr>
+
 <% unless common_details.tags_as_hash.empty? %>
   <tr valign="top">
     <th>Tags:</th>
diff --git a/app/views/browse/_paging_nav.rhtml b/app/views/browse/_paging_nav.rhtml
new file mode 100644 (file)
index 0000000..fcfbb05
--- /dev/null
@@ -0,0 +1,15 @@
+<tr><td colspan='2'>
+<% current_page = pages.current_page %>
+
+Showing page 
+<%= current_page.number %> (<%= current_page.first_item %><% 
+if (current_page.first_item < current_page.last_item) # if more than 1 trace on page 
+  %>-<%= current_page.last_item %><% 
+end %>
+of <%= pages.item_count %>)
+
+<% if pages.page_count > 1 %>
+| <%= pagination_links_each(pages, {}) { |n| link_to_page(n, page_param) } %>
+<% end %>
+</td>
+</tr>
diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml
new file mode 100644 (file)
index 0000000..57e39db
--- /dev/null
@@ -0,0 +1,18 @@
+<table width="100%">
+  <tr>
+    <td>
+      <h2>Changeset: <%= h(@changeset.id) %></h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+    <%= render :partial => "changeset_details", :object => @changeset %>
+    <hr />
+      <%= link_to "Download Changeset XML", :controller => "changeset", :action => "read" %> | 
+      <%= link_to "Download osmChange XML", :controller => "changeset", :action => "download" %>
+    </td>
+  </tr>
+</table>
index 2cd5cc9..fa7e13a 100644 (file)
@@ -1,12 +1,16 @@
-<h2><%= @nodes.length %> Recently Changed Nodes</h2> 
-<ul>
-<% @nodes.each do |node| 
-   name = node.tags_as_hash['name'].to_s 
-   if name.length == 0:
-     name = "(No name)"
+<h2><%= @changesets.length %> Recently Closed Changesets</h2> 
+<ul id="recently_changed">
+<% @changesets.each do |changeset|
+   if changeset.user.data_public?
+     user = changeset.user.display_name
+   else
+     user = "(anonymous)"
    end
-   name = name + " - " + node.id.to_s 
+   
+   cmt = changeset.tags_as_hash['comment'].to_s
+   cmt = "(no comment)" if cmt.length == 0
+   text = "#{changeset.id} by #{user} - #{cmt}"
 %>
-   <li><%= link_to h(name), :action => "node", :id => node.id %></li>
+   <li><%= link_to h(text), :action => "changeset", :id => changeset.id %></li>
 <% end %>
 </ul>
diff --git a/app/views/browse/not_found.rhtml b/app/views/browse/not_found.rhtml
new file mode 100644 (file)
index 0000000..1322a0a
--- /dev/null
@@ -0,0 +1 @@
+<p>Sorry, the <%= @type -%> with the id <%= params[:id] -%>, could not be found.</p>
index c17325a..f38b1dc 100644 (file)
@@ -189,7 +189,7 @@ page << <<EOJ
     if (size > 0.25) {
       setStatus("Unable to load: Bounding box size of " + size + " is too large (must be smaller than 0.25)");
     } else {
-      loadGML("/api/0.5/map?bbox=" + projected.toBBOX());
+      loadGML("/api/#{API_VERSION}/map?bbox=" + projected.toBBOX());
     }
   }
 
@@ -393,7 +393,7 @@ page << <<EOJ
     this.link.href = "";
     this.link.innerHTML = "Wait...";
 
-    new Ajax.Request("/api/0.5/" + this.type + "/" + this.feature.osm_id + "/history", {
+    new Ajax.Request("/api/#{API_VERSION}/" + this.type + "/" + this.feature.osm_id + "/history", {
       onComplete: OpenLayers.Function.bind(displayHistory, this)
     });
 
diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml
new file mode 100644 (file)
index 0000000..69d8933
--- /dev/null
@@ -0,0 +1,34 @@
+<tr>
+  <% cl = cycle('table0', 'table1') %>
+
+  <td class="<%= cl %>">
+    #<%= changeset.id %>
+
+  <td class="<%= cl %>">
+    <% if changeset.closed_at > DateTime.now %> (still editing)
+    <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %>
+
+  <td class="<%= cl %>">
+    <% if changeset.tags['comment'] %>
+      <%= changeset.tags['comment'] %>
+    <% else %>
+      (none)
+    <% end %>
+
+  <td class="<%= cl %>">
+    <% if changeset.min_lat.nil? %>
+      (no edits)
+    <% else 
+      lat1 = changeset.min_lat/GeoRecord::SCALE.to_f
+      lat2 = changeset.max_lat/GeoRecord::SCALE.to_f
+      lon1 = changeset.min_lon/GeoRecord::SCALE.to_f
+      lon2 = changeset.max_lon/GeoRecord::SCALE.to_f
+    %>
+      (<a href='/?lat=<%= lat1 %>&lon=<%= lon1 %>&zoom=14'><%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%></a>) to 
+      (<a href='/?lat=<%= lat2 %>&lon=<%= lon2 %>&zoom=14'><%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%></a>) 
+  <% end %>
+
+  <td class="<%= cl %>">
+    <%= link_to 'more', {:controller => 'browse', :action => 'changeset', :id => changeset.id}, {:title => 'View changeset details'} %> 
+
+</tr>
diff --git a/app/views/changeset/_changeset_paging_nav.rhtml b/app/views/changeset/_changeset_paging_nav.rhtml
new file mode 100644 (file)
index 0000000..df84a39
--- /dev/null
@@ -0,0 +1,12 @@
+<% current_page = @edit_pages.current_page %>
+
+Showing page 
+<%= current_page.number %> (<%= current_page.first_item %><% 
+if (current_page.first_item < current_page.last_item) # if more than 1 changeset on page 
+  %>-<%= current_page.last_item %><% 
+end %>
+of <%= @edit_pages.item_count %>)
+
+<% if @edit_pages.page_count > 1 %>
+ | <%= pagination_links_each(@edit_pages, {}) { |n| link_to(n, :display_name => @display_name, :page => n) }  %>
+<% end %>
diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml
new file mode 100644 (file)
index 0000000..cea1563
--- /dev/null
@@ -0,0 +1,15 @@
+<h1>Edits by <%= link_to(@display_name, {:controller=>'user', :action=>'view', :display_name=>@display_name}) %></h1>
+<%= render :partial => 'changeset_paging_nav' %>
+
+<table id="keyvalue" cellpadding="3">
+  <tr>
+    <th>ID</th>
+    <th>Saved at</th>
+    <th>Comment</th>
+    <th>Area</th>
+    <th></th>
+  </tr>
+  <%= render :partial => 'changeset', :collection => @edits unless @edits.nil? %>
+</table>
+
+<%= render :partial => 'changeset_paging_nav' %>
index 87f5e9f..a69f4fd 100644 (file)
@@ -5,24 +5,28 @@
 <% form_for :diary_entry do |f| %>
   <table>
     <tr valign="top">
-      <th>Subject</th>
+      <td class="fieldName">Subject:</td>
       <td><%= f.text_field :title, :size => 60 %></td>
     </tr>
     <tr valign="top">
-      <th>Body</th>
+      <td class="fieldName">Body:</td>
       <td><%= f.text_area :body, :cols => 80 %></td>
     </tr>
     <tr valign="top">
-      <th>Location</th>
+      <td class="fieldName">Location:</td>
       <td>
         <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px; display: none;"></div>
         <span class="location">Latitude: <%= f.text_field :latitude, :size => 20, :id => "latitude" %> Longitude: <%= f.text_field :longitude, :size => 20, :id => "longitude" %></span>
         <a href="javascript:openMap()" id="usemap">use map</a>
+        <br/><br/>
       </td>
     </tr>
     <tr>
-      <th></th>
-      <td><%= submit_tag 'Save' %></td>
+      <td></td>
+      <td>
+         <%= submit_tag 'Save' %>
+         <%# TODO: button should say 'publish' or 'save changes' depending on new/edit state %>
+      </td>
     </tr>
   </table>
 <% end %>
index 3d1f0bb..38157b1 100644 (file)
@@ -4,29 +4,33 @@
  <%= image_tag url_for_file_column(@this_user, "image") %>
 <% end %>
 
-<br />
 
 <% if @this_user %>
   <% if @user == @this_user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% else %>
   <% if @user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% end %>
 
-<h3>Recent diary entries:</h3>
 
-<%= render :partial => 'diary_entry', :collection => @entries %>
+<% if @entries.empty? %>
+  <p>No diary entries</p>
+<% else %>
+  <p>Recent diary entries:</p>
 
-<%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
-<% if @entry_pages.current.next and @entry_pages.current.previous %>
-|
-<% end %>
-<%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
+  <hr />
 
-<br />
+  <%= render :partial => 'diary_entry', :collection => @entries %>
+       
+  <%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
+  <% if @entry_pages.current.next and @entry_pages.current.previous %>|<% end %>
+  <%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
+
+  <br />
+<% end %>
 
 <%= rss_link_to :action => 'rss' %>
 
diff --git a/app/views/diary_entry/no_such_entry.rhtml b/app/views/diary_entry/no_such_entry.rhtml
new file mode 100644 (file)
index 0000000..1ebcf26
--- /dev/null
@@ -0,0 +1,2 @@
+<h2>No entry with the id: <%= h(params[:id]) %></h2>
+<p>Sorry, there is no diary entry or comment with the id <%=h params[:id] -%>, or no id was given. Please check your spelling, or maybe the link you clicked is wrong.</p>
index 34d2ecb..cc8f719 100644 (file)
@@ -7,6 +7,7 @@
     <%= stylesheet_link_tag 'site' %>
     <%= stylesheet_link_tag 'print', :media => "print" %>
     <%= tag("link", { :rel => "search", :type => "application/opensearchdescription+xml", :title => "OpenStreetMap Search", :href => "/opensearch/osm.xml" }) %>
+    <%= tag("meta", { :name => "description", :content => "OpenStreetMap is the free wiki world map." }) %>
     <%= yield :head %>
     <title>OpenStreetMap<%= ' | '+ h(@title) if @title %></title>
   </head>
         </div>
 
         <div id="cclogo" class="button" style="width: 88px">
-          <a href="http://creativecommons.org/licenses/by-sa/2.0/"><img src="/images/cc_button.png" border="0" alt="" /></a>
+          <%= link_to image_tag("cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %>
         </div>
       </center>
     </div>
index 6d45d33..263e30e 100644 (file)
@@ -1,9 +1,10 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason 
+%>
 
 <tr class="inbox-row<%= "-unread" if not message_summary.message_read? %>">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= message_summary.sent_on %></td>
   <% if message_summary.message_read? %>
     <td><%= button_to 'Mark as unread', :controller => 'message', :action => 'mark', :message_id => message_summary.id, :mark => 'unread' %></td>
   <% else %>
index f0d87aa..91fafe9 100644 (file)
@@ -1,7 +1,8 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason
+%>
 
 <tr class="inbox-row">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= sent_message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= sent_message_summary.sent_on %></td>
 </tr>
index d7bb18f..17f3588 100644 (file)
@@ -1,7 +1,4 @@
-<% user_id = params[:user_id] || @user_id %>
-<% display_name = User.find_by_id(user_id).display_name %>
-
-<h2>Send a new message to <%= h(display_name) %></h2>
+<h2>Send a new message to <%= h(@to_user.display_name) %></h2>
 
 <% if params[:display_name] %>
 <p>Writing a new message to <%= h(params[:display_name]) %></p>  
@@ -10,7 +7,7 @@
 
 <%= error_messages_for 'message' %>
 
-<% form_for :message, :url => { :action => "new", :user_id => user_id } do |f| %>
+<% form_for :message, :url => { :action => "new", :user_id => @to_user.id } do |f| %>
   <table>
     <tr valign="top">
       <th>Subject</th>
diff --git a/app/views/message/no_such_user.rhtml b/app/views/message/no_such_user.rhtml
new file mode 100644 (file)
index 0000000..c18733a
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>No such user or message</h1>
+<p>Sorry there is no user or message with that name or id</p>
index 501af74..8a57ccf 100644 (file)
@@ -1,34 +1,53 @@
-<h2>User details</h2>
+<h2>My settings</h2>
 <%= error_messages_for 'user' %>
 <% form_for :user, @user do |f| %>
-<table style="width : 100%">
-  <tr><td>Email</td><td><%= f.text_field :email %></td></tr>
-  <tr><td>Mapper since</td><td><%= @user.creation_time %> (<%= time_ago_in_words(@user.creation_time) %> ago)</td></tr>
-  <tr><td>Display Name</td><td><%= f.text_field :display_name %></td></tr>
-  <tr><td>Password</td><td><%= f.password_field :pass_crypt, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
-  <tr><td>Confirm Password</td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
+<table id="accountForm">
+  <tr><td class="fieldName">Display Name : </td><td><%= f.text_field :display_name %></td></tr>
+  <tr><td class="fieldName">Email : </td><td><%= f.text_field :email, {:size => 50, :maxlength => 255} %> <span class="minorNote">(not displayed publicly)</span></td></tr>
+  <tr><td class="fieldName" style="padding-bottom:0px;">Password : </td><td style="padding-bottom:0px;"><%= f.password_field :pass_crypt, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr>
+  <td class="fieldName" valign="top">Public editing :</td>
+  <td>
+<% if @user.data_public? %>
+  Enabled. Not anonymous <span class="minorNote">(<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits" target="_new">what's this?</a>)</span>
+<% else %>
+
+  Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
+  <b>You will need to do this if you want to use the online editor and it is encouraged</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits" target="_new">find out why</a>).
+  <br /><br >
+  This action cannot be reversed and all new users are now public by default.
+  <br /><br />
+  <%= button_to "Make all my edits public", :action => :go_public %>
+  <br /><br />
+  
+<% end %>
+  </td>
+  </tr>
 
-  <tr><td valign="top">Description</td><td><%= f.text_area :description, :class => "editDescription" %></td></tr>
+  
+   
+  <tr><td class="fieldName" valign="top">Profile Description : </td><td><%= f.text_area :description, :rows => '5', :cols => '60' %><br /><br /></td></tr>
 
-  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td>Your home</td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
+  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td class="fieldName">Home Location : </td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
 
   <tr><td></td><td>
   <p>Update home location when I click on the map? <input type="checkbox" value="1" <% unless @user.home_lat and @user.home_lon %> checked="checked" <% end %> id="updatehome" /> </p>
-  <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px;"></div>
+  <div id="map" style="border:1px solid black; position:relative; width:500px; height:400px;"></div>
   </td></tr>
+  
+  <tr><td></td><td align=right><br/></br><%= submit_tag 'Save Changes' %></td></tr>
 </table>
-<%= submit_tag 'Save Changes' %>
+<br/>
+
 <% end %>
 
 <%= render :partial => 'friend_map' %>
 
-<h2>Public editing</h2>
-<% if @user.data_public? %>
-  All your edits are public.
-<% else %>
-Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
-<b>You will need to do this if you want to use the online editor and it is encouraged</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits">find out why</a>).
-This action cannot be reversed and all new users are now public by default.
-  <br /><br />
-  <%= button_to "Make all my edits public", :action => :go_public %>
-<% end %>
+<br/>
+<br/>
+<br/>
+<%= link_to 'return to profile', :controller => 'user', :action => @user.display_name %>
+<br/>
+<br/>
\ No newline at end of file
index ff988f0..0422d21 100644 (file)
@@ -1,13 +1,13 @@
-<h1>Login:</h1><br />
-Please login or <%= link_to 'create an account', :controller => 'user', :action => 'new' %>.<br />
+<h1>Login</h1>
+
+<p>Please login or <%= link_to 'create an account', :controller => 'user', :action => 'new' %>.</p>
 
 <% form_tag :action => 'login' do %>
 <%= hidden_field_tag('referer', h(params[:referer])) %>
 <table>
-  <tr><td>Email Address or username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password:</td><td><%= password_field('user', 'password',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Email Address or Username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1}) %></td></tr>
+  <tr><td class="fieldName">Password:</td><td><%= password_field('user', 'password',{:size => 28, :maxlength => 255, :tabindex => 2}) %> <span class="minorNote">(<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align="right"><%= submit_tag 'Login', :tabindex => 3 %></td></tr>
 </table>
-
-<br />
-<%= submit_tag 'Login' %>
-<% end %> (<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)
+<% end %>
index d0b5a96..a19582f 100644 (file)
@@ -1,4 +1,4 @@
-<h1>Create a user account</h1>
+<h1>Create a User Account</h1>
 
 <% if Acl.find_by_address(request.remote_ip, :conditions => {:k => "no_account_creation"}) %>
 
 <%= error_messages_for 'user' %>
 
 <% form_tag :action => 'save' do %>
-<table>
-  <tr><td>Email Address</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Email Address</td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Display Name</td><td><%= text_field('user', 'display_name',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password</td><td><%= password_field('user', 'pass_crypt',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Password</td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
+<table id="loginForm">
+  <tr><td class="fieldName">Email Address : </td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1}) %></td></tr>
+  <tr><td class="fieldName">Confirm Email Address : </td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255, :tabindex => 2}) %></td></tr>
+  <tr><td></td><td><span class="minorNote">Not displayed publicly (see <a href="http://wiki.openstreetmap.org/index.php/Privacy_Policy" title="wiki privacy policy including section on email addresses">privacy policy)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Display Name : </td><td><%= text_field('user', 'display_name',{:size => 30, :maxlength => 255, :tabindex => 3}) %></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Password : </td><td><%= password_field('user', 'pass_crypt',{:size => 30, :maxlength => 255, :tabindex => 4}) %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 30, :maxlength => 255, :tabindex => 5}) %></td></tr>
+  
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align=right><input type="submit" value="Signup" tabindex="6"></td></tr>
 </table>
-<br>
-<br>
-<input type="submit" value="Signup">
-
 <% end %>
 
 <% end %>
index 438de83..4f2a168 100644 (file)
@@ -2,13 +2,17 @@
 <h2><%= h(@this_user.display_name) %></h2>
 <div id="userinformation">
 <% if @user and @this_user.id == @user.id %>
+<!-- Displaying user's own profile page -->
 <%= link_to 'my diary', :controller => 'diary_entry', :action => 'list', :display_name => @user.display_name %>
 | <%= link_to 'new diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+| <%= link_to 'my edits', :controller => 'changeset', :action => 'list', :display_name => @user.display_name %>
 | <%= link_to 'my traces', :controller => 'trace', :action=>'mine' %>
 | <%= link_to 'my settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
 <% else %>
+<!-- Displaying another user's profile page -->
 <%= link_to 'send message', :controller => 'message', :action => 'new', :user_id => @this_user.id %>
 | <%= link_to 'diary', :controller => 'diary_entry', :action => 'list', :display_name => @this_user.display_name %>
+| <%= link_to 'edits', :controller => 'changeset', :action => 'list', :display_name => @this_user.display_name %>
 | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %>
 | <% if @user and @user.is_friends_with?(@this_user) %>
   <%= link_to 'remove as friend', :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %>
 <% end %>
 </div>
 
+<% if @this_user != nil %>
+<P>
+<b>Mapper since : </b><%= @this_user.creation_time %> (<%= time_ago_in_words(@this_user.creation_time) %> ago)
+</P>
+<% end %>
+  
 <h3>User image</h3>
 <% if @this_user.image %>
   <%= image_tag url_for_file_column(@this_user, "image") %>
     <% end %>
   <% end %>
 <% end %>
+
+<br/>
+<br/>
+<% if @user and @this_user.id == @user.id %>
+<%= link_to 'change your settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
+<% end %>
\ No newline at end of file
index 85ebe9f..7400a7b 100644 (file)
@@ -3,8 +3,10 @@ standard_settings: &standard_settings
   max_request_area: 0.25
   # Number of GPS trace/trackpoints returned per-page
   tracepoints_per_page: 5000
-  # Maximum number of nodes
+  # Maximum number of nodes that will be returned by the api in a map request
   max_number_of_nodes: 50000
+  # Maximum number of nodes that can be in a way (checked on save)
+  max_number_of_way_nodes: 2000
  
 development:
   <<: *standard_settings
index b884f3b..cc3f9a1 100644 (file)
@@ -16,6 +16,7 @@ development:
   username: openstreetmap
   password: openstreetmap
   host: localhost
+  encoding: utf8
 
 # Warning: The database defined as 'test' will be erased and
 # re-generated from your development database when you run 'rake'.
@@ -23,14 +24,15 @@ development:
 test:
   adapter: mysql
   database: osm_test
-  username: root
-  password:
+  username: osm_test
+  password: osm_test
   host: localhost
+  encoding: utf8
 
 production:
   adapter: mysql
-  database: openstreetmap
-  username: openstreetmap
-  password: openstreetmap
-  host: db.openstreetmap.org
-
+  database: osm
+  username: osm
+  password: osm
+  host: localhost
+  encoding: utf8
index e6af619..ce1216b 100644 (file)
@@ -5,13 +5,16 @@
 ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
 
 # Set the server URL
 SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
 
+# Set the generator
+GENERATOR = ENV['OSM_SERVER_GENERATOR'] || 'OpenStreetMap server'
+
 # Application constants needed for routes.rb - must go before Initializer call
-API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
+API_VERSION = ENV['OSM_API_VERSION'] || '0.6'
 
 # Set application status - possible settings are:
 #
@@ -37,6 +40,16 @@ Rails::Initializer.run do |config|
     config.frameworks -= [ :active_record ]
   end
 
+  # Specify gems that this application depends on. 
+  # They can then be installed with "rake gems:install" on new installations.
+  # config.gem "bj"
+  # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+  # config.gem "aws-s3", :lib => "aws/s3"
+  config.gem 'composite_primary_keys', :version => '1.1.0'
+  config.gem 'libxml-ruby', :version => '>= 1.1.1', :lib => 'libxml'
+  config.gem 'rmagick', :lib => 'RMagick'
+  config.gem 'mysql'
+
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.
   # :all can be used as a placeholder for all plugins not explicitly named
@@ -63,6 +76,12 @@ Rails::Initializer.run do |config|
   # (create the session table with 'rake db:sessions:create')
   config.action_controller.session_store = :sql_session_store
 
+  # We will use the old style of migrations, rather than the newer
+  # timestamped migrations that were introduced with Rails 2.1, as
+  # it will be confusing to have the numbered and timestamped migrations
+  # together in the same folder.
+  config.active_record.timestamped_migrations = false
+
   # Use SQL instead of Active Record's schema dumper when creating the test database.
   # This is necessary if your schema can't be completely dumped by the schema dumper,
   # like if you have constraints or database-specific column types
@@ -72,5 +91,5 @@ Rails::Initializer.run do |config|
   # config.active_record.observers = :cacher, :garbage_collector
 
   # Make Active Record use UTC-base instead of local time
-  config.active_record.default_timezone = :utc
+  config.active_record.default_timezone = :utc
 end
index 09a451f..d67452f 100644 (file)
@@ -12,7 +12,6 @@ config.whiny_nils = true
 config.action_controller.consider_all_requests_local = true
 config.action_view.debug_rjs                         = true
 config.action_controller.perform_caching             = false
-config.action_view.cache_template_extensions         = false
 
 # Don't care if the mailer can't send
-config.action_mailer.raise_delivery_errors = false
\ No newline at end of file
+config.action_mailer.raise_delivery_errors = false
diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb
deleted file mode 100644 (file)
index 430bcfa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'rubygems'
-gem 'composite_primary_keys', '= 0.9.93'
-require 'composite_primary_keys'
index d9d3471..07f7966 100644 (file)
@@ -1,7 +1,3 @@
-require 'rubygems'
-gem 'libxml-ruby', '>= 1.1.1'
-require 'libxml'
-
 # This is required otherwise libxml writes out memory errors to
 # the standard output and exits uncleanly
 LibXML::XML::Error.set_handler do |message|
index c54247c..ec1c2de 100644 (file)
@@ -12,6 +12,7 @@ secondary             0xFDBF6F        1       -
 tertiary               0xFEFECB        1       -
 unclassified   0xE8E8E8        1       -
 residential            0xE8E8E8        1       -
+road                   0xAAAAAA        1       -
 footway                        0xFF6644        -       -
 cycleway               0xFF6644        -       -
 bridleway              0xFF6644        -       -
index b7d5709..4431c47 100644 (file)
@@ -1,10 +1,22 @@
 ActionController::Routing::Routes.draw do |map|
 
   # API
+  map.connect "api/capabilities", :controller => 'api', :action => 'capabilities'
+  
+  map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create'
+  map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/download", :controller => 'changeset', :action => 'download', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/expand_bbox", :controller => 'changeset', :action => 'expand_bbox', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
+  map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
+  map.connect "api/#{API_VERSION}/changesets", :controller => 'changeset', :action => 'query'
+  
   map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create'
   map.connect "api/#{API_VERSION}/node/:id/ways", :controller => 'way', :action => 'ways_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/relations", :controller => 'relation', :action => 'relations_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/history", :controller => 'old_node', :action => 'history', :id => /\d+/
+  map.connect "api/#{API_VERSION}/node/:id/:version", :controller => 'old_node', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
@@ -14,12 +26,12 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/way/:id/history", :controller => 'old_way', :action => 'history', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/full", :controller => 'way', :action => 'full', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/relations", :controller => 'relation', :action => 'relations_for_way', :id => /\d+/
+  map.connect "api/#{API_VERSION}/way/:id/:version", :controller => 'old_way', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
   map.connect "api/#{API_VERSION}/ways", :controller => 'way', :action => 'ways', :id => nil
 
-  map.connect "api/#{API_VERSION}/capabilities", :controller => 'api', :action => 'capabilities'
   map.connect "api/#{API_VERSION}/relation/create", :controller => 'relation', :action => 'create'
   map.connect "api/#{API_VERSION}/relation/:id/relations", :controller => 'relation', :action => 'relations_for_relation', :id => /\d+/
   map.connect "api/#{API_VERSION}/relation/:id/history", :controller => 'old_relation', :action => 'history', :id => /\d+/
@@ -52,7 +64,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/gpx/:id/details", :controller => 'trace', :action => 'api_details'
   map.connect "api/#{API_VERSION}/gpx/:id/data", :controller => 'trace', :action => 'api_data'
   
-  # Potlatch API
+  # AMF (ActionScript) API
   
   map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read'
   map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write'
@@ -67,6 +79,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/browse/node/:id/history', :controller => 'browse', :action => 'node_history', :id => /\d+/
   map.connect '/browse/relation/:id', :controller => 'browse', :action => 'relation', :id => /\d+/
   map.connect '/browse/relation/:id/history', :controller => 'browse', :action => 'relation_history', :id => /\d+/
+  map.connect '/browse/changeset/:id', :controller => 'browse', :action => 'changeset', :id => /\d+/
   
   # web site
 
@@ -124,6 +137,7 @@ ActionController::Routing::Routes.draw do |map|
 
   # user pages
   map.connect '/user/:display_name', :controller => 'user', :action => 'view'
+  map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list'
   map.connect '/user/:display_name/make_friend', :controller => 'user', :action => 'make_friend'
   map.connect '/user/:display_name/remove_friend', :controller => 'user', :action => 'remove_friend'
   map.connect '/user/:display_name/diary', :controller => 'diary_entry', :action => 'list'
index 447c636..f000293 100644 (file)
--- a/db/README
+++ b/db/README
@@ -25,12 +25,12 @@ $ mysql -u <uid> -p
 > flush privileges;
 > exit
 
-Creating functions
-====================
+Creating functions For MySQL
+==============================
 
 Run this command in the db/functions directory:
 
-$ make
+$ make libmyosm.so
 
 Make sure the db/functions directory is on the MySQL server's library
 path and restart the MySQL server. 
@@ -49,6 +49,22 @@ $ mysql -u <uid> -p openstreetmap
 > create function maptile_for_point returns integer soname 'libmyosm.so';
 > exit
 
+Creating functions for PgSQL
+==============================
+
+Run this command in the db/functions directory:
+
+$ make libmyosm.so
+
+Now create the function as follows:
+
+$ psql openstreetmap
+(This may need authentication or a -u <dbowneruid>)
+
+> CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+  AS '/path/to/rails-port/db/functions/libpgosm', 'maptile_for_point'
+  LANGUAGE C STRICT;
+
 Creating database skeleton tables
 ===================================
 
index 7652862..1bdddce 100644 (file)
@@ -1,5 +1,7 @@
 QTDIR=../../lib/quad_tile
 
+PGSQLINC=/usr/include/postgresql/8.3/server/
+
 OS=$(shell uname -s)
 ifeq (${OS},Darwin)
     LDFLAGS=-bundle
@@ -7,11 +9,22 @@ else
     LDFLAGS=-shared
 endif
 
-libmyosm.so: quadtile.o maptile.o
-       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile.o
+all: libmyosm.so libpgosm.so
+
+clean:
+       $(RM) *.so *.o
+
+libmyosm.so: quadtile.o maptile-mysql.o
+       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile-mysql.o
+
+libpgosm.so: maptile-pgsql.o
+       cc ${LDFLAGS} -o libpgosm.so maptile-pgsql.o
 
 quadtile.o: quadtile.c ${QTDIR}/quad_tile.h
        cc `mysql_config --include` -I${QTDIR} -fPIC -O3 -c -o quadtile.o quadtile.c
 
-maptile.o: maptile.c
-       cc `mysql_config --include` -fPIC -O3 -c -o maptile.o maptile.c
+maptile-mysql.o: maptile.c
+       cc `mysql_config --include` -fPIC -O3 -DUSE_MYSQL -c -o maptile-mysql.o maptile.c
+
+maptile-pgsql.o: maptile.c
+       cc -I${PGSQLINC} -O3 -fPIC -DUSE_PGSQL -c -o maptile-pgsql.o maptile.c
\ No newline at end of file
index f96f9c2..c2baac5 100644 (file)
@@ -1,3 +1,31 @@
+#ifndef USE_MYSQL
+#ifndef USE_PGSQL
+#error One of USE_MYSQL or USE_PGSQL must be defined
+#endif
+#endif
+
+#include <math.h>
+
+/* The real maptile-for-point functionality is here */
+
+static long long internal_maptile_for_point(double lat, double lon, long long zoom)
+{
+   double       scale = pow(2, zoom);
+   double       r_per_d = M_PI / 180;
+   unsigned int x;
+   unsigned int y;
+
+   x = floor((lon + 180.0) * scale / 360.0);
+   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+
+   return (x << zoom) | y;
+}
+
+#ifdef USE_MYSQL
+#ifdef USE_PGSQL
+#error ONLY one of USE_MYSQL and USE_PGSQL should be defined
+#endif
+
 #include <my_global.h>
 #include <my_sys.h>
 #include <m_string.h>
@@ -27,13 +55,39 @@ long long maptile_for_point(UDF_INIT *initid, UDF_ARGS *args, char *is_null, cha
    double       lat = *(long long *)args->args[0] / 10000000.0;
    double       lon = *(long long *)args->args[1] / 10000000.0;
    long long    zoom = *(long long *)args->args[2];
-   double       scale = pow(2, zoom);
-   double       r_per_d = M_PI / 180;
-   unsigned int x;
-   unsigned int y;
+   
+   return internal_maptile_for_point(lat, lon, zoom);
+}
+#endif
 
-   x = floor((lon + 180.0) * scale / 360.0);
-   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+#ifdef USE_PGSQL
+#include <postgres.h>
+#include <fmgr.h>
 
-   return (x << zoom) | y;
+Datum
+maptile_for_point(PG_FUNCTION_ARGS)
+{
+  double lat = PG_GETARG_INT64(0) / 10000000.0;
+  double lon = PG_GETARG_INT64(1) / 10000000.0;
+  int zoom = PG_GETARG_INT32(2);
+  
+  PG_RETURN_INT32(internal_maptile_for_point(lat, lon, zoom));
 }
+
+PG_FUNCTION_INFO_V1(maptile_for_point);
+
+/*
+ * To bind this into PGSQL, try something like:
+ *
+ * CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+ *  AS '/path/to/rails-port/db/functions/libpgosm', 'maptile_for_point'
+ *  LANGUAGE C STRICT;
+ *
+ * (without all the *s)
+ */
+
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+#endif
index 2c80dd8..3e33779 100644 (file)
@@ -16,7 +16,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_nodes, :id, :bigint_auto_64
 
     create_table "current_segments", innodb_table do |t|
       t.column "id",        :bigint,   :limit => 64,                 :null => false
@@ -32,7 +32,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_segments", ["node_a"], :name => "current_segments_a_idx"
     add_index "current_segments", ["node_b"], :name => "current_segments_b_idx"
 
-    change_column "current_segments", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_segments, :id, :bigint_auto_64
 
     create_table "current_way_segments", innodb_table do |t|
       t.column "id",          :bigint, :limit => 64
@@ -50,21 +50,17 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     add_index "current_way_tags", ["id"], :name => "current_way_tags_id_idx"
-    execute "CREATE FULLTEXT INDEX `current_way_tags_v_idx` ON `current_way_tags` (`v`)"
+    add_fulltext_index "current_way_tags", "v"
 
     create_table "current_ways", myisam_table do |t|
-      t.column "id",        :bigint,   :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,   :limit => 20
       t.column "timestamp", :datetime
       t.column "visible",   :boolean
     end
 
-    add_primary_key "current_ways", ["id"]
-
-    change_column "current_ways", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "diary_entries", myisam_table do |t|
-      t.column "id",         :bigint,   :limit => 20, :null => false
+      t.column "id",         :bigint_pk, :null => false
       t.column "user_id",    :bigint,   :limit => 20, :null => false
       t.column "title",      :string
       t.column "body",       :text
@@ -72,21 +68,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "updated_at", :datetime
     end
 
-    add_primary_key "diary_entries", ["id"]
-
-    change_column "diary_entries", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "friends", myisam_table do |t|
-      t.column "id",             :bigint,  :limit => 20, :null => false
+      t.column "id",             :bigint_pk, :null => false
       t.column "user_id",        :bigint,  :limit => 20, :null => false
       t.column "friend_user_id", :bigint,  :limit => 20, :null => false
     end
 
-    add_primary_key "friends", ["id"]
     add_index "friends", ["friend_user_id"], :name => "user_id_idx"
 
-    change_column "friends", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gps_points", myisam_table do |t|
       t.column "altitude",  :float
       t.column "user_id",   :integer,  :limit => 20
@@ -104,16 +93,13 @@ class CreateOsmDb < ActiveRecord::Migration
     create_table "gpx_file_tags", myisam_table do |t|
       t.column "gpx_id", :bigint,  :limit => 64, :default => 0, :null => false
       t.column "tag",    :string
-      t.column "id",     :integer, :limit => 20, :null => false
+      t.column "id",     :bigint_pk, :null => false
     end
 
-    add_primary_key "gpx_file_tags", ["id"]
     add_index "gpx_file_tags", ["gpx_id"], :name => "gpx_file_tags_gpxid_idx"
 
-    change_column "gpx_file_tags", "id", :integer, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_files", myisam_table do |t|
-      t.column "id",          :bigint,   :limit => 64,                   :null => false
+      t.column "id",          :bigint_pk_64,                   :null => false
       t.column "user_id",     :bigint,   :limit => 20
       t.column "visible",     :boolean,                :default => true, :null => false
       t.column "name",        :string,                 :default => "",   :null => false
@@ -126,12 +112,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "inserted",    :boolean
     end
 
-    add_primary_key "gpx_files", ["id"]
     add_index "gpx_files", ["timestamp"], :name => "gpx_files_timestamp_idx"
     add_index "gpx_files", ["visible", "public"], :name => "gpx_files_visible_public_idx"
 
-    change_column "gpx_files", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_pending_files", myisam_table do |t|
       t.column "originalname", :string
       t.column "tmpname",      :string
@@ -139,7 +122,7 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     create_table "messages", myisam_table do |t|
-      t.column "id",                :bigint,   :limit => 20,                    :null => false
+      t.column "id",                :bigint_pk,                                 :null => false
       t.column "user_id",           :bigint,   :limit => 20,                    :null => false
       t.column "from_user_id",      :bigint,   :limit => 20,                    :null => false
       t.column "from_display_name", :string,                 :default => ""
@@ -150,21 +133,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "to_user_id",        :bigint,   :limit => 20,                    :null => false
     end
 
-    add_primary_key "messages", ["id"]
     add_index "messages", ["from_display_name"], :name => "from_name_idx"
 
-    change_column "messages", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "nodes", myisam_table do |t|
       t.column "id",        :bigint,  :limit => 64
       t.column "latitude",  :double
@@ -194,7 +170,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     create_table "users", innodb_table do |t|
       t.column "email",         :string
-      t.column "id",            :bigint,   :limit => 20,                    :null => false
+      t.column "id",            :bigint_pk,                    :null => false
       t.column "token",         :string
       t.column "active",        :integer,                :default => 0,     :null => false
       t.column "pass_crypt",    :string
@@ -211,12 +187,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "home_zoom",     :integer,  :limit => 2,  :default => 3
     end
 
-    add_primary_key "users", ["id"]
     add_index "users", ["email"], :name => "users_email_idx"
     add_index "users", ["display_name"], :name => "users_display_name_idx"
 
-    change_column "users", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "way_segments", myisam_table do |t|
       t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
       t.column "segment_id",  :integer
@@ -226,7 +199,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     add_primary_key "way_segments", ["id", "version", "sequence_id"]
 
-    change_column "way_segments", "sequence_id", :bigint, :limit => 11, :null => false, :options => "AUTO_INCREMENT"
+    change_column "way_segments", "sequence_id", :bigint_auto_11
 
     create_table "way_tags", myisam_table do |t|
       t.column "id",      :bigint,  :limit => 64, :default => 0, :null => false
@@ -248,7 +221,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_primary_key "ways", ["id", "version"]
     add_index "ways", ["id"], :name => "ways_id_version_idx"
 
-    change_column "ways", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "ways", "version", :bigint_auto_20
   end
 
   def self.down
index b99055e..b13a920 100644 (file)
@@ -29,7 +29,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_ways", "user_id", :bigint, :limit => 20, :null => false
     change_column "current_ways", "timestamp", :datetime, :null => false
     change_column "current_ways", "visible", :boolean, :null => false
-    execute "ALTER TABLE current_ways ENGINE = InnoDB"
+    change_engine "current_ways", "InnoDB"
 
     change_column "diary_entries", "title", :string, :null => false
     change_column "diary_entries", "body", :text, :null => false
@@ -39,7 +39,9 @@ class CleanupOsmDb < ActiveRecord::Migration
     add_index "friends", ["user_id"], :name => "friends_user_id_idx"
 
     remove_index "gps_points", :name => "points_uid_idx"
+    remove_index "gps_points", :name => "points_idx"
     remove_column "gps_points", "user_id"
+    add_index "gps_points", ["latitude", "longitude"], :name => "points_idx"
     change_column "gps_points", "trackid", :integer, :null => false
     change_column "gps_points", "latitude", :integer, :null => false
     change_column "gps_points", "longitude", :integer, :null => false
@@ -150,15 +152,11 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "nodes", "id", :bigint, :limit => 64
 
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     remove_index "messages", :name => "messages_to_user_id_idx"
     change_column "messages", "message_read", :boolean, :default => false
     change_column "messages", "sent_on", :datetime
@@ -195,7 +193,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "diary_entries", "body", :text
     change_column "diary_entries", "title", :string, :default => nil
 
-    execute "ALTER TABLE current_ways ENGINE = MyISAM"
+    change_engine "current_ways", "MyISAM"
     change_column "current_ways", "visible", :boolean
     change_column "current_ways", "timestamp", :datetime
     change_column "current_ways", "user_id", :bigint, :limit => 20
@@ -223,6 +221,6 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_nodes", "user_id", :bigint, :limit => 20
     change_column "current_nodes", "longitude", :double
     change_column "current_nodes", "latitude", :double
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column "current_nodes", "id", :bigint_auto_64
   end
 end
index 7b1c754..4de0dd4 100644 (file)
@@ -1,6 +1,6 @@
 class SqlSessionStoreSetup < ActiveRecord::Migration
   def self.up
-    create_table "sessions", :options => "ENGINE=InnoDB" do |t|
+    create_table "sessions", :options => innodb_option do |t|
       t.column "session_id", :string
       t.column "data",       :text
       t.column "created_at", :timestamp
index 92f01bf..a6e81d2 100644 (file)
@@ -13,18 +13,15 @@ class UserEnhancements < ActiveRecord::Migration
     add_primary_key "user_preferences", ["user_id", "k"]
 
     create_table "user_tokens", innodb_table do |t|
-      t.column "id",      :bigint,   :limit => 20, :null => false
+      t.column "id",      :bigint_pk, :null => false
       t.column "user_id", :bigint,   :limit => 20, :null => false
       t.column "token",   :string,   :null => false
       t.column "expiry",  :datetime, :null => false
     end
 
-    add_primary_key "user_tokens", ["id"]
     add_index "user_tokens", ["token"], :name => "user_tokens_token_idx", :unique => true
     add_index "user_tokens", ["user_id"], :name => "user_tokens_user_id_idx"
 
-    change_column "user_tokens", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     User.find(:all, :conditions => "token is not null").each do |user|
       UserToken.create(:user_id => user.id, :token => user.token, :expiry => 1.week.from_now)
     end
index 51a4d13..74d85d1 100644 (file)
@@ -1,6 +1,6 @@
 class TileTracepoints < ActiveRecord::Migration
   def self.up
-    add_column "gps_points", "tile", :integer, :null => false, :unsigned => true
+    add_column "gps_points", "tile", :four_byte_unsigned
     add_index "gps_points", ["tile"], :name => "points_tile_idx"
     remove_index "gps_points", :name => "points_idx"
 
index 3a50cc9..dc4755a 100644 (file)
@@ -33,10 +33,12 @@ class TileNodes < ActiveRecord::Migration
   end
 
   def self.up
+    remove_index "current_nodes", :name => "current_nodes_timestamp_idx"
+
     rename_table "current_nodes", "current_nodes_v5"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :integer,                                :null => false
       t.column "longitude", :integer,                                :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -46,17 +48,17 @@ class TileNodes < ActiveRecord::Migration
       t.column "tile",      :integer,                                :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
     add_index "current_nodes", ["tile"], :name => "current_nodes_tile_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-    change_column "current_nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "current_nodes", "tile", :four_byte_unsigned
 
     upgrade_table "current_nodes_v5", "current_nodes", Node
     
     drop_table "current_nodes_v5"
 
+    remove_index "nodes", :name=> "nodes_uid_idx"
+    remove_index "nodes", :name=> "nodes_timestamp_idx"
     rename_table "nodes", "nodes_v5"
 
     create_table "nodes", myisam_table do |t|
@@ -74,7 +76,7 @@ class TileNodes < ActiveRecord::Migration
     add_index "nodes", ["timestamp"], :name => "nodes_timestamp_idx"
     add_index "nodes", ["tile"], :name => "nodes_tile_idx"
 
-    change_column "nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "nodes", "tile", :four_byte_unsigned
 
     upgrade_table "nodes_v5", "nodes", OldNode
 
@@ -85,7 +87,7 @@ class TileNodes < ActiveRecord::Migration
     rename_table "current_nodes", "current_nodes_v6"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :double,                                 :null => false
       t.column "longitude", :double,                                 :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -94,12 +96,9 @@ class TileNodes < ActiveRecord::Migration
       t.column "timestamp", :datetime,                               :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     downgrade_table "current_nodes_v6", "current_nodes"
 
     drop_table "current_nodes_v6"
index a30642e..c265fc3 100644 (file)
@@ -11,7 +11,7 @@ class AddRelations < ActiveRecord::Migration
       t.column "member_role", :string
     end
     # enums work like strings but are more eff