Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active December 15, 2022 09:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amkisko/2f1cc0a363ccb53fa7a3045575035772 to your computer and use it in GitHub Desktop.
Save amkisko/2f1cc0a363ccb53fa7a3045575035772 to your computer and use it in GitHub Desktop.
leaflet osm address search and reverse coordinates lookup with stimulus for Rails
<div data-controller="address-form">
<%= simple_form_for(address, url: address_path(address)) do |f| %>
<%= f.text_field :address, "data-address-form-target" => "address" %>
<%= f.hidden_field :address_lon, "data-address-form-target" => "addressLon" %>
<%= f.hidden_field :address_lat, "data-address-form-target" => "addressLat" %>
<div data-address-form-target="addressMapContainer" style="width: 500px; height: 500px; margin-left: -3.6rem;"></div>
<%= f.button :submit %>
<% end %>
</div>
import { Controller } from "@hotwired/stimulus";
import {
Address,
LocationEditor,
} from "../../../javascript/lib/location_editor";
export default class extends Controller {
static targets = [
"addressLon",
"addressLat",
"address",
"addressMapContainer",
];
declare readonly addressLonTarget: HTMLInputElement;
declare readonly addressLatTarget: HTMLInputElement;
declare readonly addressTarget: HTMLInputElement;
declare readonly addressMapContainerTarget: HTMLDivElement;
locationEditor: LocationEditor | undefined;
connect() {
this.locationEditor = new LocationEditor(
this.addressLonTarget,
this.addressLatTarget,
this.addressMapContainerTarget,
(address) => this.setAddress(address)
);
this.locationEditor.getUserPosition();
}
setAddress(address: Address | undefined) {
if (address) {
this.addressTarget.value = this.locationEditor.formatAddress(address);
} else {
this.addressTarget.value = "";
}
}
}
import "leaflet/dist/leaflet.css";
import "leaflet-control-geocoder/dist/Control.Geocoder.css";
import * as L from "leaflet";
import * as LG from "leaflet-control-geocoder";
import { MarkGeocodeEvent } from "leaflet-control-geocoder/dist/control";
export type Address = LG.geocoders.NominatimAddress;
export type SetAddressFunction = (address: Address | undefined) => void;
export class LocationEditor {
DEFAULT_LAT = 60.170718;
DEFAULT_LON = 24.941469;
geocoder = LG.geocoders.nominatim({
reverseQueryParams: {
addressdetails: true,
zoom: 18,
"accept-language": "en-US",
},
geocodingQueryParams: {
addressdetails: true,
zoom: 18,
"accept-language": "en-US",
},
});
map: L.Map | undefined;
userLocation: GeolocationCoordinates | undefined;
marker: L.Marker | undefined;
lonInput: HTMLInputElement;
latInput: HTMLInputElement;
setAddress: SetAddressFunction;
constructor(
lonInput: HTMLInputElement,
latInput: HTMLInputElement,
mapContainer: HTMLElement,
setAddress: SetAddressFunction
) {
this.lonInput = lonInput;
this.latInput = latInput;
this.setAddress = setAddress;
this.init(mapContainer);
}
getUserPosition() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
this.userLocation = pos.coords;
});
}
}
init(element: HTMLElement) {
const lat = this.latInput.value ? +this.latInput.value : this.DEFAULT_LAT;
const lon = this.lonInput.value ? +this.lonInput.value : this.DEFAULT_LON;
this.map = L.map(element).setView([lat, lon], 13);
L.tileLayer("//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(this.map);
L.Icon.Default.imagePath = "/images/leaflet/";
const geocoderControl = LG.geocoder({
geocoder: this.geocoder,
placeholder: "Search",
defaultMarkGeocode: false,
});
geocoderControl.addTo(this.map);
geocoderControl.on("markgeocode", (event) =>
this.onMapSearchResultClick(event)
);
this.map.on("click", (event) => this.onMapClick(event));
if (this.latInput.value && this.lonInput.value) {
const coords = new L.LatLng(+this.latInput.value, +this.lonInput.value);
this.updateCoordinates(coords);
}
}
onMapSearchResultClick(event: MarkGeocodeEvent) {
this.updateCoordinates(event.geocode.center);
this.updateAddress(event.geocode.properties.address);
}
onMapClick(event: L.LeafletMouseEvent) {
this.updateCoordinates(event.latlng);
this.setAddress(undefined);
this.geocoder.reverse(event.latlng, 100000000, (results) => {
if (!results) return;
this.updateAddress(results[0].properties.address);
});
}
updateCoordinates(coords: L.LatLng) {
if (this.marker) this.marker.remove();
this.marker = L.marker(coords).addTo(this.map as L.Map);
this.latInput.value = coords.lat.toString();
this.lonInput.value = coords.lng.toString();
}
updateAddress(address: Address) {
this.setAddress(address);
}
formatAddress(address: Address): string {
return `${address.postcode || ""}, ${
address.city || address.city_district || ""
}, ${address.road || ""}, ${address.house_number || ""}`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment