2This script relocates and optionally rotates an OpenStreetMap (OSM) file that uses a custom geoReference. It's useful when you want to:
3- Move a local map to a new geographic location.
4- Preserve geometry, scale, and layout.
5- Rotate the map around its origin (for alignment or testing).
101. Read the input OSM XML file.
112. Extract the original geoReference (a Transverse Mercator projection centered at some latitude/longitude).
123. Convert each nodes lat/lon to projected (X, Y) coordinates using the original projection.
134. Apply a 2D rotation (optional) around the local origin (0, 0).
145. Transform the rotated (X, Y) into new lat/lon coordinates using a new projection centered at a new location.
156. Update the <geoReference> tag to reflect the new center.
167. Save the updated OSM XML file with transformed coordinates.
221- input_file.osm: An OSM XML file that:
23- Contains a <geoReference> string using +proj=tmerc
24- Has <node> elements with lat and lon attributes
262- output_file.osm: The transformed OSM map name that:
27- All node positions have been relocated and optionally rotated
28- The <geoReference> tag is updated to match the new map center
29- The map geometry is preserved in relative terms but relocated globally
33Parameters to adjust to transform:
35- rotation_deg: Rotation angle in degrees (positive = counter-clockwise)
36- new_lat_0, new_lon_0: New center location in geographic coordinates (latitude, longitude)
41pip install lxml pyproj
46python osm_transform.py suntrax.osm suntrax_transformed.osm
51from pyproj
import CRS, Transformer
67theta_rad = math.radians(rotation_deg)
70parser = argparse.ArgumentParser(description=
"Relocate and rotate OSM map geometry around a new reference point.")
71parser.add_argument(
"input_file", help=
"Path to the input .osm file")
72parser.add_argument(
"output_file", help=
"Path to the output .osm file")
73args = parser.parse_args()
76tree = etree.parse(args.input_file)
80geo_ref_elem = root.find(
"geoReference")
81if geo_ref_elem
is None or (
not geo_ref_elem.text
and not geo_ref_elem.attrib.get(
"v")):
82 raise ValueError(
"ā geoReference tag not found or is empty in the OSM file.")
85 old_proj_str = geo_ref_elem.text.strip()
87 old_proj_str = geo_ref_elem.attrib.get(
"v").strip()
88print(f
"š Extracted old geoReference:\n{old_proj_str}\n")
90new_proj_str = f
"+proj=tmerc +lat_0={new_lat_0} +lon_0={new_lon_0} +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +geoidgrids=egm96_15.gtx +vunits=m +no_defs"
91print(f
"š Updated new geoReference:\n{new_proj_str}\n")
94crs_wgs84 = CRS.from_epsg(4326)
95crs_old = CRS.from_proj4(old_proj_str)
96crs_new = CRS.from_proj4(new_proj_str)
99to_old_xy = Transformer.from_crs(crs_wgs84, crs_old, always_xy=
True)
100to_new_latlon = Transformer.from_crs(crs_new, crs_wgs84, always_xy=
True)
104 geo_ref_elem.text = new_proj_str
106 geo_ref_elem.set(
"v", new_proj_str)
109xs_old, ys_old = [], []
110for node
in root.findall(
"node"):
113 x_old, y_old = to_old_xy.transform(lon, lat)
117cx_old = np.mean(xs_old)
118cy_old = np.mean(ys_old)
121to_new_xy = Transformer.from_crs(crs_wgs84, crs_new, always_xy=
True)
122rotate_x, rotate_y = to_new_xy.transform(rotate_lon, rotate_lat)
125offset_x = rotate_x - cx_old
126offset_y = rotate_y - cy_old
129for i, node
in enumerate(root.findall(
"node")):
130 x = xs_old[i] + offset_x
131 y = ys_old[i] + offset_y
134 x_rel, y_rel = x - rotate_x, y - rotate_y
135 x_rot = x_rel * math.cos(theta_rad) - y_rel * math.sin(theta_rad)
136 y_rot = x_rel * math.sin(theta_rad) + y_rel * math.cos(theta_rad)
141 new_lon, new_lat = to_new_latlon.transform(x_rot, y_rot)
142 node.set(
"lat", f
"{new_lat:.10f}")
143 node.set(
"lon", f
"{new_lon:.10f}")
145 for tag
in node.findall(
'tag'):
146 if tag.get(
'k') ==
'lat':
147 tag.set(
'v', f
"{new_lat:.10f}")
148 elif tag.get(
'k') ==
'lon':
149 tag.set(
'v', f
"{new_lon:.10f}")
152tree.write(args.output_file, pretty_print=
True, xml_declaration=
True, encoding=
"UTF-8")
153print(f
"\nā
Map shifted and rotated. Output saved to: {args.output_file}")