You're all set!

To start developing, please head over to our developer documentation.

Activate the Google Maps JavaScript API

To get you started we'll guide you through the Google Developers Console to do a few things first:

  1. Create or choose a project
  2. Activate the Google Maps JavaScript API
  3. Create appropriate keys
Continue

NYC Subway Locator

The NYC Subway Station Locator solution is a slightly different take on the store locator concept. It builds on the foundation that we created with the Simple Store Locator, adding enhancements to handle larger numbers of markers. This tutorial shows you how to do the following things:

  • Use GeoJSON to define a static list of locations and associated metadata.
  • Display markers and polylines on a map.
  • Create a custom InfoWindow to display location metadata.
  • Integrate marker clustering to optimize the display for larger numbers of markers.
  • Improve efficiency by returning only features within the visible area of the map.

Get the code

Clone the NYC Subway Locator repo to your Google Cloud Platform instance, or your local computer.

GeoJSON data sets

The data sets for subway stations and subway lines are publicly available, thanks to NYC Open Data. Follow these steps to download the GeoJSON files and add them to your project:

  1. In Cloud Shell, cd to the data directory:

    cd data
    
  2. Use cURL to retrieve the subway station GeoJSON:

    curl "https://data.cityofnewyork.us/api/geospatial/arq3-7z49?method=export&format=GeoJSON" -o subway-stations.geojson
    
  3. Use cURL once more to retrieve the subway line GeoJSON:

    curl "https://data.cityofnewyork.us/api/geospatial/3qz8-muuu?method=export&format=GeoJSON" -o subway-lines.geojson
    

JavaScript and HTML

Golang app files

YAML configuration file

Packages

Below is a list of the packages required to run this solution, with a summary of their functionality.

Package
go.geojson Handles parsing of GeoJSON data.
rtreego Bounds data queries to the map viewport.
go-point-clustering Handles marker clustering.

Set up your development project

Before starting this tutorial, follow these instructions to get an API key and set up Google Cloud Platform.

Add the API key to your application

Add the API key to your project as follows:

  • index.html — in the src attribute of the script tag, replace YOUR_API_KEY with your actual API key:

    <script async defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
    </script>
    

Build and run your app

Follow these steps to build and run your app using Google Cloud Platform:

  1. If you have not done so yet, clone the solution repo to your GCP machine or local computer (these instructions assume that you're using a GCP machine).
  2. From the top-level directory, run the following command to start the App Engine development server:

    $ goapp serve
    
  3. Click the first item on the Cloud Shell toolbar, and select Preview on port 8080. A new browser tab opens, displaying a map centered on New York City, with markers representing subway stations, and polylines depicting the subway lines.

Understand the code

This part of the tutorial explains the most significant parts of the NYC Subway Locator application, to help you understand how to build a similar app.

Initialize the map

index.html is a minimal HTML file that loads the Maps JavaScript API and app.js script, then displays the map in a div which is styled so that the map takes up the entire page when it loads. When index.html loads for the first time, the first script tag loads app.js, and the second script tag loads the Maps JavaScript API, specifying the API key and the name of the callback function to invoke once loading is complete:

<html>
  <head>
    <title>NYC Subway Stations</title>
    <link rel="stylesheet" type="text/css" href="style.css">
  <head>
  <body>
    <div id="map" class="map"></div>

    <script src="app.js"></script>
    <script async defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
    </script>
  </body>
</html>

The initMap() function contains all of the functionality for making data requests and displaying the map. This function runs as soon as the main page has loaded. The map constant gets the name of the div in which to display the map, and specifies initial zoom level, the coordinates at which to center the map, and the style declaration to use:

const map = new google.maps.Map(document.querySelector('#map'), {
  zoom: 12,
  center: {
    // New York City
    lat: 40.7305,
    lng: -73.9091
  },
  styles: mapStyle
});

Load subway line data

subway-lines.geojson stores the data for subway lines. The following call in app.js makes a request for subway line data:

map.data.loadGeoJson('/data/subway-lines');

Calling loadGeoJson() invokes the subwayLinesHandler() function in nycsubway.go. Since there isn't too much data involved, the handler simply returns the entire data set:

func subwayLinesHandler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-type", "application/json")
  w.Write(GeoJSON["subway-lines.geojson"])
}

When the app receives line data, the setStyle() function in app.js applies the appropriate color for each stroke. Jump ahead to learn more about styling map elements.

Filtering and clustering

subway-stations.geojson stores the data for subway station locations. For maximum efficiency, the application filters subway station location data on the server side, returning only stations that appear within the map bounds at the current zoom level. The following idle callback function in app.js makes a request for data, scoped to the map bounds:

google.maps.event.addListener(map, 'idle', () => {
  const sw = map.getBounds().getSouthWest();
  const ne = map.getBounds().getNorthEast();
  const zm = map.getZoom();
  map.data.loadGeoJson(
    `/data/subway-stations?viewport=${sw.lat()},${sw.lng()}|${ne.lat()},${ne.lng()}&zoom=${zm}`,
    null,
    features => {
      stationDataFeatures.forEach(dataFeature => {
        map.data.remove(dataFeature);
      });
      stationDataFeatures = features;
    }
  );
});

Calling loadGeoJson() invokes the subwayStationsHandler() function in stations.go, which contains all of the functionality for clustering markers based on zoom level.

  • The Bounds() and newRect() functions construct an RTree (rtree.Rect).

    ...
    
    func (s *Station) Bounds() *rtree.Rect {
      return rtree.Point{
        s.feature.Geometry.Point[0],
        s.feature.Geometry.Point[1],
      }.ToRect(1e-6)
    }
    
    ...
    
    func newRect(vp string) (*rtree.Rect, error) {
      ss := strings.Split(vp, "|")
      sw := strings.Split(ss[0], ",")
      swLat, err := strconv.ParseFloat(sw[0], 64)
      if err != nil {
        return nil, err
      }
      swLng, err := strconv.ParseFloat(sw[1], 64)
      if err != nil {
        return nil, err
      }
      ne := strings.Split(ss[1], ",")
      neLat, err := strconv.ParseFloat(ne[0], 64)
      if err != nil {
        return nil, err
      }
      neLng, err := strconv.ParseFloat(ne[1], 64)
      if err != nil {
        return nil, err
      }
      minLat := math.Min(swLat, neLat)
      minLng := math.Min(swLng, neLng)
      distLat := math.Max(swLat, neLat) - minLat
      distLng := math.Max(swLng, neLng) - minLng
    
      // Grow the rect to ameliorate issues with stations
      // disappearing on Zoom in, and being slow to appear
      // on Pan or Zoom out.
      r, err := rtree.NewRect(
        rtree.Point{
          minLng - distLng/10,
          minLat - distLat/10,
        },
        []float64{
          distLng * 1.2,
          distLat * 1.2,
        })
      if err != nil {
        return nil, err
      }
      return r, nil
    }
    
  • The loadStations() function loads all of the GeoJSON features into the RTree, bounded by the coordinates passed in from loadGeoJson().

    func loadStations() {
      stationsGeojson := GeoJSON["subway-stations.geojson"]
      fc, err := geojson.UnmarshalFeatureCollection(stationsGeojson)
      if err != nil {
        // Note: this will take down the GAE instance by exiting this process.
        log.Fatal(err)
      }
      for _, f := range fc.Features {
        Stations.Insert(&Station{f})
      }
    }
    
  • The subwayStationsHandler() function returns the constrained GeoJSON data, applying marker clustering in the process.

    func subwayStationsHandler(w http.ResponseWriter, r *http.Request) {
      w.Header().Set("Content-type", "application/json")
      vp := r.FormValue("viewport")
      rect, err := newRect(vp)
      if err != nil {
        str := fmt.Sprintf("Couldn't parse viewport: %s", err)
        http.Error(w, str, 400)
        return
      }
      zm, err := strconv.ParseInt(r.FormValue("zoom"), 10, 0)
      if err != nil {
        str := fmt.Sprintf("Couldn't parse zoom: %s", err)
        http.Error(w, str, 400)
        return
      }
      s := Stations.SearchIntersect(rect)
      fc, err := clusterStations(s, int(zm))
      if err != nil {
        str := fmt.Sprintf("Couldn't cluster results: %s", err)
        http.Error(w, str, 500)
        return
      }
      err = json.NewEncoder(w).Encode(fc)
      if err != nil {
        str := fmt.Sprintf("Couldn't encode results: %s", err)
        http.Error(w, str, 500)
        return
      }
    }
    
  • The clusterStations() function in clusterer.go handles clustering markers based on zoom level (clusterer.go contains a number of other functions which are omitted for brevity.

    func clusterStations(spatials []rtree.Spatial, zoom int) (*geojson.FeatureCollection, error) {
      var pl cluster.PointList
    
      for _, spatial := range spatials {
        station := spatial.(*Station)
        pl = append(pl, station.Point())
      }
      clusteringRadius, minClusterSize := getClusteringRadiusAndMinClusterSize(zoom)
      // The following operation groups stations determined to be nearby into elements of
      // "clusters". Some stations may end up not part of any cluster ("noise") - we
      // present these as individual stations on the map.
      clusters, noise := cluster.DBScan(pl, clusteringRadius, minClusterSize)
      fc := geojson.NewFeatureCollection()
      for _, id := range noise {
        f := spatials[id].(*Station).feature
        name, err := f.PropertyString("name")
        if err != nil {
          return nil, err
        }
        notes, err := f.PropertyString("notes")
        if err != nil {
          return nil, err
        }
        f.SetProperty("title", fmt.Sprintf("%v Station", name))
        f.SetProperty("description", notes)
        f.SetProperty("type", "station")
        fc.AddFeature(f)
      }
      for _, clstr := range clusters {
        ctr, _, _ := clstr.CentroidAndBounds(pl)
        f := geojson.NewPointFeature([]float64{ctr[0], ctr[1]})
        n := len(clstr.Points)
        f.SetProperty("title", fmt.Sprintf("Station Cluster #%v", clstr.C+1))
        f.SetProperty("description", fmt.Sprintf("Contains %v stations", n))
        f.SetProperty("type", "cluster")
        fc.AddFeature(f)
      }
      return fc, nil
    }
    

Style map elements

Styling is an important aspect of any Google Maps app. You can use styling to emphasize the information you want to present while suppressing details that your users don't need, all while making your map look more beautiful.

Style the map and features

The mapStyle constant contains the JSON style declaration, which defines the styling options for the map. The style declaration sets the colors to use for each map feature, and also specifies whether various features will be visible on the map. The following example shows the JSON style declaration:

const mapStyle = [
  {
    elementType: 'geometry',
    stylers: [
      {
        color: '#eceff1'
      }
    ]
  },
  {
    elementType: 'labels',
    stylers: [
      {
        visibility: 'off'
      }
    ]
  },
  {
    featureType: 'administrative',
    elementType: 'labels',
    stylers: [
      {
        visibility: 'off'
      }
    ]
  },
  {
    featureType: 'road',
    elementType: 'geometry',
    stylers: [
      {
        color: '#cfd8dc'
      }
    ]
  },
  {
    featureType: 'road',
    elementType: 'geometry.stroke',
    stylers: [
      {
        visibility: 'off'
      }
    ]
  },
  {
    featureType: 'road.local',
    stylers: [
      {
        visibility: 'off'
      }
    ]
  },
  {
    featureType: 'water',
    stylers: [
      {
        color: '#b0bec5'
      }
    ]
  }
];

Style station and cluster markers

Subway station locations can be represented either as individual stations, or as a cluster of stations. The setStyle() function in app.js styles each marker based on its type (station or cluster), and uses SVG paths to draw the markers.

map.data.setStyle(feature => {
  const type = feature.getProperty('type');
  if (type === 'cluster') {
    // Icon path from: https://material.io/icons/#ic_add_circle_outline
    return {
      icon: {
        fillColor: '#00b0ff',
        strokeColor: '#3c8cb8',
        fillOpacity: 1.0,
        scale: 1.2,
        path: 'M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4' +
          '.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8' +
          's3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z'
      }
    };
  } else if (type === 'station') {
    // Icon path from: https://material.io/icons/#ic_train
    return {
      icon: {
        fillColor: '#00b0ff',
        strokeColor: '#3c8cb8',
        fillOpacity: 1.0,
        scale: 1.2,
        path: 'M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h2.2' +
          '3l2-2H14l2 2h2v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-3.58-' +
          '4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1' +
          '.5S8.33 17 7.5 17zm3.5-7H6V6h5v4zm2 0V6h5v4h-5zm3.5 7c-.83 0-1.5' +
          '-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z'
      }
    };
  }

Add color to the subway lines

The routeColors constant contains the colors to use for the subway lines, based on the New York City Subway nomenclature.

const routeColors = {
  // IND Eighth Avenue Line
  A: '#2850ad',
  C: '#2850ad',
  E: '#2850ad',

  // IND Sixth Avenue Line
  B: '#ff6319',
  D: '#ff6319',
  F: '#ff6319',
  M: '#ff6319',

  // IND Crosstown Line
  G: '#6cbe45',

  // BMT Canarsie Line
  L: '#a7a9ac',

  // BMT Nassau Street Line
  J: '#996633',
  Z: '#996633',

  // BMT Broadway Line
  N: '#fccc0a',
  Q: '#fccc0a',
  R: '#fccc0a',
  W: '#fccc0a',

  // IRT Broadway – Seventh Avenue Line
  '1': '#ee352e',
  '2': '#ee352e',
  '3': '#ee352e',

  // IRT Lexington Avenue Line
  '4': '#00933c',
  '5': '#00933c',
  '6': '#00933c',

  // IRT Flushing Line
  '7': '#b933ad',

  // Shuttles
  S: '#808183'
};

The routeSymbol constant gets the data for each subway line, and the final return statement in initMap() returns the strokeColor.

const routeSymbol = feature.getProperty('rt_symbol');
return {
  strokeColor: routeColors[routeSymbol]
};

Create info windows

An info window is a pop up window that the app uses to display information about each subway station when a user clicks a marker. The info window shows the title and description for the station selected by the user.

map.data.addListener('click', ev => {
  const f = ev.feature;
  const title = f.getProperty('title');
  const description = f.getProperty('description');

  if (!description) {
    return;
  }

  infowindow.setContent(`<b>${title}</b><br/> ${description}`);
  // Hat tip geocodezip: http://stackoverflow.com/questions/23814197
  infowindow.setPosition(f.getGeometry().get());
  infowindow.setOptions({
    pixelOffset: new google.maps.Size(0, -30)
  });
  infowindow.open(map);
});

Send feedback about...

Store Locator Solution
Store Locator Solution
Need help? Visit our support page.