Jump to content
Sign in to follow this  
Czar

Autocomplete Address INformation

Recommended Posts

Showing my lack of understanding yet again. I am struggling with trying to get googles autocomplete of addresses to work with an SMS form.

This looks really good

https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete-addressform

However, I don't understand how I would go about incorporating that functionality into a SMS form. I tried adding it is using ASM and doing an inner html for the html part but I couldn't get any joy.

 

Any help would be much appreciated.

 

Share this post


Link to post
Share on other sites

Hi Czar

Here is one (of probably many) solutions. (in haste, so could be streamlined a bit)

unit Form1;

interface

uses 
  System.Types,
  System.Types.Convert,
  System.Objects,
  System.Time,
  SmartCL.System,
  SmartCL.Time,
  SmartCL.Graphics,
  SmartCL.Components,
  SmartCL.FileUtils,
  SmartCL.Forms,
  SmartCL.Fonts,
  SmartCL.Theme,
  SmartCL.Borders,
  SmartCL.Application, SmartCL.Controls.EditBox, SmartCL.Controls.Memo;

type
  TForm1 = class(TW3Form)
  private
    {$I 'Form1:intf'}
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
    EditBox1 : TW3EditBox;
    Memo1    : TW3Memo;
  end;

implementation

{ TForm1 }

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components
  EditBox1 := TW3EditBox.Create(self);
  EditBox1.SetBounds(40,40,350,35);
  EditBox1.PlaceHolder := 'Enter your address';
  var v : variant := new JObject;
  v := EditBox1.handle.id;

  Memo1 := TW3Memo.Create(self);
  Memo1.SetBounds(40,150,350,200);
  var w : variant := new JObject;
  w := Memo1.handle.id;

  var Script := browserapi.document.createElement('script');
  Script.src := 'https://maps.googleapis.com/maps/api/js?key=YOURAPIKEY&libraries=places';         //<======= API key
  browserapi.document.head.appendChild(Script);
  Script.onload := procedure
  begin
    writeln('loaded');
    asm
      var placeSearch, autocomplete;
      var componentForm = {
        street_number: 'short_name',
        route: 'long_name',
        locality: 'long_name',
        administrative_area_level_1: 'short_name',
        country: 'long_name',
        postal_code: 'short_name'
      };

      function initAutocomplete() {
        // Create the autocomplete object, restricting the search to geographical
        // location types.
        autocomplete = new google.maps.places.Autocomplete(
            /** @type {!HTMLInputElement} */(document.getElementById(@v)),
            {types: ['geocode']});

        // When the user selects an address from the dropdown, populate the address
        // fields in the form.
        autocomplete.addListener('place_changed', fillInAddress);
      }

      function fillInAddress() {
        // Get the place details from the autocomplete object.
        var place = autocomplete.getPlace();

///        for (var component in componentForm) {
//          document.getElementById(component).value = '';
//          document.getElementById(component).disabled = false;
//        }

        document.getElementById(@w).innerHTML = '';   //clear Memo

        // Get each component of the address from the place details
        // and fill the corresponding field on the form.
        for (var i = 0; i < place.address_components.length; i++) {
          var addressType = place.address_components[i].types[0];
          console.log(addressType);
          document.getElementById(@w).innerHTML += addressType + '\n';  //add addressType to Memo
          if (componentForm[addressType]) {
            var val = place.address_components[i][componentForm[addressType]];
            console.log(val);
            document.getElementById(@w).innerHTML += val + '\n\n';      //add address component value to Memo
//          document.getElementById(addressType).value = val;
          }
        }
      }

      initAutocomplete();

      document.getElementById(@v).addEventListener("focus", function( event ) {
        console.log('onfocus');
        event.target.style.background = "pink";

        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(function(position) {
            var geolocation = {
              lat: position.coords.latitude,
              lng: position.coords.longitude
            };
            var circle = new google.maps.Circle({
              center: geolocation,
              radius: position.coords.accuracy
            });
            autocomplete.setBounds(circle.getBounds());
          });
        }

      }, true);
    end;
  end;

end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
end;
 
procedure TForm1.Resize;
begin
  inherited;
end;
 
initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.

 

To break it down a bit :

The EditBox is the component where you type in (the beginning of) an address and the Google Places API produces a dropdown underneath. The initAutoComplete function searches for this field and the link is made via it's Editbox.handle.id value

I like to download files in code so hence made up a script element and for simplicity added most of Googles js code in the onload handler. Then create the autocomplete object by calling initAutoComplete. There are other ways of doing this and the RTL has some script loading functions built in, but this works too.

The focus handler is supposed to limit the global address domain to something in a particular radius from the user. Not sure if this works as expected here, might need some further attention.

Anyway, this is the result from this project :

address.jpg

Once the user selects an address, the individual address details are -in this code example- displayed in the Memo and on the console, but of course should be used to populate other components on the form

Spending some more time on it would diminish or even eliminate the asm block, but this is a quick usage of Googles example in your post

Hope this helps

 

edit : the onFocus handler (to limit addresses to the neighbourhood) depends on the 'location' settings in your Windows settings

Project tested on Chromium in the ide and on Edge, Chrome

 

 

Share this post


Link to post
Share on other sites

You are awesome. This will streamline the address entry system a great deal. As soon as I get back I will take it for a spin and figure out how I will apply it. Thanks very much.

Share this post


Link to post
Share on other sites

nice @lynkfs  i used your code to make a component......although, the "fillInAddress" javascript function could use some work (i.e. something different than the if else stements)

unit AutoCompleteEdit;

interface

uses 
  System.Types,
  System.Types.Convert,
  SmartCL.System,
  SmartCL.Controls.EditBox;

type

 TAutoCompleteEdit = class(TW3EditBox)
 private
  fKey: string;

  fstreet_number: string;
  froute: string;
  flocality: string;
  fadministrative_area_level_1: string;
  fcountry: string;
  fpostal_code: string;
  function get_street_number: string;
  function get_route: string;
  function get_locality: string;
  function get_administrative_area_level_1: string;
  function get_country: string;
  function get_postal_code: string;

  function getKey: String;
  procedure setKey(Value: String);

 protected
  procedure ObjectReady; override;
 public
  property GoogleAPIKey: String read getKey write setKey;

  property street_number: string read get_street_number;
  property route: string read get_route;
  property locality: string read  get_locality;
  property administrative_area_level_1: string read get_administrative_area_level_1;
  property country: string read get_country;
  property postal_code: string read get_postal_code;

 end;

implementation

function TAutoCompleteEdit.getKey: String;
begin
 result:= fKey;
end;

procedure TAutoCompleteEdit.setKey(Value: String);
begin
 fKey:= Value;
end;

function TAutoCompleteEdit.get_street_number: string;
begin
 result:= fstreet_number;
end;

function TAutoCompleteEdit.get_route: string;
begin
 result:= froute;
end;

function TAutoCompleteEdit.get_locality: string;
begin
 result:= flocality;
end;

function TAutoCompleteEdit.get_administrative_area_level_1: string;
begin
 result:= fadministrative_area_level_1;
end;

function TAutoCompleteEdit.get_country: string;
begin
 result:= fcountry;
end;

function TAutoCompleteEdit.get_postal_code: string;
begin
 result:= fpostal_code;
end;

procedure  TAutoCompleteEdit.ObjectReady;
begin
 inherited;
  var v : variant := new JObject;
  v := handle.id;

  var Script := browserapi.document.createElement('script');
  Script.src := 'https://maps.googleapis.com/maps/api/js?key=' + fKey + '&libraries=places';
  browserapi.document.head.appendChild(Script);

  Script.onload := procedure
  begin
    asm
     var componentForm = {
        street_number: 'short_name',
        route: 'long_name',
        locality: 'long_name',
        administrative_area_level_1: 'short_name',
        country: 'long_name',
        postal_code: 'short_name'
      };

      function initAutocomplete() {
       autocomplete = new google.maps.places.Autocomplete(
        (document.getElementById(@v)), {types: ['geocode']});
       autocomplete.addListener('place_changed', fillInAddress);
      }

      function fillInAddress() {
        var place = autocomplete.getPlace();

        for (var i = 0; i < place.address_components.length; i++) {
          var addressType = place.address_components[i].types[0];
          if (componentForm[addressType]) {
            var val = place.address_components[i][componentForm[addressType]];
          }
        if (addressType == 'street_number') {
         @fstreet_number = val
        }
        else
        if (addressType == 'route') {
         @froute = val
        }
        else
        if (addressType == 'locality') {
          @flocality = val;
        }
        else
        if (addressType == 'administrative_area_level_1') {
          @fadministrative_area_level_1 = val;
        }
        else
        if (addressType == 'country') {
          @fcountry = val;
        }
        else
        if (addressType == 'postal_code') {
          @fpostal_code = val;
        }
      }
       }

     initAutocomplete();

      document.getElementById(@v).addEventListener("focus", function( event ) {
       event.target.style.background = "pink";

        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(function(position) {
            var geolocation = {
              lat: position.coords.latitude,
              lng: position.coords.longitude
            };
            var circle = new google.maps.Circle({
              center: geolocation,
              radius: position.coords.accuracy
            });
            autocomplete.setBounds(circle.getBounds());
          });
        }

      }, true);
  end;
  end;
end;

end.

anyway, it fills in the components public properties

  • street_number
  • route
  • locality
  • state
  • country
  • postal_code

i use it like such

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components

  EditBox1:= TAutoCompleteEdit.Create(self);
  EditBox1.SetBounds( 10,10, 200, 32);
  EditBox1.PlaceHolder := 'Enter your address';
  EditBox1.GoogleAPIKey:= 'AIzaSyCU7roAAjLtFT2ItaMLcS6WN**********';
  
  Memo1:= TW3Memo.Create(self);
  Memo1.SetBounds(10, 50, 190, 260);

  Button1:= TW3Button.Create(self);
  Button1.SetBounds(10, Memo1.Top + Memo1.Height + 10, 128, 32);
  Button1.Caption:= 'Values';
  Button1.OnClick:= HandleClick;

end;
procedure TForm1.HandleClick(sender: TObject);
begin
 Memo1.Clear;
 Memo1.Add(EditBox1.street_number +  #10#13 +
           EditBox1.route  +  #10#13 +
           EditBox1.locality  +  #10#13 +
           EditBox1.administrative_area_level_1  +  #10#13 +
           EditBox1.country  +  #10#13 +
           EditBox1.postal_code  +  #10#13);
end;

EXAMPLE

rw05qr.png

Share this post


Link to post
Share on other sites

building on, see previous posts ...

got rid of most of the asm blocks.

Biggest change : generate additional change event 

unit AutoComplete;

interface

uses
  System.Types,
  System.Types.Convert,
  SmartCL.System,
  SmartCL.Controls.EditBox;

type

 TAutoComplete = class(TW3EditBox)
 private
  autocomplete: variant;
  componentForm : variant;
  procedure initAutoComplete;
  procedure fillInAddress;
 protected
  procedure ObjectReady; override;
 public
  property APIKey: String;

  property street_number: string;
  property route: string;
  property locality: string;
  property administrative_area_level_1: string;
  property country: string;
  property postal_code: string;

 end;

implementation

Procedure TAutoComplete.initAutocomplete;
begin
  var v : variant := self.handle;
  asm @autocomplete = new google.maps.places.Autocomplete(
  (@v), {types: ['geocode']}); end;
  autocomplete.addListener('place_changed', @fillInAddress);
end;

Procedure TAutoComplete.fillInAddress;
var
  place, val, addressType, evt : variant;
begin
  place := autocomplete.getPlace();
  for var i := 0 to place.address_components.length -1 do begin
    addressType := place.address_components[i].types[0];
    if componentForm[addressType] then
      val := place.address_components[i][componentForm[addressType]];
    case addressType of
      'street_number' : street_number := val;
      'route'         : route := val;
      'locality'      : locality := val;
      'administrative_area_level_1'
                      : administrative_area_level_1 := val;
      'country'       : country := val;
      'postal_code'   : postal_code := val;
    end;
  end;

  asm @evt = new Event('change'); end;
  self.handle.dispatchEvent(evt);
end;

procedure TAutoComplete.ObjectReady;
begin
  inherited;

  var Script := w3_createHtmlElement('script');
  Script.src := 'https://maps.googleapis.com/maps/api/js?key=' + APIKey + '&libraries=places';
  w3_setElementParentByRef(script, browserapi.document.head);

  Script.onload := procedure
  begin
    componentForm := new JObject;
    componentForm.street_number := 'short_name';
    componentForm.route := 'long_name';
    componentForm.locality := 'long_name';
    componentForm.administrative_area_level_1 := 'short_name';
    componentForm.country := 'long_name';
    componentForm.postal_code := 'short_name';

    self.onGotFocus := procedure(sender: TObject)
    begin
      self.handle.style.background := 'pink';

      if browserapi.window.navigator then begin
        browserapi.window.navigator.geolocation.getCurrentPosition(procedure(position:variant)
        begin
          var geolocation : variant := new JObject;
          geolocation.lat := position.coords.latitude;
          geolocation.lng := position.coords.longitude;

          var circle : variant;
          asm
            @circle = new google.maps.Circle({
              center: geolocation,
              radius: position.coords.accuracy });
          end;
          autocomplete.setBounds(circle.getBounds());
        end);
      end;
    end;

    initAutocomplete;
  end;

end;

end.

invoke :

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components

  AutoComplete1:= TAutoComplete.Create(self);
  AutoComplete1.SetBounds( 10,10, 300, 32);
  AutoComplete1.PlaceHolder := 'Enter your address';
  AutoComplete1.APIKey:= 'AIzaSyD1rrlVNrQmis5lHNIgB************';

  AutoComplete1.OnChanged := procedure(sender:TObject)
  begin
    Memo1.Clear;
    Memo1.Add(AutoComplete1.street_number +  #10 +
              AutoComplete1.route  +  #10 +
              AutoComplete1.locality  +  #10 +
              AutoComplete1.administrative_area_level_1  +  #10 +
              AutoComplete1.country  +  #10 +
              AutoComplete1.postal_code);
  end;

  Memo1:= TW3Memo.Create(self);
  Memo1.SetBounds(10, 50, 300, 260);

end;

 

 

Share this post


Link to post
Share on other sites

Thanks for all the input. It certainly makes address input much easier and slicker.

 

However,

I am running into some challenges. When I use the sample code and the AutoComplete unit with SMS I get a message saying that getCurrentPosition() and watchPosition() are deprecated on insecure origins. So that means  addresses come up with American suggestions first. Ok I thought I will simply put the app on our website.

Not as easy as I expected because now I get an error when loading googles api

 

  1. Request URL:
  2. Referrer Policy:
    no-referrer-when-downgrade
https://numberworksnwords.com/cloud/autocomplete/index.html

It seems I am missing an obvious piece of the puzzle. :( Any suggestions? I tried reading what "no-referrer-when-downgrade" meant but that gave me a head ache :)
 

 

Share this post


Link to post
Share on other sites

except for getting american addresses first, your link works as expected on all browsers for me.

Inspecting the console though I notice an error, : 'position' is not defined

probably in the part where it tries to identify your current position

I'll have a look later today

 

 

Share this post


Link to post
Share on other sites

delete the app.manifest file from the server

the other thing is the protocol used, see https://developers.google.com/web/updates/2016/04/geolocation-on-secure-contexts-only

which means location discovery doesn't work on http, only on https

I had it working in some stage, so will dig a bit deeper after work

 

cheers

 

 

Share this post


Link to post
Share on other sites

OK removed app.manifest didn't seem to make a difference.

If I enter "https://maps.googleapis.com/maps/api/js?key=AIzaSyBbF5_rXWgH79aG3LXxag6G3tsbce1Jlok&libraries=place" into my browser it will display js

 

I am aware of the htpps requirements for location. Which explains why the local region is not working. So if you bring up the app and F12 in the browser you don't see errors for loading the js file?

cheers

 

Share this post


Link to post
Share on other sites

forgetting for the moment the position discovery mechanism based on ip address, there are a couple of alternatives :

- set the domain to a country (example : Australia)

Procedure TAutoComplete.initAutocomplete;
begin
  var v : variant := self.handle;
  asm autocomplete = new google.maps.places.Autocomplete(
  (@v), {types: ['address'],componentRestrictions: {country: 'au'}}); end;                                 //geocode
  autocomplete.addListener('place_changed', @fillInAddress);
end;

- or set the domain to a self-defined region (example : part of Sydney)

Procedure TAutoComplete.initAutocomplete;
begin
  var v : variant := self.handle;
  asm
    var defaultBounds = new google.maps.LatLngBounds(
    new google.maps.LatLng(-33.8902, 151.1759),
    new google.maps.LatLng(-33.8474, 151.2631));

  @autocomplete = new google.maps.places.Autocomplete(
  (@v), {types: ['address'],bounds: defaultBounds}); end;                                 //geocode
  autocomplete.addListener('place_changed', @fillInAddress);
end;

Maybe that will help you for the time being.

still digging a bit more in the 'positioning' thing ...

added :

the problem I see here is that even using Googles examples, using their own domain (https), and changing my local location settings to 'allow' everything, it actually doesn't detect my physical location, not on Chrome desktop or mobile browser.

It won't work anyway for users on a vpn (or in China), and I suspect the ip+wifi detection mechanism is not very reliable to begin with.

Test it yourself on https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation , scrolling down to 'Live Result' :

  • doesn't work on Chrome. (read somewhere that additional restrictions have come into effect last month, requiring a Feature Policy header ?)
  • does work on FireFox, but takes a long time, which makes it unusable in applications
  • Edge same as FireFox

At this point I personally wouldn't use it in production scenario's if there are many non-standardised user environments (different browsers, versions, vpn's, wifi etc), but use one of the 2 alternatives above (country or area). In that case the focus paragraph would become really simple (or can be omitted altogether) :

self.onGotFocus := procedure(sender: TObject)
begin
  self.handle.style.background := 'pink';
end;

Hope this helps

Cheers

Share this post


Link to post
Share on other sites

Cheers, I owe you all a few beers! You will have to collect in Auckland :)

It works a treat. We have lat/long stored for each centre so it is was piece of cake to pass the coordinates along. Works really really well. 

There were a few hiccups dealing with different countries and situations. In the UK for example the "town" is contained in "postal_town". 

I added a few AddressTypes to sort it out. Otherwise this is a super useful unit, thanks very much.

 

    componentForm.subpremise := 'short_name';
    componentForm.street_number := 'short_name';
    componentForm.route := 'long_name';
    componentForm.locality := 'long_name';
    componentForm.postal_town := 'long_name';
    componentForm.sublocality_level_1 := 'long_name';
    componentForm.administrative_area_level_1 := 'short_name';
    componentForm.country := 'long_name';
    componentForm.postal_code := 'short_name';
    componentForm.postal_code_prefix := 'short_name';

 

 

 

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×