{"id":28633575,"url":"https://github.com/ptv-logistics/tutorials-geocoding-scenario","last_synced_at":"2025-07-06T01:35:36.652Z","repository":{"id":297439185,"uuid":"829927855","full_name":"ptv-logistics/tutorials-geocoding-scenario","owner":"ptv-logistics","description":"Build an application to identify whether a geocoded address is within a custom road attributes area.","archived":false,"fork":false,"pushed_at":"2025-06-05T12:34:19.000Z","size":17,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-12T15:09:04.036Z","etag":null,"topics":["ptv-developer","tutorials"],"latest_commit_sha":null,"homepage":"https://developer.myptv.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ptv-logistics.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-07-17T09:12:59.000Z","updated_at":"2025-06-05T12:34:20.000Z","dependencies_parsed_at":"2025-06-05T13:34:14.600Z","dependency_job_id":"3d5efd78-d4c4-40d9-b1cc-79cfad4dccea","html_url":"https://github.com/ptv-logistics/tutorials-geocoding-scenario","commit_stats":null,"previous_names":["ptv-logistics/tutorials-geocoding-scenario"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ptv-logistics/tutorials-geocoding-scenario","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ptv-logistics%2Ftutorials-geocoding-scenario","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ptv-logistics%2Ftutorials-geocoding-scenario/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ptv-logistics%2Ftutorials-geocoding-scenario/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ptv-logistics%2Ftutorials-geocoding-scenario/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ptv-logistics","download_url":"https://codeload.github.com/ptv-logistics/tutorials-geocoding-scenario/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ptv-logistics%2Ftutorials-geocoding-scenario/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263836647,"owners_count":23517918,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ptv-developer","tutorials"],"created_at":"2025-06-12T15:09:03.800Z","updated_at":"2025-07-06T01:35:36.267Z","avatar_url":"https://github.com/ptv-logistics.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Getting started\n\nUse the [vitejs](https://vitejs.dev/guide/) template with React and Typescript preset.\n\n```\nnpm create vite@latest\ncreate-vite@5.2.3\nOk to proceed? (y) y\n√ Project name: ... geocoding-scenario\n√ Select a framework: » React\n√ Select a variant: » TypeScript\ncd geocoding-scenario\nnpm install\nnpm run dev\n```\n\nInstall the mapbox-gl and react-map-gl to display a map. Use the 1.13.0 version to avoid the api key usage.\n\n```\nnpm install mapbox-gl@1.13.0\nnpm install react-map-gl\n```\n\nUse the [material-ui](https://mui.com/material-ui/)\n\n```\nnpm install @mui/material @emotion/react @emotion/styled\n```\n\nAdd a `.env` at root with your PTV API Key.\n\n```\nVITE_API_KEY=\u003cyour_api_key\u003e\n```\n\n# Basic map\n\nCreate a `MyMap` component to display a basic map with PTV tiles\n\n```tsx\nimport { Box } from \"@mui/material\";\nimport \"mapbox-gl/dist/mapbox-gl.css\";\nimport { useCallback } from \"react\";\nimport Map from \"react-map-gl\";\nimport { useMapStyle } from \"./hooks/useMapStyle\";\n\nconst boxStyle = {\n  gridArea: \"map\",\n  height: \"100%\",\n  width: \"100%\",\n  zIndex: 0,\n};\n\nconst mapBoxStyle = {\n  height: \"100%\",\n  width: \"100%\",\n};\n\nconst initialViewState = {\n  longitude: -122.4,\n  latitude: 37.8,\n  zoom: 14,\n};\n\nexport function MyMap() {\n  const mapStyle = useMapStyle();\n\n  const getTransformRequest = useCallback((url: string) =\u003e {\n    if (import.meta.env.VITE_API_KEY) {\n      return { url: url + \"?apiKey=\" + import.meta.env.VITE_API_KEY };\n    }\n    return { url: url, headers: {} };\n  }, []);\n\n  return (\n    \u003cBox sx={boxStyle}\u003e\n      \u003cMap\n        initialViewState={initialViewState}\n        style={mapBoxStyle}\n        mapStyle={mapStyle}\n        transformRequest={getTransformRequest}\n      /\u003e\n    \u003c/Box\u003e\n  );\n}\n```\n\nCreate a custom hook to handle the map style in `src/hooks/useMapStyle.ts`\n\n```tsx\nimport { useEffect, useState } from \"react\";\n\nexport const initialMapStyle: mapboxgl.Style = {\n  version: 8,\n  name: \"initial\",\n  pitch: 0,\n  sources: {\n    ptv: {\n      type: \"vector\",\n      tiles: [\"https://api.myptv.com/maps-osm/v1/vector-tiles/{z}/{x}/{y}\"],\n      minzoom: 0,\n      maxzoom: 17,\n    },\n  },\n  layers: [],\n  sprite: \"https://vectormaps-resources.myptv.com/icons/latest/sprite\",\n  glyphs:\n    \"https://vectormaps-resources.myptv.com/fonts/latest/{fontstack}/{range}.pbf\",\n};\n\nexport const useMapStyle = () =\u003e {\n  const [data, setData] = useState\u003cmapboxgl.Style\u003e(initialMapStyle);\n\n  useEffect(() =\u003e {\n    const fetchData = async () =\u003e {\n      try {\n        const response = await fetch(\n          \"https://vectormaps-resources.myptv.com/styles/latest/standard.json\"\n        );\n        const json = await response.json();\n        setData(json);\n      } catch (error) {\n        console.log(error);\n      }\n    };\n\n    fetchData();\n  }, []);\n\n  return data;\n};\n```\n\n# Geocoding\n\nTo geocode addresses, use the [clients-geocoding-api](https://github.com/ptv-logistics/clients-geocoding-api)\n\n```\n git clone https://github.com/ptv-logistics/clients-geocoding-api\n```\n\nAnd use it in a component like `MyGeocoder`\n\n```tsx\nimport { Box, Button, Stack, TextField } from \"@mui/material\";\nimport { Dispatch, SetStateAction, useState } from \"react\";\nimport {\n  Configuration,\n  PlacesApi,\n  PlacesSearchResult,\n} from \"./clients-geocoding-api/typescript\";\n\nconst style = {\n  bgcolor: \"background.paper\",\n  boxShadow: 2,\n  borderRadius: 2,\n  p: 1,\n  position: \"absolute\",\n  top: \"12px\",\n  left: \"12px\",\n};\n\nasync function geocode(search: string) {\n  const client = new PlacesApi(\n    new Configuration({\n      apiKey: import.meta.env.VITE_API_KEY,\n    })\n  );\n  try {\n    const response = await client.searchPlacesByText({\n      searchText: search,\n      language: \"en\",\n    });\n    return response;\n  } catch (error) {\n    console.log(error);\n    throw error;\n  }\n}\n\nexport function MyGeocoder(props: {\n  response: PlacesSearchResult | null;\n  setResponse: Dispatch\u003cSetStateAction\u003cPlacesSearchResult | null\u003e\u003e;\n}) {\n  const [searchText, setSearchText] = useState(\"\");\n\n  return (\n    \u003cBox sx={style}\u003e\n      \u003cStack direction=\"row\" gap={1} sx={{ m: 1 }}\u003e\n        \u003cTextField\n          label=\"Search places by text\"\n          variant=\"outlined\"\n          fullWidth\n          value={searchText}\n          onChange={(e) =\u003e setSearchText(e.target.value)}\n          onKeyDown={async (e) =\u003e {\n            if (e.key === \"Enter\") {\n              e.preventDefault();\n              const result = await geocode(searchText);\n              props.setResponse(result);\n            }\n          }}\n        /\u003e\n        \u003cButton\n          type=\"submit\"\n          variant=\"contained\"\n          onClick={async () =\u003e {\n            const result = await geocode(searchText);\n            props.setResponse(result);\n          }}\n        \u003e\n          Search\n        \u003c/Button\u003e\n      \u003c/Stack\u003e\n    \u003c/Box\u003e\n  );\n}\n```\n\nAdd `MyGeocoder` in parent component\n\n```tsx\nimport { useState } from \"react\";\nimport { MyGeocoder } from \"./MyGeocoder\";\nimport { MyMap } from \"./MyMap\";\nimport { PlacesSearchResult } from \"./clients-geocoding-api/typescript\";\n\nfunction App() {\n  const [response, setResponse] = useState\u003cPlacesSearchResult | null\u003e(null);\n  return (\n    \u003c\u003e\n      \u003cMyMap /\u003e\n      \u003cMyGeocoder response={response} setResponse={setResponse} /\u003e\n    \u003c/\u003e\n  );\n}\n\nexport default App;\n```\n\n# Custom Road Attributes\n\nUse The custom road attributes client to retrieve your scenario inside a custom hook.\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport {\n  Configuration,\n  CustomRoadAttributeScenario,\n  CustomRoadAttributesApi,\n} from \"../clients-data-api/typescript\";\n\nconst client = new CustomRoadAttributesApi(\n  new Configuration({\n    apiKey: import.meta.env.VITE_API_KEY,\n  })\n);\n\nconst useScenario = (id: string) =\u003e {\n  const [scenario, setScenario] = useState\u003cCustomRoadAttributeScenario | null\u003e(\n    null\n  );\n\n  useEffect(() =\u003e {\n    const fetchScenario = async () =\u003e {\n      try {\n        const response = await client.getCustomRoadAttributeScenario({\n          scenarioId: id,\n        });\n        setScenario(response);\n      } catch (error) {\n        console.log(error);\n      }\n    };\n\n    fetchScenario();\n  }, [id]);\n\n  return scenario;\n};\n\nexport default useScenario;\n```\n\nUse this custom hook in `App`\n\n```tsx\nimport { useState } from \"react\";\nimport { MyGeocoder } from \"./MyGeocoder\";\nimport { MyMap } from \"./MyMap\";\nimport { PlacesSearchResult } from \"./clients-geocoding-api/typescript\";\nimport useScenario from \"./hooks/useScenario\";\n\nfunction App() {\n  const scenario = useScenario(\"aecfe795-ba61-4c19-8c3d-e97972f11f13\");\n  const [response, setResponse] = useState\u003cPlacesSearchResult | null\u003e(null);\n  return (\n    \u003c\u003e\n      {scenario \u0026\u0026 \u003cMyMap scenario={scenario} /\u003e}\n      \u003cMyGeocoder response={response} setResponse={setResponse} /\u003e\n    \u003c/\u003e\n  );\n}\n\nexport default App;\n```\n\nModify `MyMap` to display scenario polylines. Install `\"@mapbox/polyline\"` to convert google encoded polyline to geojson and `@turf/helpers` to build geojson from string.\n\n```\nnpm install @mapbox/polyline\nnpm install @turf/helpers\n```\n\n```tsx\nimport polylineDecoder from \"@mapbox/polyline\";\nimport { Box } from \"@mui/material\";\nimport { featureCollection, geometry, geometryCollection } from \"@turf/helpers\";\nimport \"mapbox-gl/dist/mapbox-gl.css\";\nimport { useCallback, useMemo } from \"react\";\nimport Map, { Layer, Source } from \"react-map-gl\";\nimport { CustomRoadAttributeScenario } from \"./clients-data-api/typescript\";\nimport { useMapStyle } from \"./hooks/useMapStyle\";\n\nconst boxStyle = {\n  gridArea: \"map\",\n  height: \"100%\",\n  width: \"100%\",\n  zIndex: 0,\n};\n\nconst mapBoxStyle = {\n  height: \"100%\",\n  width: \"100%\",\n};\n\nconst initialViewState = {\n  longitude: 2.3333,\n  latitude: 48.86666,\n  zoom: 14,\n};\n\nfunction buildFeatureCollection(scenario: CustomRoadAttributeScenario) {\n  return featureCollection(\n    scenario.roadsToBeAttributed.map((roads) =\u003e\n      geometryCollection(\n        roads.polylines!.map((p) =\u003e\n          geometry(\n            \"LineString\",\n            polylineDecoder.decode(p).map((coords) =\u003e [coords[1], coords[0]])\n          )\n        ),\n        {\n          ...roads.attributes,\n        }\n      )\n    ),\n    {\n      id: scenario.id,\n    }\n  );\n}\n\nexport function MyMap(props: { scenario: CustomRoadAttributeScenario | null }) {\n  const mapStyle = useMapStyle();\n\n  const collection = useMemo(\n    () =\u003e (props.scenario ? buildFeatureCollection(props.scenario) : null),\n    [props.scenario]\n  );\n  const getTransformRequest = useCallback((url: string) =\u003e {\n    if (import.meta.env.VITE_API_KEY) {\n      return { url: url + \"?apiKey=\" + import.meta.env.VITE_API_KEY };\n    }\n    return { url: url, headers: {} };\n  }, []);\n\n  return (\n    \u003cBox sx={boxStyle}\u003e\n      \u003cMap\n        initialViewState={initialViewState}\n        style={mapBoxStyle}\n        mapStyle={mapStyle}\n        transformRequest={getTransformRequest}\n      \u003e\n        {collection \u0026\u0026 (\n          \u003cSource type=\"geojson\" data={collection}\u003e\n            \u003cLayer\n              type=\"line\"\n              paint={{ \"line-color\": \"#3f50b5\", \"line-width\": 3 }}\n            /\u003e\n          \u003c/Source\u003e\n        )}\n      \u003c/Map\u003e\n    \u003c/Box\u003e\n  );\n}\n```\n\nTo dermine wether a geocoded address is included in one of the roads contained in the scenario, use `booleanPointInPolygon` and `booleanPointOnLine` from `@turf`\n\n```\nnpm install @turf/boolean-point-in-polygon\nnpm install @turf/boolean-point-on-line\n```\n\n```ts\nfunction placeInRoads(roads: RoadsToBeAttributed, place: Place) {\n  const p = point([\n    place.roadAccessPosition?.longitude || place.referencePosition.longitude,\n    place.roadAccessPosition?.latitude || place.referencePosition.latitude,\n  ]);\n\n  const coordinatesString = roads.points.split(\",\");\n  const coordinates = coordinatesString.map((v) =\u003e JSON.parse(v) as number);\n  const formattedCoordinates = new Array\u003cPosition\u003e();\n  while (coordinates.length \u003e 1) {\n    const lat = coordinates.shift();\n    const lon = coordinates.shift();\n    if (lat \u0026\u0026 lon) {\n      formattedCoordinates.push([lon, lat]);\n    }\n  }\n  const firstCoordinate = formattedCoordinates[0];\n  if (formattedCoordinates.length \u003c 2)\n    return (\n      firstCoordinate[0].toFixed(3) === p.geometry.coordinates[0].toFixed(3) \u0026\u0026\n      firstCoordinate[1].toFixed(3) === p.geometry.coordinates[1].toFixed(3)\n    );\n  else if (formattedCoordinates.length \u003e 2) {\n    const plg = polygon([[...formattedCoordinates, firstCoordinate]]);\n    return booleanPointInPolygon(p, plg);\n  } else {\n    const lnstrg = lineString(formattedCoordinates);\n    return booleanPointOnLine(p, lnstrg);\n  }\n}\n```\n\nFinally, Display geocoded places in green if there are included in one of scenario's geometries, red otherwise.\n\n```tsx\nimport polylineDecoder from \"@mapbox/polyline\";\nimport { Box } from \"@mui/material\";\nimport bbox from \"@turf/bbox\";\nimport booleanPointInPolygon from \"@turf/boolean-point-in-polygon\";\nimport booleanPointOnLine from \"@turf/boolean-point-on-line\";\nimport {\n  featureCollection,\n  geometry,\n  geometryCollection,\n  lineString,\n  point,\n  points,\n  polygon,\n} from \"@turf/helpers\";\nimport \"mapbox-gl/dist/mapbox-gl.css\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport Map, { Layer, MapRef, Marker, Source } from \"react-map-gl\";\nimport {\n  CustomRoadAttributeScenario,\n  RoadsToBeAttributed,\n} from \"./clients-data-api/typescript\";\nimport { Place } from \"./clients-geocoding-api/typescript\";\nimport { useMapStyle } from \"./hooks/useMapStyle\";\n\ntype Position = number[];\n\nconst boxStyle = {\n  gridArea: \"map\",\n  height: \"100%\",\n  width: \"100%\",\n  zIndex: 0,\n};\n\nconst mapBoxStyle = {\n  height: \"100%\",\n  width: \"100%\",\n};\n\nconst initialViewState = {\n  longitude: 2.3333,\n  latitude: 48.86666,\n  zoom: 14,\n};\n\nfunction placeInRoads(roads: RoadsToBeAttributed, place: Place) {\n  const p = point([\n    place.roadAccessPosition?.longitude || place.referencePosition.longitude,\n    place.roadAccessPosition?.latitude || place.referencePosition.latitude,\n  ]);\n\n  const coordinatesString = roads.points.split(\",\");\n  const coordinates = coordinatesString.map((v) =\u003e JSON.parse(v) as number);\n  const formattedCoordinates = new Array\u003cPosition\u003e();\n  while (coordinates.length \u003e 1) {\n    const lat = coordinates.shift();\n    const lon = coordinates.shift();\n    if (lat \u0026\u0026 lon) {\n      formattedCoordinates.push([lon, lat]);\n    }\n  }\n  const firstCoordinate = formattedCoordinates[0];\n  if (formattedCoordinates.length \u003c 2)\n    return (\n      firstCoordinate[0].toFixed(3) === p.geometry.coordinates[0].toFixed(3) \u0026\u0026\n      firstCoordinate[1].toFixed(3) === p.geometry.coordinates[1].toFixed(3)\n    );\n  else if (formattedCoordinates.length \u003e 2) {\n    const plg = polygon([[...formattedCoordinates, firstCoordinate]]);\n    return booleanPointInPolygon(p, plg);\n  } else {\n    const lnstrg = lineString(formattedCoordinates);\n    return booleanPointOnLine(p, lnstrg);\n  }\n}\n\nfunction buildFeatureCollection(scenario: CustomRoadAttributeScenario) {\n  return featureCollection(\n    scenario.roadsToBeAttributed.map((roads) =\u003e\n      geometryCollection(\n        roads.polylines!.map((p) =\u003e\n          geometry(\n            \"LineString\",\n            polylineDecoder.decode(p).map((coords) =\u003e [coords[1], coords[0]])\n          )\n        ),\n        {\n          ...roads.attributes,\n        }\n      )\n    ),\n    {\n      id: scenario.id,\n    }\n  );\n}\n\nexport function MyMap(props: {\n  scenario: CustomRoadAttributeScenario | null;\n  places: Place[];\n}) {\n  const mapRef = useRef\u003cMapRef\u003e(null);\n  const mapStyle = useMapStyle();\n\n  const collection = useMemo(\n    () =\u003e (props.scenario ? buildFeatureCollection(props.scenario) : null),\n    [props.scenario]\n  );\n\n  const waypoints = useMemo(\n    () =\u003e\n      props.places.map((p, i) =\u003e (\n        \u003cMarker\n          key={i}\n          latitude={\n            p.roadAccessPosition?.latitude || p.referencePosition.latitude\n          }\n          longitude={\n            p.roadAccessPosition?.longitude || p.referencePosition.longitude\n          }\n          color={\n            props.scenario?.roadsToBeAttributed.reduce(\n              (prev, curr) =\u003e (placeInRoads(curr, p) ? true : prev),\n              false\n            )\n              ? \"green\"\n              : \"red\"\n          }\n        /\u003e\n      )),\n    [props.places, props.scenario]\n  );\n\n  useEffect(() =\u003e {\n    if (props.places.length \u003e 0) {\n      const [minLng, minLat, maxLng, maxLat] = bbox(\n        points(\n          props.places.map((p) =\u003e [\n            p.referencePosition.longitude,\n            p.referencePosition.latitude,\n          ])\n        )\n      );\n\n      mapRef.current?.fitBounds(\n        [\n          [minLng, minLat],\n          [maxLng, maxLat],\n        ],\n        { padding: 40, duration: 1000 }\n      );\n    }\n  });\n\n  const getTransformRequest = useCallback((url: string) =\u003e {\n    if (import.meta.env.VITE_API_KEY) {\n      return { url: url + \"?apiKey=\" + import.meta.env.VITE_API_KEY };\n    }\n    return { url: url, headers: {} };\n  }, []);\n\n  return (\n    \u003cBox sx={boxStyle}\u003e\n      \u003cMap\n        ref={mapRef}\n        initialViewState={initialViewState}\n        style={mapBoxStyle}\n        mapStyle={mapStyle}\n        transformRequest={getTransformRequest}\n      \u003e\n        {collection \u0026\u0026 (\n          \u003cSource type=\"geojson\" data={collection}\u003e\n            \u003cLayer\n              type=\"line\"\n              paint={{ \"line-color\": \"#3f50b5\", \"line-width\": 3 }}\n            /\u003e\n          \u003c/Source\u003e\n        )}\n        {waypoints}\n      \u003c/Map\u003e\n    \u003c/Box\u003e\n  );\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fptv-logistics%2Ftutorials-geocoding-scenario","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fptv-logistics%2Ftutorials-geocoding-scenario","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fptv-logistics%2Ftutorials-geocoding-scenario/lists"}