馃嚭馃嚫 English | 馃嚜馃嚫 Espa帽ol

Mapas de MapKit JS en Ruby on Rails y Stimulus

Ya que estoy pagando por la cuenta de desarrollador de Apple he querido hacer uso de su servicio de mapas para desarrollar Pizza Pals.

Apple proporciona toda la documentaci贸n necesaria para usar sus mapas. Aqu铆 voy a explicar c贸mo los he integrado con una aplicaci贸n Rails.

Necesitamos un controlador con su ruta y su vista:

$ bin/rails g controller maps index

La documentaci贸n nos ofrece el script que debemos insertar para cargar MapKit. Lo puedes meter en la vista que acabas de generar o en el header de la aplicaci贸n.

<script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js"
    crossorigin async
    data-callback="initMapKit"
    data-libraries="services,full-map,geojson"></script>

Lo siguiente que vamos a generar es un controlador de Stimulus. En realidad este paso es totalmente innecesario 馃し鈥嶁檪锔 se podr铆a omitir y todo funcionar铆a igual.

$ bin/rails g stimulus maps

Actualiza la vista a帽adiendo el div que contendr谩 el mapa. Has de especificar ancho y alto o de lo contrario el mapa no se ver谩. Tambi茅n has de darle un id y, como hemos decidido usar Stimulus de manera innecesaria, llamar al controlador.

# app/views/maps/index.html.erb

<div style="width: 600px; height: 300px" id="map-container" data-controller="maps"></div>

Apple proporciona ejemplos de c贸mo usar la librer铆a. Me voy a basar en el Embedded Map. Es posible que necesites una cuenta de desarrollador para acceder a este enlace.

Trasladamos el c贸digo a Stimulus y queda algo as铆:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="maps"
export default class extends Controller {
  async connect() {
    this.initMap()
  }

  async setupMapKit() {
    if (!window.mapkit || window.mapkit.loadedLibraries.length === 0) {
      await new Promise(resolve => { window.initMapKit = resolve });

      delete window.initMapKit;
    }

    const jwt = "Insert Your JWT Here";
    mapkit.init({
        authorizationCallback: done => { done(jwt); }
    });
  }

  async initMap() {
    await this.setupMapKit()

    const cupertino = new mapkit.CoordinateRegion(
      new mapkit.Coordinate(37.3316850890998, -122.030067374026),
      new mapkit.CoordinateSpan(0.167647972, 0.354985255)
    );

    const map = new mapkit.Map("map-container");
    map.region = cupertino;
  }
}

Como todavia no hemos generado un jwt, veremos el siguiente error en la consola al visitar la p谩gina: [MapKit] Authorization token is invalid.

Vamos a solucionarlo. Necesitamos un identificador de mapa y una clave privada. Una vez creados puedes ir al dashboard y acceder a create token. Necesitaremos los datos generados en el paso anterior y subir el archivo .p8. En domain restriction puedes poner localhost o dejarlo vac铆o por el momento.

Dashboard

Una vez generado el token lo puedes pegar en el codigo: const jwt = "Insert Your JWT Here";

Si arrancas el servidor y vas a la ruta que has creado deber铆as ver el mapa. Si no ves nada, asegurate de haberle dado ancho y alto al div.

Gestionar tokens de esta manera es engorroso y peligroso. Por ello vamos a crear un endpoint en nuestro backend que genere tokens cada vez que se cargue un mapa. Puedes especificar la caducidad de un token y reutilizarlo tantas veces como quieras en un a帽o.

Cambia dentro del controlador de Stimulus:

const jwt = "Insert Your JWT Here";
mapkit.init({
    authorizationCallback: done => { done(jwt); }
});

por:

mapkit.init({
    authorizationCallback: done => {
      fetch("/tokens/mapkit")
        .then(res => res.text())
        .then(done)
    }
})

Usando fetch, el inicializador del mapa har谩 una petici贸n http a una ruta de nuestro servidor que devolver谩 el token. De este modo no lo tenemos incrustado en el c贸digo.

Crea otro controlador. En este caso lo he llamado tokens y al m茅todo mapkit. A帽ade tambi茅n una ruta. No necesitamos la vista.

$ bin/rails g controller tokens mapkit

Dentro del controlador, en el metodo mapkit, puedes devolver el token:

def mapkit
  render plain: "[reemplaza por el token]"
end

Todavia no es donde queremos llegar, pero es un paso mas cerca. Hemos pasado de tener el token incrustado en el frontend a tenerlo en el backend. Ahora, en vez de devolver el mismo token tenemos que generar uno cada vez. C贸mo?

Vamos a usar una clase Mapkit y un metodo est谩tico token para generar el token. Mientras la implementamos podemos actualizar el codigo del controlador y asi ya nos olvidamos:

def mapkit
  render plain: Mapkit.token
end

Ahora crea la clase donde m谩s rabia te d茅 dentro de tu aplicaci贸n Rails.

Como vamos pasito a pasito, puedes llevarte ah铆 el token que ya tenemos y comprobar que todo sigue funcionando:

class Mapkit
  class << self
    def token
      "[reemplaza por el token]"
    end
  end
end

Vamos a necesitar algo de ayuda para generar el jwt. En este caso he decidido usar la gema jwt pero imagino que puedes usar la que te de la gana o hac茅rtelo a mano.

$ bundle add jwt

La documentaci贸n dice que para construir el token necesitamos dos cosas: un header y un payload. Cada uno se compone de varias cosas:

En el header tenemos que especificar el algoritmo, que ha de ser ES256. Tambi茅n necesitamos especificar la key id que hemos obtenido antes y por 煤ltimo simplemente indicar que el typ es 鈥淛TW鈥.

En el payload tenemos que especificar el team id que tambi茅n hemos obtenido antes, la fecha de generaci贸n del token (es decir, ahora), la fecha de expiraci贸n del token (hasta un a帽o) y finalmente el origen. Como estamos desarrollando en local, he puesto localhost. En producci贸n y otros entornos tendr谩s que especificar el dominio desde el que se usar谩, sino el token ser谩 inv谩lido y los mapas no se mostrar谩n.

class Mapkit
  class << self
    ALGORITHM = "ES256"

    def token
      header = {
        alg: ALGORITHM,
        kid: "[replace with your key id]",
        typ: "JWT"
      }

      payload = {
        iss: "[replace with your team id]",
        iat: Time.now.to_i,
        exp: 1.day.from_now.to_i,
        origin: "localhost"
      }

      JWT.encode(payload, private_key, ALGORITHM, header)
    end

    private

      def private_key
        OpenSSL::PKey::EC.new("-----BEGIN PRIVATE KEY-----
RANDOM STRING INSIDE THE .P8 FILE
-----END PRIVATE KEY-----")
      end
  end
end

Si miras el m茅todo private_key ver谩s que el contenido no est谩 tabulado. Esto me trajo un poco de cabeza. Hay que poner el contenido tal cual esta en el fichero .p8 que te descargas, si no te va a dar errores como OpenSSL::PKey::ECError (invalid curve name):.

Si reinicias el servidor todo deber铆a seguir funcionando correctamente. Ya casi hemos terminado.

Obviamente trat谩ndose de informaci贸n sensible no la vamos a dejar al descubierto.

Puedes guardar las claves en variables de entorno. Yo he decidido hacerlo en los credenciales de Rails:

$ EDITOR="code --wait" bin/rails credentials:edit

Si no usas VS Code no necesitas la primera parte.

mapkit:
  key_id: your-key-id
  team_id: your-team-id
  key_file: |
    -----BEGIN PRIVATE KEY-----
    random text
    -----END PRIVATE KEY-----

F铆jate bien en el s铆mbolo | despu茅s de key_files. Es parte de YAML y es muy importante ya que hace que se respeten los saltos de l铆nea. Si no lo pones o usas el s铆mbolo > como se usa a veces, volver谩s a tener el mismo error OpenSSL::PKey::ECError (invalid curve name): de antes. Si crees que lo est谩s haciendo bien y a煤n as铆 obtienes el error, prueba a reiniciar el servidor. Puede que no se hayan recargado los credenciales correctamente.

脷ltimos escritos

Proyectos

Recibe actualizaciones de mis proyectos

    Nombre
    Email

    D贸nde encontrarme