######################################################################## #### #### Title: shape_export.magik #### Author: Mark Cederholm #### Last Revised: 25-Mar-2004 #### #### Notes: #### #### Export Smallworld features to shapefiles in m/ft/cm. Requires #### shapefile class. #### #### Geometry type :text is exported as a point (text.coord_1) with #### three attributes representing string, orientation, and #### justification. #### #### If no output attributes are specified, a dummy ID field is added. #### #### Supports attributes for join fields joined to a single record. #### ######################################################################## #********************************************************************** # Exemplars for shape export utility #********************************************************************** remex(:shape_export_utility) $ def_slotted_exemplar(:shape_export_utility, { {:grs, _unset}, {:items, _unset}, {:geom_table, _unset}, {:table_list, _unset}, {:current_table, _unset}, {:geom_list, _unset}, {:current_geom, _unset}, {:attr_list, _unset}, {:current_attrs, _unset}, {:shp, _unset} }, :engine_model) $ # DESCRIPTION OF SLOTS: # # .grs stores the graphics_system passed into method new() # .items is a hash_table storing dialog items that need to be tracked # .geom_table is a hash_table storing supported geometry and the # shapefile equivalent [see method init() for more info] # .table_list stores the browsable ds_collections in the currently # selected partition that have supported geometry # .current_table stores the currently selected .table_list item # .geom_list stores the supported geometry fields for .current_table # .current_geom stores the currently selected .geom_list item # .attr_list stores the supported attribute fields for .current_table # each entry is in the form one of the following: # {a_field, _unset} indicates a supported local field # {field1, field2} indicates a join field mapping to an object # which has supported field2 # .current_attrs stores the currently selected .attr_list item(s) # .shp stores handle to the shapefile created (used for cleanup) #********************************************************************** # Method to launch shape export utility #********************************************************************** _method graphics_system.launch_shape_export_utility() menu_key << :shape_export_utility _if (menu << .sub_menus[menu_key]) _is _unset _then menu << .sub_menus[menu_key] << shape_export_utility.new(_self) _endif menu.activate() >> menu _endmethod #********************************************************************** # Public methods for shape export utility #********************************************************************** _method shape_export_utility.new(a_grs) >> _clone.init(a_grs) _endmethod _method shape_export_utility.activate_in(a_frame) ## set up dialog _self.title << "Export Shapefiles for GPS" p << panel.new(a_frame) # add items for directory and shapefile .items << hash_table.new() label_item.new(p,"Export features to shapefiles in UTM Zone 12 NAD83") p.start_row() i << .items[:path] << text_item.new(p,"Save Directory:") i.display_length << 40 i.value << system.getenv("temp") button_item.new(p,"Pick...",_self,:pick_dir|()|) # add dataset item p.start_row() (datasets, names) << _self.get_datasets() .items[:dataset] << choice_item.new(p,"Dataset",names,datasets, :model, _self, :display_all?, _false, :change_selector, :select_dataset|()|) # p.start_row() i << .items[:sfname] << text_item.new(p,"Shapefile name:") i.display_length << 20 .items[:overwrite] << toggle_item.new(p,"Confirm overwrite", :value, _true) # add table list p2 << panel.new(a_frame) label_item.new(p2,"Tables Available:") p2.start_row() .items[:tables] << list_view.new(_self, p2, :get_tables|()|, :select_table|()|, _unset, 15, 30, :one, _unset, _unset, _unset, _true) p2.start_row() strings << {"meters", "feet", "cm"} values << {:m, :ft, :cm} .items[:units] << choice_item.new(p2, _unset, strings, values, :model, _self, :display_all?, _true, :display_vertical?, _false) # add field lists p3 << panel.new(a_frame) p3.set_right_of(p2) p3.set_below(p) label_item.new(p3,"Geometry Fields:") p3.start_row() .items[:geoms] << list_view.new(_self, p3, :get_geoms|()|, :select_geom|()|, _unset, 3, 30, :single, _unset, _unset, _unset, _true) p3.start_row() label_item.new(p3,"Attribute Fields:") p3.start_row() .items[:attrs] << list_view.new(_self, p3, :get_attrs|()|, :select_attrs|()|, _unset, 13, 30, :many, _unset, _unset, _unset, _true) p << panel.new(a_frame) # add list field option item p.start_row() strings << {"List local attribute fields only", "Include attribute fields from joined objects"} values << {_false, _true} .items[:join] << choice_item.new(p,_unset,strings,values, :model, _self, :display_all?, _true, :change_selector, :select_join|()|) # add remaining items p.start_row() .items[:cross] << toggle_item.new(p,"Include features that cross trail", :value, _false) p.start_row() button_item.new(p,"Generate",_self,:do_it|()|) button_item.new(p,"Interrupt",_self,:interrupt|()|) button_item.new(p,"Refresh",_self,:refresh|()|) button_item.new(p,"Quit",_self,:quit|()|) .items[:status] << label_item.new(p,"Done.") _endmethod _method shape_export_utility.pick_dir() ## activate directory picker f << directory_selection.new(_self,"Select target directory", :pick_dir_ok|()|, _unset, :directory, .items[:path].value, :do_existence_check?, _true) f.activate() _endmethod _method shape_export_utility.pick_dir_ok(p) ## assign picker results to path item .items[:path].value << p _endmethod _method shape_export_utility.get_datasets() ## get list of datasets available d << .grs.spatial_object_controller.browsable_datasets n << d.map(_proc(dataset) >> dataset.external_name _endproc) datasets << rope.new_from(d) datasets.add_last(:clipboard) names << rope.new_from(n) names.add_last("Clipboard") >> (datasets, names) _endmethod _method shape_export_utility.get_tables() ## get list of exportable tables for current dataset # first, get list of accessible tables which can be browsed .current_table << _unset v << .items[:dataset].value tables_found << _unset _if v _is :clipboard _then cb << gis_program_manager.scrapbook().clipboard _if cb _is _unset _orif cb.n_objects = 0 _then .table_list << _unset _return {} _endif tables_found << cb.sources().as_simple_vector() _else ace_control << .grs.ace_control _for dsm, vhs_tree _over ace_control.rwo_hierarchy.fast_keys_and_elements() _loop _if dsm.name _isnt v.name _then _continue _endif _if ~ dsm.browsable? _then _leave _endif tables_found << vhs_tree.objects_from_datasets() _endloop _endif _if tables_found _is _unset _then .table_list << _unset _return {} _endif # filter out tables which lack supported geometry table_rope << rope.new() _for t _over tables_found.fast_elements() _loop has_geometry? << _false _for f _over t.visible_fields.fast_elements() _loop _if ~ f.is_geometry? _then _continue _endif _if .geom_table[f.geom_type] _isnt _unset _then has_geometry? << _true _leave _endif _endloop _if has_geometry? _then table_rope.add(t) _endif _endloop _if table_rope.size = 0 _then .table_list << _unset _return {} _endif # now create sorted lists of tables and external names string_rope << rope.new() _for t _over table_rope.fast_elements() _loop string_rope.add(t.external_name) _endloop .table_list << sorted_collection.new(table_rope.size, _proc(table1, table2) string1 << table1.external_name string2 << table2.external_name >> string1 _cf string2 _endproc) .table_list.add_all(table_rope) strings << sorted_collection.new(string_rope.size) strings.add_all(string_rope) _return strings _endmethod _method shape_export_utility.get_geoms() ## get list of supported geometry fields .geom_list << rope.new() .current_geom << _unset _if .current_table _is _unset _then _return {} _endif # build list of geometry fields [there will always be at least one # because of conditions matched in collection_names()] t << .current_table _for f _over t.visible_fields.fast_elements() _loop _if ~ f.is_geometry? _then _continue _endif _if .geom_table[f.geom_type] _isnt _unset _then .geom_list.add(f) _endif _endloop # build list of names strings << rope.new() _for f _over .geom_list.fast_elements() _loop strings.add(f.name.as_charvec()) _endloop _return strings _endmethod _method shape_export_utility.get_attrs() ## get list of supported attribute fields .attr_list << rope.new() .current_attrs << rope.new() _if .current_table _is _unset _then _return {} _endif # build list of visible physical, derived, or text join fields t << .current_table _self.get_supported_attrs(t,_unset) # list join field attributes _if .items[:join].value _then _for f _over t.visible_fields.fast_elements() _loop _if ~ f.is_join? _then _continue _endif _if f.join_type _is :text _then _continue _endif _if ~ f.simple_result? _then _continue _endif _self.get_supported_attrs(f.result_table,f) _endloop _endif # build list of names strings << rope.new() _for f _over .attr_list.fast_elements() _loop nm << f[1].name.as_charvec() _if f[2] _isnt _unset _then nm +<< "." + f[2].name.as_charvec() _endif strings.add(nm) _endloop _return strings _endmethod _method shape_export_utility.select_dataset() ## action when dataset is selected d << .items[:dataset].value _if d _is :clipboard _then .items[:cross].visibility << _false _else .items[:cross].visibility << _true _endif .items[:tables].update_list(_self.get_tables(),_unset) .items[:geoms].update_list(_self.get_geoms(),_unset) .items[:attrs].update_list(_self.get_attrs(),_unset) _endmethod _method shape_export_utility.select_table(an_index) ## action when table selection changes _if an_index _is _unset _then .current_table << _unset _else .current_table << .table_list[an_index] _endif # update shapefile name _if .current_table _is _unset _then .items[:sfname].value << _unset _else .items[:sfname].value << .current_table.name.as_charvec() _endif # update field lists .items[:geoms].update_list(_self.get_geoms(),_unset) .items[:attrs].update_list(_self.get_attrs(),_unset) _endmethod _method shape_export_utility.select_geom(an_index) ## action when geometry selection changes _if an_index _is _unset _then .current_geom << _unset _else .current_geom << .geom_list[an_index] _endif _endmethod _method shape_export_utility.select_attrs(an_index) ## action when attribute selection changes .current_attrs << rope.new() _for i _over an_index.fast_elements() _loop .current_attrs.add(.attr_list[i]) _endloop _endmethod _method shape_export_utility.select_join() ## action when attribute display option changes .items[:attrs].update_list(_self.get_attrs(),_unset) _endmethod _method shape_export_utility.do_it() ## action when Generate button is clicked # check for trail v << .items[:dataset].value _if v _isnt :clipboard _then g << .grs tr << g.gtrail _if (tr.empty?) _or (~ tr.closed?) _then _self.show_alert("No closed trail!") _return _endif _endif # check if path is set, exists, and is directory p << .items[:path].value _if p _is _unset _then _self.show_alert("Save directory not specified!") _return _endif _if ~ system.file_exists?(p) _then _self.show_alert("Directory doesn't exist!") _return _endif f << file_status.new(p) _if f.type ~= :directory _then _self.show_alert("Not a directory!") _return _endif # check for valid shapefile name s << .items[:sfname].value _if s _is _unset _or s.trim_spaces() = "" _then _self.show_alert("Shapefile name not specified!") _return _endif sfname << system.pathname_from_components(s,p) + ".shp" _if ~ system.file_creatable?(sfname) _then _self.show_alert("Invalid shapefile name!") _return _endif # confirm file overwrite confirm? << .items[:overwrite].value _if confirm? _and system.file_exists?(sfname) _then result << _self.show_question("Yes", "No", "Overwrite existing file?") _if ~ result _then _return _endif _endif # check for selections _if .current_table _is _unset _then _self.show_alert("No table specified!") _return _endif _if .current_geom _is _unset _then _self.show_alert("A geometry field must be selected.") _return _endif # do it! _global thread _self.run_engine(thread.interactive_priority,:do_export|()|) _endmethod _method shape_export_utility.do_export() ## method executed by run_engine ok? << _false _protect _self.busy? << _true _self.export_shapes() ok? << _true _protection _self.busy? << _false _if ~ ok? _then _self.cleanup() _endif _endprotect _endmethod _method shape_export_utility.interrupt() ## action when Generate button is clicked _return _self.interrupt_engine() _endmethod _method shape_export_utility.refresh() ## refresh dialog (e.g. when clipboard is updated) (datasets, names) << _self.get_datasets() .items[:dataset].set_strings_and_values(names, datasets) .items[:tables].update_list(_self.get_tables(),_unset) .items[:geoms].update_list(_self.get_geoms(),_unset) .items[:attrs].update_list(_self.get_attrs(),_unset) _endmethod #********************************************************************** # Private methods for shape export utility #********************************************************************** _private _method shape_export_utility.init(a_grs) ## initialize .grs and .geom_table slots .grs << a_grs .geom_table << hash_table.new() .geom_table[:simple_point] << :point .geom_table[:simple_chain] << :line .geom_table[:simple_area] << :poly .geom_table[:point] << :point .geom_table[:chain] << :line .geom_table[:area] << :poly .geom_table[:text] << :point >> _super.init() _endmethod _private _method shape_export_utility.export_shapes() ## perform shapefile export # get elements to export stat << .items[:status] stat.label << "Querying dataset..." g << .grs v << .items[:dataset].value t << .current_table gfield << .current_geom gtype << gfield.geom_type gname << gfield.name sftype << .geom_table[gfield.geom_type] # numrecs is required to initialize shapefile _if v _is :clipboard _then cb << gis_program_manager.scrapbook().clipboard _local an_e << t.an_element() test_proc << _proc(r) _import an_e >> r.is_class_of?(an_e) _endproc pred << predicate.using(test_proc) rset << cb.record_read_stream(pred) numrecs << rset.size _if (numrecs = 0) _then _self.show_alert("No records to export") stat.label << "Done." _return _endif _else tr << g.gtrail sr << tr.geom_for_pred() pred << predicate.within(gname, {sr}) _if .items[:cross].value _and sftype _isnt :point _then pred << pred _or predicate.overlaps(gname, {sr}) _endif rset << t.select(pred).read_stream() numrecs << rset.size _if (numrecs = 0) _then s << internal_text_output_stream.new() s.write("No ", t.name.as_charvec(), " ") s.write(gname.as_charvec(), " geometries within trail") _self.show_alert(s.string) stat.label << "Done." _return _endif _endif rset.reset() # BUG: this statement is required in 3.3 !!!! # build field mapping list stat.label << "Processing..." field_list << rope.new() dbfield_list << rope.new() fname_list << sorted_set.new() # if geometry is :text then add annotation attribute fields _if gtype _is :text _then fkey << {gname,_unset} field_list.add({fkey,"text"}) field_list.add({fkey,"angle"}) field_list.add({fkey,"just"}) dbfield_list.add(db_field.new("anno_text",:db_char,254,0)) dbfield_list.add(db_field.new("anno_angle",:db_decimal,13,6)) dbfield_list.add(db_field.new("anno_just",:db_decimal,2,0)) fname_list.add("anno_text") fname_list.add("anno_angle") fname_list.add("anno_just") _endif # add selected attribute(s) _for f_entry _over .current_attrs.fast_elements() _loop # check for join field _if f_entry[2] _isnt _unset _then fkey << {f_entry[1].name,f_entry[2].name} f << f_entry[2] _else fkey << {f_entry[1].name,_unset} f << f_entry[1] _endif # set up dbfield types ftype << _unset _if f.is_physical? _or f.is_derived? _then t << f.type _if t.enumerator _isnt _unset _then ftype << "enum" dbtype << :db_char dbwidth << f.print_width dbdec << _unset _elif t.is_string? _then ftype << "char" dbtype << :db_char dbwidth << t.phys_size dbdec << _unset _elif t.phys_type _is :ds_float _then ftype << "numb" dbtype << :db_decimal dbwidth << 13 dbdec << 6 _elif t.phys_type _is :ds_int _then # ftype << "numb" dbtype << :db_decimal dbwidth << 11 dbdec << 0 _elif t.phys_type _is :ds_date _then ftype << "date" dbtype << :db_date dbwidth << _unset dbdec << _unset _else _continue _endif _elif f.is_join? _then _if f.join_type _isnt :text _then _continue _endif ftype << "text" dbtype << :db_char dbwidth << 254 dbdec << _unset _else _continue _endif # find unique shapefile field name dbname << f.name.as_charvec().truncate(10) _if fname_list.includes?(dbname) _then i << 1 _loop p << i.write_string d << dbname.truncate(9 - p.size) + "_" + p _if ~ fname_list.includes?(d) _then dbname << d _leave _endif i +<< 1 _endloop _endif field_list.add({fkey,ftype}) dbfield_list.add(db_field.new(dbname,dbtype,dbwidth,dbdec)) fname_list.add(dbname) _endloop # if no attribute fields in list, add dummy ID _if dbfield_list.size = 0 _then field_list.add({_unset,"numb"}) dbfield_list.add(db_field.new("id",:db_decimal,11,0)) _endif num_fields << field_list.size # get bounds of record set _if v _is :clipboard _then stat.label << "Finding bounds of record set..." first? << _true _loop _if (e << rset.get()) _is _unset _then _leave _endif _if (g << e.perform(gname)) _is _unset _then _continue # this can happen when exporting from the clipboard _endif _if first? _then bounds << g.bounds first? << _false _else bounds << bounds.union(g.bounds) _endif _endloop rset.reset() _else # to save time, instead of collecting the bounds of selected # features we'll just use the trail to set the shapefile bounds bounds << tr.bounds _endif bounds << _self.transform(bounds) # if index files exist, delete them sfname << system.pathname_from_components(.items[:sfname].value,.items[:path].value) _for ext _over {".sbn", ".sbx", ".aih", ".ain"}.fast_elements() _loop fname << sfname + ext _if system.file_exists?(fname) _then system.do_command("erase " + fname) _endif _endloop # open shapefile for writing dbf << dbfield_list.as_simple_vector() s << .shp << shapefile.open_write(sfname,sftype,dbf,numrecs,bounds) _if s _is _unset _then _self.show_alert("Error opening shapefile for writing") stat.label << "Done." _return _endif # write records recnum << 0 _loop _if (e << rset.get()) _is _unset _then _leave _endif recnum +<< 1 _if recnum _mod 100 = 0 _then msg << "Processing..." + recnum.write_string + " of " + numrecs.write_string stat.label << msg _endif # get geometry _if (g << e.perform(gname)) _is _unset _then # this can happen when exporting from the clipboard r1 << _unset _else _if gtype _is :point _or gtype _is :simple_point _then r1 << _self.transform(g.coordinate) _elif gtype _is :text _then r1 << _self.transform(g.coord_1) _elif gtype _is :area _then r1 << rope.new() a << e.extent _for ap _over a.polygons.fast_elements() _loop r << _self.transform(ap.sectors) r1.add(r) _for hp _over ap.holes.fast_elements() _loop r << _self.transform(hp.sectors) r1.add(r.reversed()) _endloop _endloop _else r << _self.transform(g.sectors) r1 << rope.new() r1.add(r) _endif _endif # assemble and format dbfile record value_list << simple_vector.new(num_fields) _for fi _over range(1,num_fields) _loop f << field_list[fi] fkey << f[1] ftype << f[2] _if fkey _isnt _unset _then the_value << e.perform(fkey[1]) _if fkey[1] _is gname _then # anno attribute values _if ftype = "text" _then the_value << the_value.string _elif ftype = "angle" _then the_value << the_value.orientation _else the_value << the_value.justification _endif _elif fkey[2] _isnt _unset _and the_value _isnt _unset _then the_value << the_value.perform(fkey[2]) _endif _else the_value << _unset _endif _if the_value _isnt _unset _then _if ftype = "text" _then the_value << the_value.truncate(254) _elif ftype = "enum" _then the_value << the_value.write_string _endif _endif value_list[fi] << the_value _endloop r2 << s.format_dbf_record(value_list) s.write_record({r1,r2}) _endloop s.close() .shp << _unset stat.label << "Done." _endmethod _private _method shape_export_utility.cleanup() ## perform cleanup in case of crash or interruption # close shapefile _if .shp _isnt _unset _then .shp.close() .shp << _unset _endif # delete shapefile files path << .items[:path].value sfname << system.pathname_from_components(.items[:sfname].value,path) shpname << sfname + ".shp" _if system.file_exists?(shpname) _then system.do_command("erase " + shpname) _endif shxname << sfname + ".shx" _if system.file_exists?(shxname) _then system.do_command("erase " + shxname) _endif dbfname << sfname + ".dbf" _if system.file_exists?(dbfname) _then system.do_command("erase " + dbfname) _endif # reset status label .items[:status].label << "Done." _endmethod _private _method shape_export_utility.get_supported_attrs(t,j) ## add supported attribute field entries to .attr_list ## ## T is a ds_collection ## J is a parent join field or _unset _for f _over t.visible_fields.fast_elements() _loop add? << _false _if f.is_physical? _or f.is_derived? _then add? << _true _elif f.is_join? _then _if f.join_type _is :text _then add? << _true _endif _endif _if add? _then _if j _is _unset _then .attr_list.add({f,_unset}) _else .attr_list.add({j,f}) _endif _endif _endloop _endmethod _private _method shape_export_utility.transform(a_geom) ## convert coordinates from cm to desired units units << .items[:units].value _if units _is :cm _then _return a_geom _elif units _is :m _then sf << 0.01 _elif units _is :ft _then sf << 39.37 / 1200 _endif the_transform << transform.scale(sf, sf) cname << a_geom.class_name _if cname _is :ds_coord _then new_geom << a_geom.scalar_multiply(sf) _elif cname _is :coordinate _then new_geom << a_geom.transformed(the_transform) _elif cname _is :sector_rope _then a_geom.transform_in_situ(the_transform) new_geom << a_geom _else new_geom << a_geom.transform(the_transform) _endif _return new_geom _endmethod