/*************************************************************************** * Project: osmdata * File: osmdata-sf.cpp * Language: C++ * * osmdata is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * osmdata is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * osm-router. If not, see . * * Author: Mark Padgham * E-Mail: mark.padgham@email.com * * Description: Extract OSM data from an object of class XmlData and return * it in Rcpp::List format. * * Limitations: * * Dependencies: none (rapidXML header included in osmdata) * * Compiler Options: -std=c++11 ***************************************************************************/ #include "osmdata.h" #include // Note: This code uses explicit index counters within most loops which use Rcpp // objects, because these otherwise require a // static_cast (std::distance (...)). This operation copies each // instance and can slow the loops down by several orders of magnitude! /************************************************************************ ************************************************************************ ** ** ** 1. PRIMARY FUNCTIONS TO TRACE WAYS AND RELATIONS ** ** ** ************************************************************************ ************************************************************************/ //' get_osm_relations //' //' Return a dual Rcpp::List containing all OSM relations, the first element of //' which holds `multipolygon` relations, while the second holds all others, //' which are stored as `multilinestring` objects. //' //' @param rels Pointer to the vector of Relation objects //' @param nodes Pointer to the vector of node objects //' @param ways Pointer to the vector of way objects //' @param unique_vals Pointer to a UniqueVals object containing std::sets of //' all unique IDs and keys for each kind of OSM object (nodes, ways, rels). //' //' @return A Rcpp::List which contains the geometry, tags and metadata of the //' multipolygon and multilinestring relations. //' //' @noRd Rcpp::List osm_sf::get_osm_relations (const Relations &rels, const std::map &nodes, const std::map &ways, const UniqueVals &unique_vals, const Rcpp::NumericVector &bbox, const Rcpp::List &crs) { /* Trace all multipolygon relations. These are the only OSM types where * sizes are not known before, so lat-lons and node names are stored in * dynamic vectors. These are 3D monsters: #1 for relation, #2 for polygon * in relation, and #3 for data. There are also associated 2D vector * objects for IDs and multilinestring roles. */ std::set keyset; // must be ordered! std::vector colnames = {"lat", "lon"}, rownames; Rcpp::List dimnames (0); Rcpp::NumericMatrix nmat (Rcpp::Dimension (0, 0)); double_arr2 lat_vec, lon_vec; double_arr3 lat_arr_mp, lon_arr_mp, lon_arr_ls, lat_arr_ls; string_arr2 rowname_vec, id_vec_mp, roles_ls; string_arr3 rowname_arr_mp, rowname_arr_ls; std::vector ids_ls; std::vector ids_mp, rel_id_mp, rel_id_ls; osmt_arr2 id_vec_ls; std::vector roles; unsigned int nmp = 0, nls = 0; // number of multipolygon and multilinestringrelations for (auto itr = rels.begin (); itr != rels.end (); ++itr) { if (itr->ispoly) nmp++; else { // TODO: Store these as std::vector > to avoid // repetition below std::set roles_set; for (auto itw = itr->ways.begin (); itw != itr->ways.end (); ++itw) roles_set.insert (itw->second); nls += roles_set.size (); } } std::vector mp_okay (nmp); std::fill (mp_okay.begin (), mp_okay.end (), true); size_t ncol = unique_vals.k_rel.size (); rel_id_mp.reserve (nmp); rel_id_ls.reserve (nls); Rcpp::CharacterMatrix kv_mat_mp (Rcpp::Dimension (nmp, ncol)), kv_mat_ls (Rcpp::Dimension (nls, ncol)); std::fill (kv_mat_mp.begin (), kv_mat_mp.end (), NA_STRING); std::fill (kv_mat_ls.begin (), kv_mat_ls.end (), NA_STRING); Rcpp::CharacterMatrix meta_mat_mp (Rcpp::Dimension (nmp, 5L)), meta_mat_ls (Rcpp::Dimension (nls, 5L)); std::fill (meta_mat_mp.begin (), meta_mat_mp.end (), NA_STRING); std::fill (meta_mat_ls.begin (), meta_mat_ls.end (), NA_STRING); unsigned int count_mp = 0, count_ls = 0; for (auto itr = rels.begin (); itr != rels.end (); ++itr) { Rcpp::checkUserInterrupt (); if (itr->ispoly) // itr->second can only be "outer" or "inner" { trace_multipolygon (itr, ways, nodes, lon_vec, lat_vec, rowname_vec, ids_mp); rel_id_mp.push_back (std::to_string (itr->id)); lon_arr_mp.push_back (lon_vec); lat_arr_mp.push_back (lat_vec); rowname_arr_mp.push_back (rowname_vec); id_vec_mp.push_back (ids_mp); if (rowname_vec.size () == 0) mp_okay [count_mp] = false; lon_vec.clear (); lon_vec.shrink_to_fit (); lat_vec.clear (); lat_vec.shrink_to_fit (); rowname_vec.clear (); rowname_vec.shrink_to_fit (); ids_mp.clear (); ids_mp.shrink_to_fit (); meta_mat_mp (count_mp, 0L) = itr->_version; meta_mat_mp (count_mp, 1L) = itr->_timestamp; meta_mat_mp (count_mp, 2L) = itr->_changeset; meta_mat_mp (count_mp, 3L) = itr->_uid; meta_mat_mp (count_mp, 4L) = itr->_user; osm_convert::get_value_mat_rel (itr, unique_vals, kv_mat_mp, count_mp++); } else // store as multilinestring { // multistrings are grouped here by roles, unlike GDAL which just // dumps all of them. std::set roles_set; for (auto itw = itr->ways.begin (); itw != itr->ways.end (); ++itw) roles_set.insert (itw->second); roles.reserve (roles_set.size ()); for (auto it = roles_set.begin (); it != roles_set.end (); ++it) roles.push_back (*it); roles_set.clear (); for (std::string role: roles) { trace_multilinestring (itr, role, ways, nodes, lon_vec, lat_vec, rowname_vec, ids_ls); std::stringstream ss; ss.str (""); if (role == "") ss << std::to_string (itr->id) << "-(no role)"; else ss << std::to_string (itr->id) << "-" << role; rel_id_ls.push_back (ss.str ()); lon_arr_ls.push_back (lon_vec); lat_arr_ls.push_back (lat_vec); rowname_arr_ls.push_back (rowname_vec); id_vec_ls.push_back (ids_ls); lon_vec.clear (); lon_vec.shrink_to_fit (); lat_vec.clear (); lat_vec.shrink_to_fit (); rowname_vec.clear (); rowname_vec.shrink_to_fit (); ids_ls.clear (); ids_ls.shrink_to_fit (); meta_mat_ls (count_ls, 0L) = itr->_version; meta_mat_ls (count_ls, 1L) = itr->_timestamp; meta_mat_ls (count_ls, 2L) = itr->_changeset; meta_mat_ls (count_ls, 3L) = itr->_uid; meta_mat_ls (count_ls, 4L) = itr->_user; osm_convert::get_value_mat_rel (itr, unique_vals, kv_mat_ls, count_ls++); } roles_ls.push_back (roles); roles.clear (); } } // Erase any multipolygon ways that are not okay. An example of these is // opq("salzburg") |> add_osm_feature (key = "highway"), for which // $osm_multipolygons [[42]] with way#4108738 is not okay. std::vector not_okay_id; for (size_t i = 0; i < mp_okay.size (); i++) if (!mp_okay [i]) not_okay_id.push_back (rel_id_mp [i]); for (std::string i: not_okay_id) { std::vector ::iterator it = std::find (rel_id_mp.begin (), rel_id_mp.end (), i); //size_t j = static_cast (std::distance (rel_id_mp.begin (), it)); long int j = std::distance (rel_id_mp.begin (), it); lon_arr_mp.erase (lon_arr_mp.begin () + j); lat_arr_mp.erase (lat_arr_mp.begin () + j); rowname_arr_mp.erase (rowname_arr_mp.begin () + j); id_vec_mp.erase (id_vec_mp.begin () + j); rel_id_mp.erase (rel_id_mp.begin () + j); // Retain static_cast here because there will generally be very few // instances of this loop size_t st_nrow = static_cast (kv_mat_mp.nrow ()); Rcpp::CharacterMatrix kv_mat_mp2 (Rcpp::Dimension (st_nrow - 1, ncol)); Rcpp::CharacterMatrix meta_mp2 (Rcpp::Dimension (st_nrow - 1, 5L)); // k is int for type-compatible Rcpp indexing for (int k = 0; k < kv_mat_mp.nrow (); k++) { if (k < j) { kv_mat_mp2 (k, Rcpp::_) = kv_mat_mp (k, Rcpp::_); meta_mp2 (k, Rcpp::_) = meta_mat_mp (k, Rcpp::_); } else if (k > j) { kv_mat_mp2 (k - 1, Rcpp::_) = kv_mat_mp (k, Rcpp::_); meta_mp2 (k - 1, Rcpp::_) = meta_mat_mp (k, Rcpp::_); } } kv_mat_mp = kv_mat_mp2; meta_mat_mp = meta_mp2; } Rcpp::List polygonList = osm_convert::convert_poly_linestring_to_sf (lon_arr_mp, lat_arr_mp, rowname_arr_mp, id_vec_mp, rel_id_mp, "MULTIPOLYGON"); polygonList.attr ("n_empty") = 0; polygonList.attr ("class") = Rcpp::CharacterVector::create ("sfc_MULTIPOLYGON", "sfc"); polygonList.attr ("precision") = 0.0; polygonList.attr ("bbox") = bbox; polygonList.attr ("crs") = crs; Rcpp::List linestringList = osm_convert::convert_poly_linestring_to_sf (lon_arr_ls, lat_arr_ls, rowname_arr_ls, id_vec_ls, rel_id_ls, "MULTILINESTRING"); // TODO: linenames just as in ways? // linestringList.attr ("names") = ? linestringList.attr ("n_empty") = 0; linestringList.attr ("class") = Rcpp::CharacterVector::create ("sfc_MULTILINESTRING", "sfc"); linestringList.attr ("precision") = 0.0; linestringList.attr ("bbox") = bbox; linestringList.attr ("crs") = crs; Rcpp::DataFrame kv_df_ls; Rcpp::DataFrame meta_df_ls; if (rel_id_ls.size () > 0) // only if there are linestrings { kv_mat_ls.attr ("dimnames") = Rcpp::List::create (rel_id_ls, unique_vals.k_rel); kv_df_ls = osm_convert::restructure_kv_mat (kv_mat_ls, true); meta_mat_ls.attr ("dimnames") = Rcpp::List::create (rel_id_ls, metanames); meta_df_ls = meta_mat_ls; } else { kv_df_ls = R_NilValue; meta_df_ls = R_NilValue; } Rcpp::DataFrame kv_df_mp; Rcpp::DataFrame meta_df_mp; if (rel_id_mp.size () > 0) { kv_mat_mp.attr ("dimnames") = Rcpp::List::create (rel_id_mp, unique_vals.k_rel); kv_df_mp = osm_convert::restructure_kv_mat (kv_mat_mp, false); meta_mat_mp.attr ("dimnames") = Rcpp::List::create (rel_id_mp, metanames); meta_df_mp = meta_mat_mp; } else { kv_df_mp = R_NilValue; meta_df_mp = R_NilValue; } // ****** clean up ***** lon_arr_mp.clear (); lon_arr_mp.shrink_to_fit (); lon_arr_ls.clear (); lon_arr_ls.shrink_to_fit (); lat_arr_mp.clear (); lat_arr_mp.shrink_to_fit (); lat_arr_ls.clear (); lat_arr_ls.shrink_to_fit (); rowname_arr_mp.clear (); rowname_arr_mp.shrink_to_fit (); rowname_arr_ls.clear (); rowname_arr_ls.shrink_to_fit (); rel_id_mp.clear (); rel_id_mp.shrink_to_fit (); rel_id_ls.clear (); rel_id_ls.shrink_to_fit (); roles_ls.clear (); roles_ls.shrink_to_fit (); Rcpp::List ret (6); ret [0] = polygonList; ret [1] = kv_df_mp; ret [2] = meta_df_mp; ret [3] = linestringList; ret [4] = kv_df_ls; ret [5] = meta_df_ls; return ret; } //' get_osm_ways //' //' Store OSM ways as `sf::LINESTRING` or `sf::POLYGON` objects. //' //' @param wayList Pointer to Rcpp::List to hold the resultant geometries //' @param kv_df Pointer to Rcpp::DataFrame to hold key-value pairs //' @param way_ids Vector of IDs of ways to trace //' @param ways Pointer to all ways in data set //' @param nodes Pointer to all nodes in data set //' @param unique_vals pointer to all unique values (OSM IDs and keys) in data set //' @param geom_type Character string specifying "POLYGON" or "LINESTRING" //' @param bbox Pointer to the bbox needed for `sf` construction //' @param crs Pointer to the crs needed for `sf` construction //' //' @noRd void osm_sf::get_osm_ways (Rcpp::List &wayList, Rcpp::DataFrame &kv_df, Rcpp::DataFrame &meta_df, const std::set &way_ids, const Ways &ways, const Nodes &nodes, const UniqueVals &unique_vals, const std::string &geom_type, const Rcpp::NumericVector &bbox, const Rcpp::List &crs) { if (!(geom_type == "POLYGON" || geom_type == "LINESTRING")) throw std::runtime_error ("geom_type must be POLYGON or LINESTRING"); // NOTE that Rcpp `.size()` returns a **signed** int if (static_cast (wayList.size ()) != way_ids.size ()) throw std::runtime_error ("ways and IDs must have same lengths"); size_t nrow = way_ids.size (), ncol = unique_vals.k_way.size (); std::vector waynames; waynames.reserve (way_ids.size ()); Rcpp::CharacterMatrix kv_mat (Rcpp::Dimension (nrow, ncol)); std::fill (kv_mat.begin (), kv_mat.end (), NA_STRING); Rcpp::CharacterMatrix meta (Rcpp::Dimension (nrow, 5L)); std::fill (meta.begin (), meta.end (), NA_STRING); unsigned int count = 0; for (auto wi = way_ids.begin (); wi != way_ids.end (); ++wi) { //unsigned int count = static_cast ( // std::distance (way_ids.begin (), wi)); Rcpp::checkUserInterrupt (); waynames.push_back (std::to_string (*wi)); Rcpp::NumericMatrix nmat; osm_convert::trace_way_nmat (ways, nodes, (*wi), nmat); if (geom_type == "LINESTRING") { nmat.attr ("class") = Rcpp::CharacterVector::create ("XY", geom_type, "sfg"); wayList [count] = nmat; } else // polygons are lists { Rcpp::List polyList_temp = Rcpp::List (1); polyList_temp (0) = nmat; polyList_temp.attr ("class") = Rcpp::CharacterVector::create ("XY", geom_type, "sfg"); wayList [count] = polyList_temp; } auto wj = ways.find (*wi); osm_convert::get_value_mat_way (wj, unique_vals, kv_mat, count); meta (count, 0L) = wj->second._version; meta (count, 1L) = wj->second._timestamp; meta (count, 2L) = wj->second._changeset; meta (count, 3L) = wj->second._uid; meta (count, 4L) = wj->second._user; count++; } wayList.attr ("names") = waynames; wayList.attr ("n_empty") = 0; std::stringstream ss; ss.str (""); ss << "sfc_" << geom_type; std::string sfc_type = ss.str (); wayList.attr ("class") = Rcpp::CharacterVector::create (sfc_type, "sfc"); wayList.attr ("precision") = 0.0; wayList.attr ("bbox") = bbox; wayList.attr ("crs") = crs; kv_df = R_NilValue; if (way_ids.size () > 0) { kv_mat.attr ("dimnames") = Rcpp::List::create (waynames, unique_vals.k_way); if (kv_mat.nrow () > 0 && kv_mat.ncol () > 0) kv_df = osm_convert::restructure_kv_mat (kv_mat, false); meta.attr ("dimnames") = Rcpp::List::create (waynames, metanames); meta_df = meta; } } //' get_osm_nodes //' //' Store OSM nodes as `sf::POINT` objects //' //' @param ptxy Pointer to Rcpp::List to hold the resultant geometries //' @param kv_df Pointer to Rcpp::DataFrame to hold key-value pairs //' @param nodes Pointer to all nodes in data set //' @param unique_vals pointer to all unique values (OSM IDs and keys) in data set //' @param bbox Pointer to the bbox needed for `sf` construction //' @param crs Pointer to the crs needed for `sf` construction //' //' @noRd void osm_sf::get_osm_nodes (Rcpp::List &ptList, Rcpp::DataFrame &kv_df, Rcpp::DataFrame &meta_df, const Nodes &nodes, const UniqueVals &unique_vals, const Rcpp::NumericVector &bbox, const Rcpp::List &crs) { size_t nrow = nodes.size (), ncol = unique_vals.k_point.size (); if (static_cast (ptList.size ()) != nrow) throw std::runtime_error ("points must have same size as nodes"); Rcpp::CharacterMatrix kv_mat (Rcpp::Dimension (nrow, ncol)); std::fill (kv_mat.begin (), kv_mat.end (), NA_STRING); Rcpp::CharacterMatrix meta (Rcpp::Dimension (nrow, 5L)); std::fill (meta.begin (), meta.end (), NA_STRING); std::vector ptnames; ptnames.reserve (nodes.size ()); unsigned int count = 0; for (auto ni = nodes.begin (); ni != nodes.end (); ++ni) { // std::distance requires a static_cast which copies each instance and // slows this down by lots of orders of magnitude //unsigned int count = static_cast ( // std::distance (nodes.begin (), ni)); if (count % 1000 == 0) Rcpp::checkUserInterrupt (); // These are pointers and so need to be explicitly recreated each time, // otherwise they all just point to the initial value. Rcpp::NumericVector ptxy = Rcpp::NumericVector::create (NA_REAL, NA_REAL); ptxy.attr ("class") = Rcpp::CharacterVector::create ("XY", "POINT", "sfg"); ptxy (0) = ni->second.lon; ptxy (1) = ni->second.lat; ptList (count) = ptxy; ptnames.push_back (std::to_string (ni->first)); meta (count, 0L) = ni->second._version; meta (count, 1L) = ni->second._timestamp; meta (count, 2L) = ni->second._changeset; meta (count, 3L) = ni->second._uid; meta (count, 4L) = ni->second._user; for (auto kv_iter = ni->second.key_val.begin (); kv_iter != ni->second.key_val.end (); ++kv_iter) { const std::string &key = kv_iter->first; unsigned int ndi = unique_vals.k_point_index.at (key); kv_mat (count, ndi) = kv_iter->second; } count++; } if (unique_vals.k_point.size () > 0) { kv_mat.attr ("dimnames") = Rcpp::List::create (ptnames, unique_vals.k_point); kv_df = osm_convert::restructure_kv_mat (kv_mat, false); meta.attr ("dimnames") = Rcpp::List::create (ptnames, metanames); meta_df = meta; } else kv_df = R_NilValue; ptList.attr ("names") = ptnames; ptnames.clear (); ptList.attr ("n_empty") = 0; ptList.attr ("class") = Rcpp::CharacterVector::create ("sfc_POINT", "sfc"); ptList.attr ("precision") = 0.0; ptList.attr ("bbox") = bbox; ptList.attr ("crs") = crs; } /************************************************************************ ************************************************************************ ** ** ** THE FINAL RCPP FUNCTION CALLED BY osmdata_sf ** ** ** ************************************************************************ ************************************************************************/ //' rcpp_osmdata_sf //' //' Return OSM data in Simple Features format //' //' @param st Text contents of an overpass API query //' @return Rcpp::List objects of OSM data //' //' @noRd // [[Rcpp::export]] Rcpp::List rcpp_osmdata_sf (const std::string& st) { #ifdef DUMP_INPUT { std::ofstream dump ("./osmdata-sf.xml"); if (dump.is_open()) { dump.write (st.c_str(), st.size()); } } #endif XmlData xml (st); const std::map & nodes = xml.nodes (); const std::map & ways = xml.ways (); const std::vector & rels = xml.relations (); const UniqueVals& unique_vals = xml.unique_vals (); std::vector lons, lats; std::set keyset; // must be ordered! Rcpp::List dimnames (0); Rcpp::NumericMatrix nmat (Rcpp::Dimension (0, 0)); /* -------------------------------------------------------------- * 1. Set up bbox and crs * --------------------------------------------------------------*/ std::vector colnames, rownames; colnames.push_back ("lon"); colnames.push_back ("lat"); Rcpp::NumericVector bbox = rcpp_get_bbox_sf (xml.x_min (), xml.y_min (), xml.x_max (), xml.y_max ()); Rcpp::List crs = Rcpp::List::create (NA_STRING, Rcpp::CharacterVector::create (NA_STRING)); crs (0) = "EPSG:4326"; crs (1) = wkt; //Rcpp::List crs = Rcpp::List::create ((int) 4326, p4s); crs.attr ("names") = Rcpp::CharacterVector::create ("input", "wkt"); crs.attr ("class") = "crs"; /* -------------------------------------------------------------- * 2. Extract OSM Relations * --------------------------------------------------------------*/ Rcpp::List tempList = osm_sf::get_osm_relations (rels, nodes, ways, unique_vals, bbox, crs); Rcpp::List multipolygons = tempList [0]; // the followin line errors because of ambiguous conversion //Rcpp::DataFrame kv_df_mp = tempList [1]; Rcpp::List kv_df_mp = tempList [1]; kv_df_mp.attr ("class") = "data.frame"; Rcpp::List meta_df_mp = tempList [2]; meta_df_mp.attr ("class") = "data.frame"; Rcpp::List multilinestrings = tempList [3]; Rcpp::List kv_df_ls = tempList [4]; kv_df_ls.attr ("class") = "data.frame"; Rcpp::List meta_df_ls = tempList [5]; meta_df_ls.attr ("class") = "data.frame"; /* -------------------------------------------------------------- * 3. Extract OSM ways * --------------------------------------------------------------*/ // first divide into polygonal and non-polygonal std::set poly_ways, non_poly_ways; for (auto itw = ways.begin (); itw != ways.end (); ++itw) { if ((*itw).second.nodes.front () == (*itw).second.nodes.back ()) { if (poly_ways.find ((*itw).first) == poly_ways.end ()) poly_ways.insert ((*itw).first); } else if (non_poly_ways.find ((*itw).first) == non_poly_ways.end ()) non_poly_ways.insert ((*itw).first); } Rcpp::List polyList (poly_ways.size ()); Rcpp::DataFrame kv_df_polys; Rcpp::DataFrame meta_df_polys; osm_sf::get_osm_ways (polyList, kv_df_polys, meta_df_polys, poly_ways, ways, nodes, unique_vals, "POLYGON", bbox, crs); Rcpp::List lineList (non_poly_ways.size ()); Rcpp::DataFrame kv_df_lines; Rcpp::DataFrame meta_df_lines; osm_sf::get_osm_ways (lineList, kv_df_lines, meta_df_lines, non_poly_ways, ways, nodes, unique_vals, "LINESTRING", bbox, crs); /* -------------------------------------------------------------- * 3. Extract OSM nodes * --------------------------------------------------------------*/ Rcpp::List pointList (nodes.size ()); // NOTE: kv_df_points is actually an Rcpp::CharacterMatrix, and the // following line *should* construct the wrapped data.frame version with // strings not factors, yet this does not work. //Rcpp::DataFrame kv_df_points = Rcpp::DataFrame::create (Rcpp::_["stringsAsFactors"] = false); Rcpp::DataFrame kv_df_points; Rcpp::DataFrame meta_df_points; osm_sf::get_osm_nodes (pointList, kv_df_points, meta_df_points, nodes, unique_vals, bbox, crs); /* -------------------------------------------------------------- * 5. Collate all data * --------------------------------------------------------------*/ Rcpp::List ret (16); ret [0] = bbox; ret [1] = pointList; ret [2] = kv_df_points; ret [3] = meta_df_points; ret [4] = lineList; ret [5] = kv_df_lines; ret [6] = meta_df_lines; ret [7] = polyList; ret [8] = kv_df_polys; ret [9] = meta_df_polys; ret [10] = multipolygons; ret [11] = kv_df_mp; ret [12] = meta_df_mp; ret [13] = multilinestrings; ret [14] = kv_df_ls; ret [15] = meta_df_ls; std::vector retnames {"bbox", "points", "points_kv", "points_meta", "lines", "lines_kv", "lines_meta", "polygons", "polygons_kv", "polygons_meta", "multipolygons", "multipolygons_kv", "multipolygons_meta", "multilines", "multilines_kv", "multilines_meta"}; ret.attr ("names") = retnames; return ret; }