Integrating Google Maps into Frappe Framework Replace OpenStreet: A Step-by-Step Guide

Integrating Google Maps into Frappe Framework: A Step-by-Step Guide

image

This documentation provides a clean and structured explanation of how to replace OpenStreetMap with Google Maps in a Frappe-based application. The example demonstrates integrating Google Maps for geolocation visualization and updating the map dynamically based on latitude and longitude values.


1. Overview

The goal is to replace OpenStreetMap with Google Maps for better usability and functionality. This involves:

  • Fetching the Google Maps API key securely.
  • Initializing the Google Maps JavaScript API.
  • Dynamically updating the map based on latitude and longitude values stored in the document.
  • Ensuring proper error handling and debouncing for performance optimization.

2. Implementation

2.1 Backend Configuration

a. Fetching the Google Maps API Key

To ensure security, the Google Maps API key is stored encrypted in the iScore Settings document. A custom method retrieves the decrypted key.

import frappe
from frappe.utils.password import get_decrypted_password
@frappe.whitelist()
def get_google_map_api_key():
    """
    Retrieves the decrypted Google Maps API key from the system settings.
    """
    return get_decrypted_password("System Settings", "System Settings", "google_map_api_key")

2.2 Frontend Integration

a. Loading the Google Maps JavaScript API

The Google Maps JavaScript API is loaded dynamically using the importLibrary method. This ensures modularity and avoids loading unnecessary libraries.

refresh: function(frm) {
    frappe.call({
        method: 'your_app.your_module.get_google_map_api_key',
        callback: function(response) {
            (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn():d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
                key: response.message,
                v: "weekly",
            });
        // Initialize the map after loading the API
        initializeMap(frm);
        }
    });
}

b. Initializing the Map

Once the API is loaded, the map is initialized in the form’s UI. The map replaces the existing OpenStreetMap element and is dynamically updated based on the document’s latitude and longitude.

function initializeMap(frm) {
    const originalElement = frm.$wrapper.find('.frappe-control[data-fieldtype="Geolocation"][data-fieldname="map"] .control-input-wrapper');
    if (originalElement.length) {
        originalElement.hide(); // Hide the original OpenStreetMap element
        if (!document.getElementById('google_map')) {
            // Create a new div for Google Maps
            $('<div id="google_map" style="height:500px;width:100%"></div>').insertAfter(originalElement);
        }
    }
    updateMap(parseFloat(frm.doc.latitude), parseFloat(frm.doc.longitude));
}

c. Updating the Map Dynamically

The map and marker are updated whenever the latitude and longitude values change. If the map does not exist, it is initialized; otherwise, the existing map and marker are updated.

let map;
let marker;
async function updateMap(lat,lon) {
    const position = { lat: lat, lng: lon };
    if (!map) {
        // First time initialization
        const { Map } = await google.maps.importLibrary("maps");
        const { Marker } = await google.maps.importLibrary("marker");
        map = new Map(document.getElementById('google_map'), {
            center: position,
            zoom: 17
        });
        marker = new Marker({
            position,
            map,
            title: "Selected Location"
        });
    } else {
        // Update existing map and marker
        map.setCenter(position);
        marker.setPosition(position);
    }


3. Key Benefits

  1. Improved Usability : Google Maps provides a familiar interface with advanced features like street view and satellite imagery.
  2. Dynamic Updates : The map updates automatically based on latitude and longitude values, ensuring real-time visualization.
  3. Security : The Google Maps API key is stored securely and fetched dynamically.

4. Prerequisites

  1. Google Maps API Key : Obtain a valid API key from the Google Cloud Console .
  2. Frappe Framework : Ensure you are using a compatible version of Frappe.
  3. Encrypted Storage : Use the get_decrypted_password utility to securely store and retrieve the API key.
5 Likes

Just placing this here, you really want to limit api keys to your prod and test servers on the GCP Platform.
With website restrictions, IP restrictions, wherever else.
Example of web restrictions that work:

https://subdomain.maindomain.com/*
https://www.maindomain.com/*
http://www.maindomain.com/*

Also, and I could be wrong but this function ASSUMES all users will have access to the google maps integration settings which you definitely do NOT want. Here is a better, more robust solution to the python function imo

  def get_google_map_api_key():
      """Retrieves the decrypted Google Maps API key - 
  accessible to all authenticated users"""
      try:
          settings = frappe.get_doc("System Settings",
  "System Settings", ignore_permissions=True)
          api_key =
  settings.get_password("google_map_api_key")
          if not api_key:
              frappe.throw("Google Maps API key not 
  configured. Please contact your administrator.")
          return api_key
      except Exception as e:
          frappe.log_error(f"Failed to get API key: 
  {str(e)}", "Google Maps")
          frappe.throw("Could not retrieve Google Maps 
  configuration. Please contact support.")

Tangent to this is an address autocomplete field. Which I believe is much more widely used and vaulable compared to a map.

 def get_google_maps_api_key():
      """Fetch encrypted API key with elevated 
  permissions - users can't access settings directly"""
      try:
          # Bypass permissions to allow any authenticated
   user to get the API key
          frappe.flags.ignore_permissions = True
          api_key = get_decrypted_password("System 
  Settings", "System Settings", "google_maps_api_key")
          frappe.flags.ignore_permissions = False

          if not api_key:
              frappe.throw("Google Maps API key not 
  configured")
          return api_key
      except Exception as e:
          frappe.log_error(f"Failed to get API key: 
  {str(e)}", "Google Maps")
          frappe.throw("Could not retrieve Google Maps 
  configuration")

Then in the doctype with address field = [address_field]

frappe.call({
      method:
  "avaza_frappe.avaza_frappe.doctype.onsite_interpreting.
  onsite_interpreting.get_google_maps_api_key",
      callback: async function(r) {
          if (r.message) {
              // Dynamically load Google Maps
              await loadGoogleMapsScript(r.message);

              // Initialize autocomplete on address field
              const input =
  frm.fields_dict.address_field.$input[0];
              const autocomplete = new
  google.maps.places.Autocomplete(input, {
                  types: ['address'],
                  componentRestrictions: { country: 'us'
  }
              });

              // Update field when address selected
              autocomplete.addListener('place_changed',
  function() {
                  const place = autocomplete.getPlace();
                  if (place.formatted_address) {
                      frm.set_value('address_field',
  place.formatted_address);
                  }
              });
          }
      }
  });
2 Likes

I was thinking, given the simplicity of changing the Map of ERPNext, and the high demand and trust people have in Google Maps compared to any other alternative. What do you think about creating a custom app to integrate Google Maps with ERPNext, as an open-source project.

It could have a Single Page DocType to add the API keys and other important settings and enable or disable Google Maps.
Minor updates could be related to accommodate with Google Maps integration changes or bug fixes to reduce the API calls; thus reduce costs.

I really think many would be interested in such app!

1 Like

thank you so much, i will try it

that is a great idea, i already wanted to start side project to enhance some features in erpnext, it would be great idea to add google map option for “geolocation” field

let’s do it

I will push back on this, only because, truly, it takes ONE doctype and one field to make this happen.

One password api key field.

The rest can, quite easily, be done with client/server side scripts.

If a custom app were to be made, it should focus on morphing existing data fields utilizing the “options” menu. However and again, this is more useful as a “code snippet”.

That said, I wish there was a repository of code snippets with the ability to upvote really helpful ones.

That might be worth making…

hmmm…