Skip to content

ptv-logistics/tutorials-displaying-custom-road-attributes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tutorial: Displaying Custom Road Attributes on a PTV Raster Map with Leaflet

This tutorial walks you through building a web app that displays a PTV Raster Map and overlays custom road attribute scenarios fetched from the PTV Developer Data API. Replace YOUR_API_KEY with your actual PTV Developer key, open index.html in a browser, and the app will load your custom road attribute scenarios on the map automatically.

Prerequisites

  • A PTV Developer API key (get one at developer.myptv.com)
  • Basic knowledge of HTML, CSS, and JavaScript
  • At least one custom road attribute scenario created in your PTV Developer account

Project Structure

index.html   — Page layout with map container and side panel
style.css    — Styling for the map, panel, and tooltips
app.js       — Map initialization, API calls, and rendering logic

Step 1: Set Up the HTML

Create index.html with a full-page map container, a side panel for listing scenarios, and the Leaflet library loaded from CDN.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PTV Raster Map</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="map"></div>
  <div id="panel">
    <h3>Custom Road Attributes</h3>
    <button id="load-btn">Load Scenarios</button>
    <div id="scenarios-list"></div>
  </div>
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <script src="app.js"></script>
</body>
</html>

Step 2: Create a Custom Tile Layer for PTV Raster Maps

The PTV Raster Maps API requires authentication via an apiKey header. Since Leaflet's default TileLayer only supports URL-based tokens, we extend it with a custom createTile method that uses fetch with the required header.

const API_KEY = 'YOUR_API_KEY';

const PtvTileLayer = L.TileLayer.extend({
  createTile(coords, done) {
    const tile = document.createElement('img');
    tile.alt = '';
    tile.setAttribute('role', 'presentation');

    const url = this.getTileUrl(coords);

    fetch(url, {
      headers: { 'apiKey': API_KEY }
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`Tile fetch failed: ${response.status}`);
        }
        return response.blob();
      })
      .then(blob => {
        tile.src = URL.createObjectURL(blob);
        done(null, tile);
      })
      .catch(err => {
        done(err, tile);
      });

    return tile;
  }
});

Initialize the map and add the tile layer:

const map = L.map('map').setView([49.006889, 8.403653], 10);

const ptvLayer = new PtvTileLayer(
  'https://api.myptv.com/rastermaps/v1/image-tiles/{z}/{x}/{y}', {
    maxZoom: 22,
    tileSize: 256,
    attribution: '&copy; <a href="https://www.myptv.com">PTV Logistics</a>'
  }
);
ptvLayer.addTo(map);

Step 3: Fetch Custom Road Attribute Scenarios

Call the PTV Data API's GET /road-attributes endpoint with results=POLYLINES and polylineFormat=GEO_JSON to get scenario data including road geometries.

const DATA_API_BASE = 'https://api.myptv.com/data/v1';

async function fetchCustomRoadAttributeScenarios() {
  const url = `${DATA_API_BASE}/road-attributes?results=POLYLINES&polylineFormat=GEO_JSON`;

  const response = await fetch(url, {
    headers: { 'apiKey': API_KEY }
  });

  if (!response.ok) {
    const errorBody = await response.json().catch(() => null);
    throw new Error(errorBody?.description || `HTTP ${response.status}`);
  }

  const data = await response.json();
  return data.customRoadAttributeScenarios || [];
}

Step 4: Display Scenarios on the Map

Each scenario contains roadsToBeAttributed, which includes polylines in GeoJSON format. Parse them and add to the map using L.geoJSON. Use onEachFeature to attach a tooltip showing the road attributes on hover.

function addScenarioToMap(scenario, color) {
  const layerGroup = L.layerGroup();

  if (scenario.roadsToBeAttributed) {
    scenario.roadsToBeAttributed.forEach(road => {
      if (!road.polylines) {
        return;
      }

      road.polylines.forEach(polylineGeoJson => {
        const geojson = JSON.parse(polylineGeoJson);
        const layer = L.geoJSON(geojson, {
          style: { color: color, weight: 4, opacity: 0.8 },
          onEachFeature(_feature, featureLayer) {
            const attrParts = formatAttributes(road.attributes);
            let tooltipContent = `<strong>${scenario.name}</strong>`;
            if (attrParts.length) {
              tooltipContent += `<br>${attrParts.join(', ')}`;
            }

            featureLayer.bindTooltip(tooltipContent, { sticky: true });

            // Highlight on hover
            featureLayer.on('mouseover', () => {
              featureLayer.setStyle({ weight: 7, opacity: 1 });
            });
            featureLayer.on('mouseout', () => {
              featureLayer.setStyle({ weight: 4, opacity: 0.8 });
            });
          }
        });
        layerGroup.addLayer(layer);
      });
    });
  }

  layerGroup.addTo(map);
  return layerGroup;
}

Step 5: Format Attributes Generically

Rather than hard-coding each attribute name, iterate over all keys so the app automatically picks up any new attributes the API may add in the future.

function formatAttributes(attributes) {
  if (!attributes) {
    return [];
  }
  const parts = [];

  Object.entries(attributes).forEach(([key, value]) => {
    if (value === null || value === undefined) {
      return;
    }
    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());

    if (typeof value === 'boolean') {
      if (value) {
        parts.push(label);
      }
    } else {
      parts.push(`${label}: ${value}`);
    }
  });

  return parts;
}

Step 6: Generate Unique Colors

To ensure each scenario is visually distinct regardless of how many exist, generate colors by spacing hues evenly around the HSL color wheel.

function getScenarioColor(index, total) {
  const hue = Math.round((360 / total) * index);
  return `hsl(${hue}, 70%, 50%)`;
}

Step 7: Build the Scenario List Panel

Render each scenario in the side panel with a visibility toggle and a zoom button.

function renderScenarioList(scenarios) {
  const listEl = document.getElementById('scenarios-list');

  if (scenarios.length === 0) {
    listEl.innerHTML = '<p class="info-msg">No custom road attribute scenarios found.</p>';
    return;
  }

  listEl.innerHTML = '';

  scenarios.forEach((scenario, index) => {
    const color = getScenarioColor(index, scenarios.length);
    const layer = addScenarioToMap(scenario, color);
    scenarioLayers[scenario.id] = layer;

    const item = document.createElement('div');
    item.className = 'scenario-item';
    item.style.borderLeftColor = color;
    item.style.borderLeftWidth = '4px';

    const name = scenario.name || 'Unnamed Scenario';
    const id = `scenario-${index}`;

    let html = `
      <div class="scenario-header">
        <label>
          <input type="checkbox" id="${id}" checked>
          <span style="color:${color}">&#9632;</span> ${escapeHtml(name)}
        </label>
        <button class="zoom-btn" data-scenario="${index}" title="Zoom to scenario">&#8982;</button>
      </div>
    `;

    if (scenario.description) {
      html += `<div class="description">${escapeHtml(scenario.description)}</div>`;
    }

    item.innerHTML = html;
    listEl.appendChild(item);

    // Toggle visibility
    item.querySelector(`#${id}`).addEventListener('change', (e) => {
      if (e.target.checked) {
        layer.addTo(map);
      } else {
        map.removeLayer(layer);
      }
    });

    // Zoom to scenario bounds
    item.querySelector('.zoom-btn').addEventListener('click', () => {
      const bounds = L.latLngBounds([]);
      layer.eachLayer(l => {
        if (l.getBounds) {
          bounds.extend(l.getBounds());
        }
      });
      if (bounds.isValid()) {
        map.fitBounds(bounds, { padding: [40, 40] });
      }
    });
  });

  // Fit map to all scenario bounds
  const allBounds = L.latLngBounds([]);
  Object.values(scenarioLayers).forEach(lg => {
    lg.eachLayer(l => {
      if (l.getBounds) {
        allBounds.extend(l.getBounds());
      }
    });
  });
  if (allBounds.isValid()) {
    map.fitBounds(allBounds, { padding: [40, 40] });
  }
}

About

Build a web app that displays a PTV Raster Map and overlays custom road attribute scenarios

Topics

Resources

License

Stars

Watchers

Forks

Contributors