diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml new file mode 100644 index 0000000..71ec794 --- /dev/null +++ b/.github/workflows/ant.yml @@ -0,0 +1,55 @@ +name: Java CI + +on: + push: + branches: + - master + - $default-branch + - $protected-branches + pull_request: + branches: + - master + - $default-branch + workflow_dispatch: + +permissions: + id-token: write + attestations: write + contents: write + packages: write + +jobs: + call-workflow: + strategy: + matrix: + josm-revision: ["", "r19067"] + uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v3 + with: + java-version: 22 + josm-revision: ${{ matrix.josm-revision }} + perform-revision-tagging: ${{ matrix.josm-revision == 'r19067' && github.repository == 'JOSM/routing2' && github.ref_type == 'branch' && github.ref_name == 'master' && github.event_name != 'schedule' && github.event_name != 'pull_request' }} + secrets: inherit + build-valhalla: + uses: ./.github/workflows/valhalla.yaml + upload-valhalla: + runs-on: ubuntu-latest + needs: [build-valhalla, call-workflow] + if: needs.call-workflow.outputs.tag + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Get macos build + uses: actions/download-artifact@v4 + with: + name: macOS-build-valhalla-fat + - run: gh release upload --repo ${{ github.repository }} ${{ needs.call-workflow.outputs.tag }} valhalla-*-Darwin.tar.gz + - name: Get linux build + uses: actions/download-artifact@v4 + with: + name: Linux-build-valhalla-X64 + - run: gh release upload --repo ${{ github.repository }} ${{ needs.call-workflow.outputs.tag }} valhalla-*-Linux.tar.gz + - name: Get Windows build + uses: actions/download-artifact@v4 + with: + name: Windows-build-valhalla-X64 + - run: gh release upload --repo ${{ github.repository }} ${{ needs.call-workflow.outputs.tag }} valhalla-*-Windows.tar.gz diff --git a/.github/workflows/valhalla.yaml b/.github/workflows/valhalla.yaml new file mode 100644 index 0000000..3bf52de --- /dev/null +++ b/.github/workflows/valhalla.yaml @@ -0,0 +1,208 @@ +on: + workflow_call: + inputs: + valhalla_ref: + type: string + default: 3.5.1 + description: The valhalla version to build + required: false + vcpkg_ref: + type: string + default: 2024.10.21 + description: The vcpkg version to use + required: false + +jobs: + build_unix: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, macos-13, ubuntu-22.04, windows-2019] + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.8' + - uses: actions/checkout@v4 + with: + repository: valhalla/valhalla + ref: ${{ inputs.valhalla_ref }} + submodules: 'recursive' + - uses: actions/checkout@v4 + with: + repository: microsoft/vcpkg + ref: 2024.10.21 + path: vcpkg + submodules: 'recursive' + - name: Cache dependencies + id: dependency-cache + uses: actions/cache@v4 + with: + path: vcpkg/downloads + key: valhalla-${{ inputs.valhalla_ref }}-${{ inputs.vcpkg_ref }}-vcpkg-downloads-${{ runner.os }}-${{ runner.arch }} + - name: Cache build + id: build-cache + uses: actions/cache@v4 + with: + path: build/valhalla-${{ inputs.valhalla_ref }}-${{ runner.os == 'macos' && 'Darwin' || runner.os }}.tar.gz + key: ${{ runner.os }}-${{ runner.arch }}-valhalla-${{ inputs.valhalla_ref }} + - name: Build valhalla + if: steps.build-cache.outputs.cache-hit != 'true' + shell: bash + run: | + set -ex + if [ "${{ runner.os }}" == "macOS" ]; then + brew install automake cmake bash coreutils binutils libtool autoconf automake autoconf-archive pkg-config autoconf + os="osx" + elif [ "${{ runner.os }}" == "Linux" ]; then + apt-get update && apt-get install -y curl zip unzip tar npm pkg-config autoconf libtool python3 cmake git build-essential gcc g++ make + os="linux" + elif [ "${{ runner.os }}" == "Windows" ]; then + os="windows-static" + cat < 1.patch + diff --git a/scripts/valhalla_build_extract b/scripts/valhalla_build_extract + index 236822513..0ccafabf6 100755 + --- a/scripts/valhalla_build_extract + +++ b/scripts/valhalla_build_extract + @@ -95,7 +95,7 @@ class TileResolver: + tar.addfile(tar_member, self._tar_obj.extractfile(tar_member.name)) + else: + tar.add(str(self.path.joinpath(t)), arcname=t) + - tar_member = tar.getmember(str(t)) + + #tar_member = tar.getmember(str(t)) + + + description = "Builds a tar extract from the tiles in mjolnir.tile_dir to the path specified in mjolnir.tile_extract." + EOF + git apply 1.patch + fi + export VCPKG_ROOT=$(pwd)/vcpkg + ./vcpkg/bootstrap-vcpkg.sh + if [ "${{ runner.arch }}" == "X64" ]; then + echo "set(VCPKG_BUILD_TYPE release)" >> vcpkg/triplets/x64-${os}.cmake + else + if [ "${{ runner.os }}" == "macOS" ]; then brew install vcpkg; fi + echo "set(VCPKG_BUILD_TYPE release)" >> vcpkg/triplets/arm64-${os}.cmake + fi + npm install --ignore-scripts + mkdir build + # We don't need python bindings + sed -i.bak '/pybind11/d' vcpkg.json + # We don't need gdal (we disable compile-time support for it) + sed -i.bak '/gdal/d' vcpkg.json + export CMAKE_MAKE_PROGRAM=make && export CMAKE_CXX_COMPILER=g++ && export CMAKE_C_COMPILER=gcc + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake -DDENABLE_STATIC_LIBRARY_MODULES=ON -DBUILD_SHARED_LIBS=OFF -DDENABLE_GDAL=OFF -DENABLE_SERVICES=OFF -DENABLE_SINGLE_FILES_WERROR=OFF + if [ "${{ runner.os }}" == "Windows" ]; then cmake --build build --config Release -- //clp:ErrorsOnly //p:BuildInParallel=true //m:8 + elif [ "${{ runner.os }}" == "macOS" ]; then cmake --build build -- -j$(sysctl -n hw.physicalcpu) + elif [ "${{ runner.os }}" == "Linux" ]; then cmake --build build -- -j"$(nproc)" + fi + if [ "${{ runner.os }}" == "Windows" ]; then + ls build + ls build/Release + pip install -U pyinstaller + # Create static python files + $(cd build && pyinstaller valhalla_build_config && pyinstaller valhalla_build_extract) + ls build/dist/valhalla_build_config + cp build/dist/valhalla_build_config/valhalla_build_config.exe build/Release + cp build/dist/valhalla_build_extract/valhalla_build_extract.exe build/Release + cp build/valhalla_build_timezones build/Release + cp -r build/dist/valhalla_build_config/_internal build/Release + cp -r build/dist/valhalla_build_extract/_internal build/Release + tar -cavf build/valhalla-${{ inputs.valhalla_ref }}-Windows.tar.gz -C build/Release . + else sudo cmake --build build -- package + fi + if [ "${{ runner.os }}" == "Linux" ]; then + # Strip first path component. + mkdir tmp + tar -xf build/valhalla-${{ inputs.valhalla_ref }}-Linux.tar.gz -C tmp + rm build/valhalla-${{ inputs.valhalla_ref }}-Linux.tar.gz + tar -cavf build/valhalla-${{ inputs.valhalla_ref }}-Linux.tar.gz -C tmp/valhalla-${{ inputs.valhalla_ref }}-Linux . + fi + ls build + - name: Debug output + if: failure() + run: | + git diff + ls build/vcpkg_installed + find . -iname '*-err.log' -print -exec cat {} \; + - name: Upload mac build + id: upload-mac-build + if: runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-build-valhalla-${{ runner.arch }} + path: | + build/valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz + - name: Upload linux build + id: upload-linux-build + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-build-valhalla-${{ runner.arch }} + path: | + build/valhalla-${{ inputs.valhalla_ref }}-Linux.tar.gz + - name: Upload Windows build + id: upload-windows-build + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-build-valhalla-${{ runner.arch }} + path: | + build/valhalla-${{ inputs.valhalla_ref }}-Windows.tar.gz + + combine_macos: + runs-on: macos-latest + needs: [build_unix] + steps: + - name: Cache build + id: build-cache + uses: actions/cache@v4 + with: + path: valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz + key: ${{ runner.os }}-fat-valhalla-${{ inputs.valhalla_ref }}-${{ github.workflow_sha }} + - name: Get macos arm build + if: steps.build-cache.outputs.cache-hit != 'true' + uses: actions/download-artifact@v4 + with: + name: macOS-build-valhalla-ARM64 + - name: Extract arm build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + mkdir arm64 + tar -xf valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz -C arm64 + rm valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz + - name: Get macos x64 build + if: steps.build-cache.outputs.cache-hit != 'true' + uses: actions/download-artifact@v4 + with: + name: macOS-build-valhalla-X64 + - name: Build fat binaries + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + mkdir x86 + tar -xf valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz -C x86 + rm valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz + # Now create the fat files + mkdir fat + function makefat() { + echo "${1}" + echo "${@}" + file="${1#x86/}" + file="${file#arm64/}" + if [ -f "${1}" ]; then + mkdir -p "fat/${file%/*}" + # Account for platform specific resource files + cp "${1}" "fat/${file}" + lipo -create -output "fat/${file}" "x86/${file}" "arm64/${file}" + fi + } + export -f makefat + find x86 -type f -exec bash -c 'makefat "${0}"' {} \; + find arm64 -type f -exec bash -c 'makefat "${0}"' {} \; + tar -czf valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz -C fat/valhalla-${{ inputs.valhalla_ref }}-Darwin . + - name: Upload mac build + id: upload-mac-build + uses: actions/upload-artifact@v4 + with: + name: macOS-build-valhalla-fat + path: | + valhalla-${{ inputs.valhalla_ref }}-Darwin.tar.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a88bef --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# The routing plugin +## Supported engines +### Valhalla +Valhalla is supported using the valhalla CLI interface. + +## Usage +1. Install the `routing2` plugin +2. Download data +3. Open `Routing` toggle dialog (`Windows` -> `Routing`) +4. Input start/end points into dialog. HINT: `ctrl`+`shift`+`c` will copy a node's coordinates! +5. `Calculate Route` +6. Wait for route to be calculated. The more data you have, the longer it will take. \ No newline at end of file diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..7f31e41 --- /dev/null +++ b/build.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/engines.sh b/engines.sh new file mode 100755 index 0000000..f11fe9b --- /dev/null +++ b/engines.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# We need to build bindings for the engines so we can use them in Java +if [ ! -d "engines" ]; then mkdir engines; fi +cd "engines" || exit 1 +function jextract_get() { + if [ ! -d "jextract-22" ]; then + curl -L "https://download.java.net/java/early_access/jextract/22/5/openjdk-22-jextract+5-33_macos-x64_bin.tar.gz" -o jextract.tar.gz + tar xf jextract.tar.gz + rm jextract.tar.gz + fi + PATH="$(pwd)/jextract-22/bin:${PATH}" +} +function git_latest_tag() { + ### Checkout the latest tag + local tag + tag="$(git describe origin/master --abbrev=0 --tags)" + git checkout "${tag}" +} + +function prime_server() { + if [ ! -d prime_server ]; then git clone https://github.com/kevinkreiser/prime_server.git ; else git -C prime_server remote update; fi + cd prime_server || exit 1 + # latest tag (0.7.0) is years old + git checkout master && git pull + #git_latest_tag + git submodule update --init --recursive + ./autogen.sh + ./configure + make test -j8 + #sudo make install + cd .. +} + +function valhalla() { + ## Valhalla + ### Clone valhalla + if [ ! -d "valhalla" ]; then git clone https://github.com/valhalla/valhalla.git; else git -C valhalla remote update; fi + cd "valhalla" || exit 1 + git checkout master && git pull + git checkout 3.5.1 + npm install --ignore-scripts + git submodule update --init --recursive + if [ -d build ] ; then rm -rf build; fi + mkdir build + cd build || exit 1 + cmake .. -DCMAKE_BUILD_TYPE=Release -DDENABLE_STATIC_LIBRARY_MODULES=On -DDENABLE_GDAL=OFF -DENABLE_SERVICES=OFF + make -j"$(nproc)" + make package + #sudo make install + cd ../../../ + + mkdir -p valhalla_tiles + ./engines/valhalla/build/valhalla_build_config --mjolnir-tile-dir $(pwd)/valhalla_tiles --mjolnir-tile-extract $(pwd)/valhalla_tiles.tar --mjolnir-timezone $(pwd)/valhalla_tiles/timezones.sqlite --mjolnir-admin $(pwd)/valhalla_tiles/admins.sqlite > valhalla.json + ./engines/valhalla/build/valhalla_build_timezones > valhalla_tiles/timezones.sqlite + ./engines/valhalla/build/valhalla_build_admins -c valhalla.json ~/Downloads/colorado-latest.osm.pbf + ./engines/valhalla/build/valhalla_build_tiles -c valhalla.json ~/Downloads/colorado-latest.osm.pbf + ./engines/valhalla/build/valhalla_build_extract -c valhalla.json -v --overwrite + # Test install + ./engines/valhalla/build/valhalla_run_route --config valhalla.json --json '{"locations": [{"lat":39.0776524, "lon":-108.4588285}, {"lat":39.0676135, "lon":-108.5601538}], "costing":"auto","directions_options":{"units":"miles"}}' --verbose-lanes + ./engines/valhalla/build/valhalla_service valhalla.json route '{"locations": [{"lat":39.0776524, "lon":-108.4588285}, {"lat":39.0676135, "lon":-108.5601538}], "costing":"auto","directions_options":{"units":"miles"}}' + + # Note: future versions of jextract *may* be able to take multiple header files. TODO change trailing \; to \+ + # Important file is tyr/actor.h anyway. + find engines/valhalla/valhalla -name '*.h' -exec jextract \ + --include-dir /Library/Developer/CommandLineTools/SDKs/MacOSX14.4.sdk/usr/include/c++/v1 \ + --target-package org.openstreetmap.josm.plugins.routing2.lib.valhalla \ + --output src/main/java \ + {} \; + +} +jextract_get +prime_server +set -ex +valhalla + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..84da0c5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.openstreetmap.josm.plugins + plugin-root + SNAPSHOT + + routing2 + + ${plugin.link} + + 21 + ${java.lang.version} + ${java.lang.version} + ${java.lang.version} + UTF-8 + src/main/java + src/test/java + 19044 + Taylor Smock + org.openstreetmap.josm.plugins.routing2.Routing2Plugin + Production-ready routing with valhalla + https://github.com/tsmock/routing2 + src/main/resources + apache-commons;pbf + true + + + + + org.openstreetmap.josm.plugins + apache-commons + SNAPSHOT + provided + + + org.openstreetmap.josm.plugins + pbf + SNAPSHOT + provided + + + junit + junit + 4.13.2 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.wiremock + wiremock + test + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + + ${java.lang.version} + ${plugin.requires} + ${plugin.canloadatruntime} + ${plugin.link} + + + + + + + + diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/Routing2Plugin.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/Routing2Plugin.java new file mode 100644 index 0000000..1faf9fc --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/Routing2Plugin.java @@ -0,0 +1,51 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2; + +import java.util.ArrayList; +import java.util.List; + +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.plugins.Plugin; +import org.openstreetmap.josm.plugins.PluginInformation; +import org.openstreetmap.josm.tools.Destroyable; + +public class Routing2Plugin extends Plugin implements Destroyable { + private static PluginInformation pluginInformation; + + /** + * Creates the plugin + * + * @param info the plugin information describing the plugin. + */ + public Routing2Plugin(PluginInformation info) { + super(info); + pluginInformation = info; + } + + @Override + public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) { + super.mapFrameInitialized(oldFrame, newFrame); + if (newFrame != null) { + MainApplication.getMap().addToggleDialog(new RoutingDialog()); + } + } + + @Override + public void destroy() { + final List layerList = new ArrayList<>( + MainApplication.getLayerManager().getLayersOfType(RoutingLayer.class)); + layerList.forEach(MainApplication.getLayerManager()::removeLayer); + if (MainApplication.getMap() != null) { + MainApplication.getMap().removeToggleDialog(MainApplication.getMap().getToggleDialog(RoutingDialog.class)); + } + } + + /** + * Get the information for the plugin + * @return The plugin information + */ + public static PluginInformation getInfo() { + return pluginInformation; + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingDialog.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingDialog.java new file mode 100644 index 0000000..30a1c7f --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingDialog.java @@ -0,0 +1,263 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2; + +import static org.openstreetmap.josm.tools.I18n.tr; + +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.ListCellRenderer; +import javax.swing.text.JTextComponent; + +import org.openstreetmap.josm.actions.JosmAction; +import org.openstreetmap.josm.data.coor.LatLon; +import org.openstreetmap.josm.data.coor.conversion.LatLonParser; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.SideButton; +import org.openstreetmap.josm.gui.dialogs.ToggleDialog; +import org.openstreetmap.josm.gui.util.GuiHelper; +import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; +import org.openstreetmap.josm.gui.widgets.JosmTextField; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Legs; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Maneuver; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Trip; +import org.openstreetmap.josm.tools.GBC; +import org.openstreetmap.josm.tools.Logging; +import org.openstreetmap.josm.tools.Shortcut; + +/** + * Create a new dialog for routing + */ +public class RoutingDialog extends ToggleDialog { + /** Create the dialog */ + public RoutingDialog() { + super(tr("Routing"), "routing", tr("Generate routes between points"), Shortcut + .registerShortcut("routing:dialog", tr("Routing Dialog"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), 200, + false, null, false); + build(); + } + + private void build() { + // TODO add routing methods/option button here, see https://valhalla.openstreetmap.de/ for sample + final JosmTextField start = new JosmTextField(); + final JosmTextField end = new JosmTextField(); + final RouteInstructions instructions = new RouteInstructions(); + final SideButton doRouting = new SideButton(new JosmAction(tr("Calculate route"), "dialogs/routing", + tr("Calculate route"), Shortcut.registerShortcut("routing:calculate", tr("Calculate route"), + KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), + false, false) { + @Override + public void actionPerformed(ActionEvent e) { + final RoutingLayer layer = new RoutingLayer("Route", LatLonParser.parse(start.getText()), + LatLonParser.parse(end.getText())); + layer.addTripListener(instructions); + MainApplication.getLayerManager().addLayer(layer); + } + }); + final JPanel dataPanel = new JPanel(new GridBagLayout()); + start.addFocusListener(new HintListener(start, tr("Starting point"))); + end.addFocusListener(new HintListener(end, tr("Destination"))); + dataPanel.add(start, GBC.eol().fill(GBC.HORIZONTAL)); + dataPanel.add(end, GBC.eol().fill(GBC.HORIZONTAL)); + dataPanel.add(instructions, GBC.eol().fill(GBC.BOTH)); + this.createLayout(dataPanel, false, Collections.singleton(doRouting)); + new LatLonValidator(doRouting, start); + new LatLonValidator(doRouting, end); + } + + private static class RouteInstructions extends JPanel implements Consumer { + public RouteInstructions() { + super(new GridBagLayout()); + } + + @Override + public void accept(Trip trip) { + if (trip != null) { + GuiHelper.runInEDT(() -> rebuildTrip(trip)); + } + } + + private void rebuildTrip(Trip trip) { + this.removeAll(); + JPanel scroller = new JPanel(new GridBagLayout()); + for (Legs leg : trip.legs()) { + scroller.add(new LegPanel(leg), GBC.eol().anchor(GBC.LINE_START).fill(GBC.BOTH)); + } + this.add(GuiHelper.embedInVerticalScrollPane(scroller), GBC.eol().fill(GBC.BOTH)); + } + } + + private static class HintListener implements FocusListener { + private final String hint; + private final JTextComponent textComponent; + + public HintListener(JTextComponent textComponent, String hint) { + Objects.requireNonNull(textComponent); + Objects.requireNonNull(hint); + this.textComponent = textComponent; + this.hint = hint; + } + + @Override + public void focusGained(FocusEvent e) { + if (hint.equals(this.textComponent.getText())) { + this.textComponent.setText(""); + this.textComponent.setForeground(Color.BLACK); + } + } + + @Override + public void focusLost(FocusEvent e) { + if (this.textComponent.getText().isBlank() || this.hint.equals(this.textComponent.getText())) { + this.textComponent.setText(this.hint); + this.textComponent.setForeground(Color.GRAY); + } + } + } + + private static class LegPanel extends JPanel { + public LegPanel(Legs leg) { + super(new GridBagLayout()); + this.add(new ManeuverTable(leg.maneuvers()), GBC.eol().fill(GBC.BOTH)); + } + } + + private static class ManeuverTable extends JList { + public ManeuverTable(Maneuver[] maneuvers) { + super(maneuvers); + this.setCellRenderer(new ManeuverCellRenderer()); + this.getSelectionModel().addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + MainApplication.getLayerManager().getLayersOfType(RoutingLayer.class) + .forEach(l -> l.setHighlightedManeuver(getSelectedValue())); + } + }); + } + } + + private static class ManeuverCellRenderer extends JPanel implements ListCellRenderer { + private final Color defaultColor; + private final JLabel header; + private final JLabel preVerbalTransitionInstruction; + private final JLabel verbalTransitionInstruction; + private final JLabel postVerbalTransitionInstruction; + private final JLabel time; + private final JLabel length; + private final JLabel cost; + private final JLabel travelMode; + private final JLabel travelType; + private final JLabel type; + + ManeuverCellRenderer() { + super(new GridBagLayout()); + this.setAlignmentX(JPanel.LEFT_ALIGNMENT); + this.header = new JLabel(); + this.preVerbalTransitionInstruction = new JLabel(); + this.verbalTransitionInstruction = new JLabel(); + this.postVerbalTransitionInstruction = new JLabel(); + this.cost = new JLabel(); + this.length = new JLabel(); + this.time = new JLabel(); + this.travelMode = new JLabel(); + this.travelType = new JLabel(); + this.type = new JLabel(); + + final GridBagConstraints gbc = GBC.eol().anchor(GBC.LINE_START); + this.add(this.header, gbc); + this.add(this.preVerbalTransitionInstruction, gbc); + this.add(this.verbalTransitionInstruction, gbc); + this.add(this.postVerbalTransitionInstruction, gbc); + this.add(new JSeparator(), GBC.eol()); + this.add(this.cost, GBC.std().insets(1, 0, 1, 0)); + this.add(this.length, GBC.std().insets(5, 0, 1, 0)); + this.add(this.time, GBC.eol().insets(5, 0, 1, 0)); + this.add(new JSeparator(), GBC.eol()); + this.add(this.travelMode, GBC.std().insets(1, 0, 1, 0)); + this.add(this.travelType, GBC.std().insets(5, 0, 1, 0)); + this.add(this.type, GBC.eol().insets(5, 0, 1, 0)); + + this.defaultColor = this.getBackground(); + } + + @Override + public Component getListCellRendererComponent(JList list, Maneuver value, int index, + boolean isSelected, boolean cellHasFocus) { + if (value instanceof Maneuver maneuver) { + this.header.setText((index + 1) + ". " + maneuver.instruction() + " (" + maneuver.travelType() + ")"); + this.preVerbalTransitionInstruction.setText( + tr("Pre-verbal Transition Instruction: {0}", maneuver.preVerbalTransitionInstruction())); + this.verbalTransitionInstruction + .setText(tr("Verbal Transition Instruction: {0}", maneuver.verbalTransitionInstruction())); + this.postVerbalTransitionInstruction.setText( + tr("Post-verbal Transition Instruction: {0}", maneuver.postVerbalTransitionInstruction())); + this.cost.setText(tr("Cost: {0}", maneuver.cost())); + this.length.setText(tr("Length: {0}", maneuver.length())); + this.time.setText(tr("Time: {0}", maneuver.time())); + this.travelMode.setText(tr("Travel mode: {0}", maneuver.travelMode())); + this.travelType.setText(tr("Travel type: {0}", maneuver.travelType())); + this.type.setText(tr("Type: {0}", maneuver.type())); + if (cellHasFocus || isSelected) { + this.setBackground(Color.RED); + } else { + this.setBackground(defaultColor); + } + // I'm not certain why doing something similar didn't work in the constructor. + JPanel forceLeftAlign = new JPanel(new FlowLayout(FlowLayout.LEADING)); + forceLeftAlign.add(this); + return forceLeftAlign; + } + return null; + } + } + + private static class LatLonValidator extends AbstractTextComponentValidator { + + private final JButton toEnable; + + protected LatLonValidator(JButton toEnable, JTextComponent tc) { + super(tc); + this.toEnable = toEnable; + } + + @Override + public void validate() { + LatLon latLon; + try { + latLon = LatLonParser.parse(this.getComponent().getText()); + if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) { + latLon = null; + } + } catch (IllegalArgumentException e) { + Logging.trace(e); + latLon = null; + } + if (latLon == null) { + feedbackInvalid(tr("Please enter GPS coordinates")); + this.toEnable.setEnabled(false); + } else { + feedbackValid(null); + this.toEnable.setEnabled(true); + } + } + + @Override + public boolean isValid() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingLayer.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingLayer.java new file mode 100644 index 0000000..d645215 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/RoutingLayer.java @@ -0,0 +1,278 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2; + +import static org.openstreetmap.josm.tools.I18n.tr; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import javax.swing.Action; +import javax.swing.Icon; + +import org.openstreetmap.josm.data.Bounds; +import org.openstreetmap.josm.data.UndoRedoHandler; +import org.openstreetmap.josm.data.coor.ILatLon; +import org.openstreetmap.josm.data.coor.LatLon; +import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapView; +import org.openstreetmap.josm.gui.layer.Layer; +import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Legs; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Locations; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Maneuver; +import org.openstreetmap.josm.plugins.routing2.lib.generic.SetupException; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Trip; +import org.openstreetmap.josm.plugins.routing2.lib.valhalla.ValhallaServer; +import org.openstreetmap.josm.tools.JosmRuntimeException; +import org.openstreetmap.josm.tools.ListenerList; + +public class RoutingLayer extends Layer implements UndoRedoHandler.CommandQueueListener { + private final ListenerList> tripConsumers = ListenerList.create(); + private final ILatLon start; + private final ILatLon end; + private Trip trip; + private Maneuver maneuver; + + /** + * Create the layer and fill in the necessary components. + * + * @param name Layer name + * @param start The start of the route + * @param end The end of the route + */ + protected RoutingLayer(String name, ILatLon start, ILatLon end) { + super(name); + UndoRedoHandler.getInstance().addCommandQueueListener(this); + this.start = start; + this.end = end; + this.commandChanged(0, 0); + this.setOpacity(.5); + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public String getToolTipText() { + return "Routing layer"; + } + + @Override + public void mergeFrom(Layer from) { + // Yeah, no + } + + @Override + public boolean isMergable(Layer other) { + return false; + } + + @Override + public void visitBoundingBox(BoundingXYVisitor v) { + + } + + @Override + public Object getInfoComponent() { + return null; + } + + @Override + public Action[] getMenuEntries() { + return new Action[0]; + } + + @Override + public void paint(Graphics2D g, MapView mv, Bounds bbox) { + final Trip current = this.trip; + if (current != null) { + Path2D.Double maneuverShape = new Path2D.Double(); + for (Legs leg : current.legs()) { + double[] shape = leg.shape(); + final Path2D.Double drawShape = new Path2D.Double(Path2D.WIND_NON_ZERO, shape.length / 2); + for (int i = 0; i < shape.length; i += 2) { + final double lat = shape[i]; + final double lon = shape[i + 1]; + Point2D p = mv.getPoint2D(new LatLon(lat, lon)); + if (this.maneuver != null && i / 2 >= this.maneuver.startShape() + && i / 2 <= this.maneuver.endShape()) { + if (i / 2 == this.maneuver.startShape()) { + maneuverShape.moveTo(p.getX(), p.getY()); + } else { + maneuverShape.lineTo(p.getX(), p.getY()); + } + } + if (i == 0) { + drawShape.moveTo(p.getX(), p.getY()); + } else { + drawShape.lineTo(p.getX(), p.getY()); + } + } + g.setColor(Color.GREEN); + g.setStroke(new BasicStroke(10)); + g.draw(drawShape); + g.setStroke(new BasicStroke(5)); + g.setColor(Color.RED); + g.draw(maneuverShape); + // Draw maneuver locations + for (Maneuver t : leg.maneuvers()) { + final int i = t.startShape(); + final double lat = shape[2 * i]; + final double lon = shape[2 * i + 1]; + final Point2D p = mv.getPoint2D(new LatLon(lat, lon)); + final Point2D previous; + final Point2D next; + if (i == 0 && shape.length <= 2 * (i + 1) + 1) { + previous = null; + next = null; + } else if (i == 0) { + previous = null; + next = mv.getPoint2D(new LatLon(shape[2 * (i + 1)], shape[2 * (i + 1) + 1])); + } else if (shape.length <= 2 * (i + 1) + 1) { + previous = mv.getPoint2D(new LatLon(shape[2 * (i - 1)], shape[2 * (i - 1) + 1])); + next = null; + } else { + previous = mv.getPoint2D(new LatLon(shape[2 * (i - 1)], shape[2 * (i - 1) + 1])); + next = mv.getPoint2D(new LatLon(shape[2 * (i + 1)], shape[2 * (i + 1) + 1])); + } + g.setColor(Color.ORANGE); + g.drawRect((int) (p.getX() - 4), (int) (p.getY() - 4), 8, 8); + paintArrow(g, t.type(), previous, p, next); + } + } + if (current.locations() != null) { + for (Locations loc : current.locations()) { + g.setColor(Color.RED); + Point2D point = mv.getPoint2D(loc); + g.drawRect((int) point.getX(), (int) point.getY(), 3, 3); + } + } + } + } + + private void paintArrow(Graphics2D g, Maneuver.Type type, Point2D previous, Point2D current, Point2D next) { + final Polygon arrowHead = new Polygon(); + arrowHead.addPoint(0, -5); + arrowHead.addPoint(-5, 5); + arrowHead.addPoint(5, 5); + final AffineTransform original = g.getTransform(); + final AffineTransform transform = AffineTransform.getTranslateInstance(current.getX(), current.getY()); + final double angle; + if (previous != null) { + angle = Math.atan2(current.getY() - previous.getY(), current.getX() - previous.getX()) + Math.PI / 2; + } else if (next != null) { + angle = Math.atan2(current.getY() - next.getY(), current.getX() - next.getX()) + Math.PI / 2; + } else { + angle = 0; + } + transform.rotate(angle); + transform.preConcatenate(original); + // Rotate relative to previous point + g.setColor(Color.YELLOW); + final double turnAngle = switch (type) { + case RIGHT, DESTINATION_RIGHT, EXIT_RIGHT, START_RIGHT -> Math.PI / 2; + case LEFT, DESTINATION_LEFT, EXIT_LEFT, START_LEFT -> 3 * Math.PI / 2; + // Don't bother painting arrows + case NONE -> Double.NaN; + default -> Double.NaN; + }; + if (Double.isNaN(turnAngle)) + return; + transform.rotate(turnAngle); + try { + g.setTransform(transform); + g.fill(arrowHead); + } finally { + g.setTransform(original); + } + } + + /** + * Set the trip for this layer + * @param newTrip The trip to show the user + */ + public void setTrip(Trip newTrip) { + this.trip = newTrip; + this.tripConsumers.fireEvent(c -> c.accept(newTrip)); + this.invalidate(); + } + + /** + * Get the current trip shown + * @return The current trip + */ + public Trip getTrip() { + return this.trip; + } + + /** + * Add a listener for when a trip updates + * @param tripConsumer The consumer to notify + */ + public void addTripListener(Consumer tripConsumer) { + this.tripConsumers.addListener(tripConsumer); + } + + @Override + public void commandChanged(int queueSize, int redoSize) { + MainApplication.worker.execute(() -> { + ValhallaServer valhallaServer = new ValhallaServer(); + final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor( + tr("Downloading configured router")); + if (valhallaServer.shouldPerformSetup()) { + try { + monitor.beginTask(tr("Download"), 1); + valhallaServer.performSetup(monitor); + } catch (SetupException setupException) { + throw new JosmRuntimeException(setupException); + } finally { + monitor.close(); + } + } + if (!monitor.isCanceled()) { + this.setTrip(valhallaServer.generateRoute(MainApplication.getLayerManager().getActiveDataLayer(), + this.start, this.end)); + } + }); + } + + @Override + public synchronized void destroy() { + super.destroy(); + UndoRedoHandler.getInstance().removeCommandQueueListener(this); + } + + /** + * Set the currently highlighted maneuver + * @param maneuver The maneuver to highlight + */ + public void setHighlightedManeuver(Maneuver maneuver) { + if (this.trip != null && maneuver != null + && Stream.of(this.trip.legs()).flatMap(l -> Arrays.stream(l.maneuvers())).anyMatch(maneuver::equals)) { + this.maneuver = maneuver; + this.invalidate(); + } else if (maneuver == null) { + this.maneuver = null; + this.invalidate(); + } + } + + /** + * Get the currently highlighted maneuver + * @return The highlighted maneuver + */ + public Maneuver getHighlightedManeuver() { + return this.maneuver; + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Costing.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Costing.java new file mode 100644 index 0000000..5f7eeec --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Costing.java @@ -0,0 +1,6 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +public enum Costing { + AUTO, BICYCLE, BUS, BIKESHARE, TRUCK, TAXI, MOTOR_SCOOTER, MOTORCYCLE, MULTIMODAL, PEDESTRIAN +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolyline.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolyline.java new file mode 100644 index 0000000..4b1e99f --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolyline.java @@ -0,0 +1,125 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +import java.util.Arrays; + +/** + * A java implementation of Google's encoded + * polyline. + */ +public final class GooglePolyline { + private GooglePolyline() { + // Hide constructor + } + + /** + * Convert a series of latitude/longitude points + * @param doubles The series of points in lat/lon format + * @return The encoded polyline + */ + static String encode(double... doubles) { + if (doubles.length % 2 != 0) { + throw new IllegalArgumentException("Coordinate points must come in pairs"); + } + int lastLat1e5 = 0; + int lastLon1e5 = 0; + StringBuilder sb = new StringBuilder(4 * doubles.length); + for (int i = 0; i < doubles.length; i += 2) { + // Get the 1e5 value + final int e5lat = Math.toIntExact(Math.round(1e5 * doubles[i])); + final int e5lon = Math.toIntExact(Math.round(1e5 * doubles[i + 1])); + sb.append(convertE5(e5lat - lastLat1e5)); + sb.append(convertE5(e5lon - lastLon1e5)); + lastLat1e5 = e5lat; + lastLon1e5 = e5lon; + } + // Finally, convert the chunks to a string + return sb.toString(); + } + + /** + * Perform the encoding of a 1e5 value + * @param e5 The value to encode + * @return The encoded value as a char array + */ + private static char[] convertE5(int e5) { + // Java is already 2 complement, so we don't have to specially handle negative numbers here. + int shift = e5 << 1; // we do lose the leading binary + if (e5 < 0) { // If original was negative, invert the encoding + shift = ~shift; + } + // Convert to binary + final String binary = Integer.toBinaryString(shift); + // Split into 5 bit chunks, starting from right side (reverse order) + final byte overflow = (byte) (binary.length() % 5 > 0 ? 1 : 0); + final char[] chunks = new char[binary.length() / 5 + overflow]; + final String[] cc = new String[chunks.length]; + for (int i = binary.length(); i > 0; i -= 5) { + int j = chunks.length - i / 5 - overflow; + cc[j] = binary.substring(Math.max(0, i - 5), i); + chunks[j] = (char) Short.parseShort(cc[j], 2); + if (j != chunks.length - 1) { + chunks[j] |= 0x20; // If not the last chunk or it with 0x20 + } + chunks[j] += 63; // Add 63 to each chunk + } + return chunks; + } + + /** + * Decode with a default precision of 1e5 + * @param polyline The polyline to decode + * @return The decoded polyline + */ + static double[] decode(String polyline) { + return decode(polyline, 1e5); + } + + /** + * Decode with a given precision + * @param polyline The polyline to decode + * @param precision The precision to use + * @return The decoded polyline + */ + public static double[] decode(String polyline, double precision) { + // This is the absolute "maximum" number of points. Could be optimized, probably not high traffic code path. + double[] points = new double[polyline.length()]; + // Start performing char operations + char[] chars = polyline.toCharArray(); + int point = 0; + int current = 0; + char[] chunks = new char[8]; // 8 * 4 = 32 bits + int lastLat1e5 = 0; + int lastLon1e5 = 0; + for (int i = 0; i < chars.length; i++) { + // First, remove 63 from each char value + chars[i] -= 63; + // Check if this is the last one + boolean isLast = (chars[i] & 0x20) == 0; + chars[i] &= (char) (~0x20 & chars[i]); + chunks[current++] = chars[i]; + if (isLast) { + int value = 0; + while (current > 0) { + value = (value << 5) + chunks[--current]; + } + if ((chunks[0] & 1) != 0) { + // Assume it was negative + value = ~value; + } + value = value >> 1; + if (point % 2 == 0) { + value += lastLat1e5; + lastLat1e5 = value; + } else { + value += lastLon1e5; + lastLon1e5 = value; + } + points[point++] = value / precision; + current = 0; + } + } + + return Arrays.copyOf(points, point); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/IRouter.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/IRouter.java new file mode 100644 index 0000000..90bf73f --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/IRouter.java @@ -0,0 +1,32 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +import org.openstreetmap.josm.data.coor.ILatLon; +import org.openstreetmap.josm.gui.layer.OsmDataLayer; +import org.openstreetmap.josm.gui.progress.ProgressMonitor; + +/** + * An interface for communicating with routers + */ +public interface IRouter { + /** + * Check if setup should be performed + * @return {@code true} if {@link #performSetup(ProgressMonitor)} should be called + */ + boolean shouldPerformSetup(); + + /** + * Perform setup steps + * @param progressMonitor The progress monitor to update + * @throws SetupException when something fails during setup + */ + void performSetup(ProgressMonitor progressMonitor) throws SetupException; + + /** + * Generate a route + * @param layer The layer to do routing on + * @param locations The locations (at least two locations must be specified; the start and end points) + * @throws TripException when trip calculations fail + */ + Trip generateRoute(OsmDataLayer layer, ILatLon... locations) throws TripException; +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Legs.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Legs.java new file mode 100644 index 0000000..256cc60 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Legs.java @@ -0,0 +1,5 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +public record Legs(Maneuver[] maneuvers, Trip.Summary summary, double[] shape) { +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Locations.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Locations.java new file mode 100644 index 0000000..1b6f6d2 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Locations.java @@ -0,0 +1,41 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +import java.util.Map; + +import org.openstreetmap.josm.data.coor.ILatLon; + +public record Locations( + /* We first have the items that affect routing */ + double lat, double lon, Type type, double heading, double heading_tolerance, String street, + long way_id, int minimum_reachability, double radius, boolean rank_candidates, + Side preferred_side, double display_lat, double display_lon, double search_cutoff, + double node_snap_tolerance, double street_side_max_distance, + StreetType street_side_cutoff, Map search_filter, + Object preferred_layer, + /* Then we have the items that only affect returns for convenience */ + String name, String city, String state, String postal_code, String country, + String phone, String url, Double waiting) implements ILatLon { + enum Type { + BREAK, + THROUGH, + VIA, + BREAK_THROUGH + } + enum Side { + SAME, + OPPOSITE, + EITHER + } + enum StreetType { + MOTORWAY, TRUNK, PRIMARY, SECONDARY, TERTIARY, UNCLASSIFIED, RESIDENTIAL, SERVICE_OTHER + } + enum SearchFilter { + EXCLUDE_TUNNEL, + EXCLUDE_BRIDGE, + EXCLUDE_RAMP, + EXCLUDE_CLOSURES, + MIN_ROAD_CLASS, + MAX_ROAD_CLASS + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Maneuver.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Maneuver.java new file mode 100644 index 0000000..56587d0 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Maneuver.java @@ -0,0 +1,70 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +/** + * A maneuver (originally taken from valhalla output) + * @param type + * @param instruction + * @param verbalTransitionInstruction + * @param preVerbalTransitionInstruction + * @param postVerbalTransitionInstruction + * @param time + * @param length + * @param cost + * @param startShape + * @param endShape + * @param multiVerbalCue + * @param travelMode + * @param travelType + */ +public record Maneuver(Type type, String instruction, String verbalTransitionInstruction, String preVerbalTransitionInstruction, + String postVerbalTransitionInstruction, double time, double length, double cost, int startShape, int endShape, + boolean multiVerbalCue, String travelMode, String travelType) { + + /** The maneuver types */ + public enum Type { + NONE, + START, + START_RIGHT, + START_LEFT, + DESTINATION, + DESTINATION_RIGHT, + DESTINATION_LEFT, + BECOMES, + CONTINUE, + SLIGHT_RIGHT, + RIGHT, + SHARP_RIGHT, + U_TURN_RIGHT, + U_TURN_LEFT, + SHARP_LEFT, + LEFT, + SLIGHT_LEFT, + RAMP_STRAIGHT, + RAMP_LEFT, + EXIT_RIGHT, + EXIT_LEFT, + STAY_STRAIGHT, + STAY_RIGHT, + STAY_LEFT, + MERGE, + ROUNDABOUT_ENTER, + ROUNDABOUT_EXIT, + FERRY_ENTER, + FERRY_EXIT, + TRANSIT, + TRANSIT_TRANSFER, + TRANSIT_REMAIN_ON, + TRANSIT_CONNECTION_START, + TRANSIT_CONNECTION_TRANSFER, + TRANSIT_CONNECTION_DESTINATION, + POST_TRANSIT_CONNECTION_DESTINATION, + MERGE_RIGHT, + MERGE_LEFT, + ELEVATOR_ENTER, + STEPS_ENTER, + ESCALATOR_ENTER, + BUILDING_ENTER, + BUILDING_EXIT + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/SetupException.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/SetupException.java new file mode 100644 index 0000000..414f548 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/SetupException.java @@ -0,0 +1,15 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +/** + * Thrown when there is an issue setting up a router + */ +public class SetupException extends Exception { + public SetupException(String message) { + super(message); + } + + public SetupException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Trip.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Trip.java new file mode 100644 index 0000000..b04d4f7 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/Trip.java @@ -0,0 +1,8 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +public record Trip(Locations[] locations, Legs[] legs, Summary summary) { + public record Summary(boolean has_time_restrictions, boolean has_toll, boolean has_highway, boolean has_ferry, + double min_lat, double min_lon, double max_lat, double max_lon, + double time, double length, double cost) {} +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/TripException.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/TripException.java new file mode 100644 index 0000000..f415cc3 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/generic/TripException.java @@ -0,0 +1,11 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +/** + * Thrown when there is an issue calculating a trip + */ +public class TripException extends Exception { + public TripException(String message) { + super(message); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/valhalla/ValhallaServer.java b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/valhalla/ValhallaServer.java new file mode 100644 index 0000000..bd56636 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/routing2/lib/valhalla/ValhallaServer.java @@ -0,0 +1,445 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.valhalla; + +import static org.openstreetmap.josm.tools.I18n.tr; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; + +import javax.swing.JOptionPane; + +import jakarta.json.stream.JsonParsingException; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.io.file.Counters; +import org.apache.commons.io.file.DeletingPathVisitor; +import org.openstreetmap.josm.data.coor.ILatLon; +import org.openstreetmap.josm.gui.Notification; +import org.openstreetmap.josm.gui.layer.OsmDataLayer; +import org.openstreetmap.josm.gui.progress.ProgressMonitor; +import org.openstreetmap.josm.gui.util.GuiHelper; +import org.openstreetmap.josm.io.ProgressInputStream; +import org.openstreetmap.josm.plugins.pbf.io.PbfExporter; +import org.openstreetmap.josm.plugins.routing2.Routing2Plugin; +import org.openstreetmap.josm.plugins.routing2.lib.generic.GooglePolyline; +import org.openstreetmap.josm.plugins.routing2.lib.generic.IRouter; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Legs; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Locations; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Maneuver; +import org.openstreetmap.josm.plugins.routing2.lib.generic.SetupException; +import org.openstreetmap.josm.plugins.routing2.lib.generic.Trip; +import org.openstreetmap.josm.spi.preferences.Config; +import org.openstreetmap.josm.tools.HttpClient; +import org.openstreetmap.josm.tools.JosmRuntimeException; +import org.openstreetmap.josm.tools.Logging; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonReader; +import jakarta.json.JsonValue; +import org.openstreetmap.josm.tools.PlatformManager; + +/** + * Make calls to a local install of Valhalla + */ +public final class ValhallaServer implements IRouter { + private static final String valhallaVersion = "3.5.1"; + + @Override + public boolean shouldPerformSetup() { + try { + final Path dir = getCacheDir().resolve("bin").resolve("valhalla"); + if (!Files.isDirectory(dir)) { + return true; + } + final Path binDir = dir.resolve("bin"); + final Path versionFile = dir.resolve("version"); + if (!Files.isRegularFile(versionFile) || !valhallaVersion.equals(Files.readString(versionFile))) { + return true; + } + if (!Files.isDirectory(binDir) && !PlatformManager.isPlatformWindows()) { // Windows doesn't have a bin dir + return true; + } + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + return false; + } + + @Override + public void performSetup(ProgressMonitor progressMonitor) throws SetupException { + try { + realPerformSetup(progressMonitor); + } catch (IOException ioException) { + throw new SetupException(ioException); + } + } + + /** + * Perform the actual setup steps in a synchronized block, to avoid downloading stuff multiple times + * @param updateable The object to use for progress updates + * @throws IOException If there is an issue performing setup + */ + private static synchronized void realPerformSetup(ProgressMonitor updateable) throws IOException { + final Path dir = getCacheDir().resolve("bin").resolve("valhalla"); + if (!Files.isDirectory(dir)) { + Files.createDirectories(dir); + } + final Path binDir = dir.resolve("bin"); + final Path versionFile = dir.resolve("version"); + if (!Files.isRegularFile(versionFile) || !valhallaVersion.equals(Files.readString(versionFile))) { + updateable.indeterminateSubTask(tr("Deleting old valhalla binaries")); + // Delete old binaries + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (updateable.isCanceled()) { + return FileVisitResult.TERMINATE; + } + Files.delete(file); + updateable.worked(1); + return super.visitFile(file, attrs); + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (updateable.isCanceled()) { + return FileVisitResult.TERMINATE; + } + Files.delete(dir); + updateable.worked(1); + return super.postVisitDirectory(dir, exc); + } + }); + Files.createDirectories(dir); + } + if (!Files.isDirectory(binDir)) { + updateable.subTask(tr("Downloading valhalla binaries")); + if (PlatformManager.isPlatformOsx()) { + extractBinaries(updateable, "Darwin", dir); + } else if (PlatformManager.isPlatformUnixoid()) { + extractBinaries(updateable, "Linux", dir); + } else if (PlatformManager.isPlatformWindows()) { + extractBinaries(updateable, "Windows", dir); + } else { + throw new UnsupportedOperationException("Your platform is not currently supported"); + } + // Do this last in case of cancellation + if (!updateable.isCanceled()) { + Files.writeString(versionFile, valhallaVersion); + } + } + } + + @Override + public Trip generateRoute(OsmDataLayer layer, ILatLon... locations) { + final Path config = generateConfig(); + final Path dataPath = writeDataSet(layer); + try { + if (!Files.isDirectory(getCacheDir().resolve("valhalla_tiles"))) { + Files.createDirectory(getCacheDir().resolve("valhalla_tiles")); + } else { + Files.walkFileTree(getCacheDir().resolve("valhalla_tiles"), + new DeletingPathVisitor(Counters.noopPathCounters())); + } + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + if (!PlatformManager.isPlatformWindows()) + generateTimezones(config.resolveSibling("valhalla_tiles").resolve("timezones.sqlite")); + generateAdmins(config, dataPath); + generateTiles(config, dataPath); + generateExtract(config); + JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add("costing", "auto").add("directions_options", Json.createObjectBuilder().add("units", "miles")); + JsonArrayBuilder locationsArray = Json.createArrayBuilder(); + for (ILatLon location : locations) { + locationsArray.add(Json.createObjectBuilder().add("lat", location.lat()).add("lon", location.lon())); + } + builder.add("locations", locationsArray); + Process p; + final Path route = config.resolveSibling("route.json"); + try { + final String json = builder.build().toString(); + Files.writeString(route, json); + String[] args = new String[] { getPath("valhalla_service"), config.toString(), "route", route.toString() }; + Logging.info("Route command: " + String.join(" ", args)); + ProcessBuilder processBuilder = new ProcessBuilder(args); + processBuilder.directory(getCacheDir().toFile()); // FIXME remove + p = processBuilder.start(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (BufferedReader errors = p.errorReader()) { + errors.lines().forEach(Logging::error); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + final JsonObject data; + try (BufferedReader br = new BufferedReader(p.inputReader())) { + br.mark(40); + try (JsonReader reader = Json.createReader(br)) { + try { + data = reader.readObject(); + } catch (JsonParsingException jsonParsingException) { + br.reset(); + Logging.error(br.lines().collect(Collectors.joining("\n"))); + throw jsonParsingException; + } + } + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + // check if error + if (data.containsKey("status_code") && 200 != data.getInt("status_code")) { + if (data.getInt("error_code") == 442) { + GuiHelper.runInEDTAndWait( + () -> new Notification(tr("No route found")).setIcon(JOptionPane.WARNING_MESSAGE).show()); + return null; // No route found // FIXME: Throw RouteException with message? + } // FIXME: Look through https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/#http-status-codes-and-conditions for other "valid" problems. + throw new JosmRuntimeException(data.toString()); + } + final JsonObject trip = data.getJsonObject("trip"); + final Locations[] locations1 = trip.getJsonArray("locations").stream().map(ValhallaServer::parseLocation) + .filter(Objects::nonNull).toArray(Locations[]::new); + final Legs[] legs = trip.getJsonArray("legs").stream().map(ValhallaServer::parseLeg).toArray(Legs[]::new); + final Trip.Summary summary = parseSummary(trip.getJsonObject("summary")); + return new Trip(locations1, legs, summary); + } + + private static Path getCacheDir() throws IOException { + final Path dir = Config.getDirs().getCacheDirectory(true).toPath().resolve("routing2"); + if (!Files.isDirectory(dir)) { + Files.createDirectory(dir); + } + return dir; + } + + private static String getPath(String binary) throws IOException { + final Path dir = getCacheDir().resolve("bin").resolve("valhalla"); + if (PlatformManager.isPlatformWindows()) { + final Path exe = dir.resolve(binary + ".exe"); + if (Files.isExecutable(exe)) { + return exe.toString(); + } + return dir.resolve(binary).toString(); + } + final Path binDir = dir.resolve("bin"); + final Path binaryPath = binDir.resolve(binary); + return binaryPath.toString(); + } + + private static void extractBinaries(ProgressMonitor updateable, String platform, Path dir) throws IOException { + Objects.requireNonNull(dir); + final String version = Optional.ofNullable(Routing2Plugin.getInfo().version).orElse("SNAPSHOT"); + final String linkStart = Optional.ofNullable(Routing2Plugin.getInfo().link) + .orElse("https://github.com/JOSM/routing2"); + final URI downloadLocation; + if ("latest".equals(version) || "SNAPSHOT".equals(version)) { + downloadLocation = URI.create( + linkStart + "/releases/latest/download/valhalla-" + valhallaVersion + '-' + platform + ".tar.gz"); + } else { + downloadLocation = URI.create(linkStart + "/releases/download/v" + version + "/valhalla-" + valhallaVersion + + '-' + platform + ".tar.gz"); + } + HttpClient client = HttpClient.create(downloadLocation.toURL()); + try { + HttpClient.Response response = client.connect(); + if (response.getResponseCode() != 200) { + Logging.error(response.fetchContent()); + throw new IllegalStateException("Valhalla server download location returned HTTP error code " + + response.getResponseCode() + ": " + response.getResponseMessage()); + } + updateable.setTicks(0); + try (InputStream is = response.getContent(); + ProgressInputStream pis = new ProgressInputStream(is, response.getContentLength(), updateable); + InputStream gis = new GZIPInputStream(pis); + TarArchiveInputStream tais = new TarArchiveInputStream(gis)) { + byte[] bytes = new byte[1024]; + TarArchiveEntry tarArchiveEntry; + while (!updateable.isCanceled() && (tarArchiveEntry = tais.getNextEntry()) != null) { + Path saveLocation = dir.resolve(tarArchiveEntry.getName()); + if (tarArchiveEntry.isDirectory()) { + if (!Files.isDirectory(saveLocation)) { + Files.createDirectories(saveLocation); + } + } else { + try (OutputStream fos = Files.newOutputStream(saveLocation)) { + int len; + while ((len = tais.read(bytes)) > 0) { + fos.write(bytes, 0, len); + } + } + if (saveLocation.getParent().endsWith("bin") && !Files.isExecutable(saveLocation)) { + saveLocation.toFile().setExecutable(true, true); + } + } + } + } + } finally { + client.disconnect(); + } + } + + private Path generateConfig() { + try { + final Path dataDir = getCacheDir(); + final Path config = dataDir.resolve("valhalla.json").toAbsolutePath(); + if (!Files.exists(config) || Files.size(config) < 1) { + try (InputStream is = runCommand(getPath("valhalla_build_config"), "--mjolnir-tile-dir", + dataDir.resolve("valhalla_tiles").toString(), "--mjolnir-tile-extract", + dataDir.resolve("valhalla_tiles.tar").toString(), "--mjolnir-timezone", + dataDir.resolve("valhalla_tiles").resolve("timezones.sqlite").toString(), "--mjolnir-admin", + dataDir.resolve("valhalla_tiles").resolve("admins.sqlite").toString())) { + Files.copy(is, config); + } + } + return config; + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + + private void generateTimezones(Path output) { + if (!Files.exists(output)) { + try (InputStream is = runCommand(getPath("valhalla_build_timezones"))) { + Files.copy(is, output); + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + } + + private void generateAdmins(Path config, Path input) { + // FIXME: This needs to have full boundary information. Overpass download? + try (InputStream is = runCommand(getPath("valhalla_build_admins"), "--config", config.toString(), + input.toString())) { + printStdOut(is); + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + + private void generateTiles(Path config, Path input) { + try (InputStream is = runCommand(getPath("valhalla_build_tiles"), "--config", config.toString(), + input.toString())) { + printStdOut(is); + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + + private void generateExtract(Path config) { + try (InputStream is = runCommand(getPath("valhalla_build_extract"), "--config", config.toString(), "-v", + "--overwrite")) { + printStdOut(is); + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + + private static Trip.Summary parseSummary(JsonObject summary) { + return new Trip.Summary(summary.getBoolean("has_time_restrictions", false), + summary.getBoolean("has_toll", false), summary.getBoolean("has_highway", false), + summary.getBoolean("has_ferry", false), summary.getJsonNumber("min_lat").doubleValue(), + summary.getJsonNumber("min_lon").doubleValue(), summary.getJsonNumber("max_lat").doubleValue(), + summary.getJsonNumber("max_lon").doubleValue(), summary.getJsonNumber("time").doubleValue(), + summary.getJsonNumber("length").doubleValue(), summary.getJsonNumber("cost").doubleValue()); + } + + private static Locations parseLocation(JsonValue value) { + if (value instanceof JsonObject loc) { + return new Locations(loc.getJsonNumber("lat").doubleValue(), loc.getJsonNumber("lon").doubleValue(), null, + Double.NaN, Double.NaN, null, 0L, 0, Double.NaN, // FIXME + false, null, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, null, null, null, null, + null, null, null, null, null, null, null); + } + return null; + } + + private static Legs parseLeg(JsonValue value) { + if (value instanceof JsonObject leg) { + final Maneuver[] maneuvers = leg.getJsonArray("maneuvers").stream().map(ValhallaServer::parseManeuver) + .filter(Objects::nonNull).toArray(Maneuver[]::new); + final double[] shape = GooglePolyline.decode(leg.getString("shape"), 1e6); + final Trip.Summary summary = parseSummary(leg.getJsonObject("summary")); + return new Legs(maneuvers, summary, shape); + } + return new Legs(new Maneuver[0], null, new double[0]); + } + + private static Maneuver parseManeuver(JsonValue value) { + if (value instanceof JsonObject maneuver) { + return new Maneuver(Maneuver.Type.values()[maneuver.getInt("type")], maneuver.getString("instruction", ""), + maneuver.getString("verbal_succinct_transition_instruction", ""), + maneuver.getString("verbal_pre_transition_instruction", ""), + maneuver.getString("verbal_post_transition_instruction", ""), + maneuver.getJsonNumber("time").doubleValue(), maneuver.getJsonNumber("length").doubleValue(), + maneuver.getJsonNumber("cost").doubleValue(), maneuver.getInt("begin_shape_index"), + maneuver.getInt("end_shape_index"), maneuver.getBoolean("verbal_multi_cue", false), + maneuver.getString("travel_mode", ""), maneuver.getString("travel_type", "")); + } + return null; + } + + private Path writeDataSet(OsmDataLayer layer) { + try { + Path saveLocation = getCacheDir().resolve(layer.getName() + ".pbf"); + new PbfExporter().exportData(saveLocation.toFile(), layer); + saveLocation.toFile().deleteOnExit(); // Not perfect, but should reduce amount of space used long-term. + return saveLocation; + } catch (IOException ioException) { + throw new UncheckedIOException(ioException); + } + } + + private static void printStdOut(InputStream is) { + try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) { + br.lines().forEach(Logging::info); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static InputStream runCommand(String... args) throws IOException { + Logging.info("Running command: \"" + String.join(" ", args) + "\""); + ProcessBuilder builder = new ProcessBuilder(args); + builder.directory(getCacheDir().toFile()); + Process p = builder.start(); + if (false) { + try { + p.waitFor(); + } catch (InterruptedException interruptedException) { + Logging.error(interruptedException); + Thread.currentThread().interrupt(); + throw new JosmRuntimeException(interruptedException); + } + } + // Do not block here. + ForkJoinPool.commonPool().submit(() -> { + try (BufferedReader errors = p.errorReader()) { + errors.lines().forEach(Logging::error); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + return p.getInputStream(); + } +} diff --git a/src/main/resources/images/dialogs/routing.svg b/src/main/resources/images/dialogs/routing.svg new file mode 100644 index 0000000..fae99a4 --- /dev/null +++ b/src/main/resources/images/dialogs/routing.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/test/java/unit/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolylineTest.java b/src/test/java/unit/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolylineTest.java new file mode 100644 index 0000000..b94cdd3 --- /dev/null +++ b/src/test/java/unit/org/openstreetmap/josm/plugins/routing2/lib/generic/GooglePolylineTest.java @@ -0,0 +1,55 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.routing2.lib.generic; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.net.URI; +import java.util.Objects; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openstreetmap.josm.tools.Logging; + +class GooglePolylineTest { + static Stream values() { + return Stream.of(Arguments.of((Object) new double[] { 38.5, -120.2 }), + Arguments.of((Object) new double[] { 38.5, -120.2, 40.7, -120.95, 43.252, -126.453 })); + } + + @Test + void testEncode() { + assertEquals("_p~iF~ps|U_ulLnnqC_mqNvxq`@", + GooglePolyline.encode(38.5, -120.2, 40.7, -120.95, 43.252, -126.453)); + } + + @ParameterizedTest + @MethodSource("values") + void testDecode(double[] coordinates) { + assertArrayEquals(coordinates, GooglePolyline.decode(GooglePolyline.encode(coordinates))); + } + + /** + * Just to debug shape points + * @param args Not read + */ + public static void main(String... args) throws IOException { + final double[] shape1 = GooglePolyline.decode( + "sobpiAdayzmEp@C~GiAPsK`JG?tJ@zN?h@?dJ{GHaA?sd@Ja{@Lk@?mN@oVE}WG{TEy]?e_@?q]A_E?g`@B_e@eA}BOcGq@kGiAsCi@iE_A}DeAcD_AkDqAmEoBeHsCoGkDuFcD_HyEgIaH_}@iw@cZyXeEyC}DwBgFwBcGcBaFm@oFK_FXcEn@qEvAmEjBoJvEmHbJ_R`YYb@q@bAqDzF|CfEfOpShE~Frd@pm@rXta@xDjGhDtGtAnC~HvP~IxUjHnU`FzRhHd^`DnSfLjy@jaAbvGpIjk@Hj@lAdIt`@fmCjPphA`I~h@dAfHtJto@rPvgANbAbh@rhD~Jzq@hF|[hSvkAbq@xjDtEvXdMpv@fIlk@bFx\\jIti@jF~\\hGjd@`Kfq@x]r_Cv@lFpb@ttCzBfUxAtQx@pPd@jRT`SHrXLpg@VpfA@fA@jF?lF?~CVdTZtUd@dLv@hNdAxLxB~OfGx_@zHbWrEbNbElJnDdIxCdGn@pA|AxCtBxD~Pp[tDtHvL`VjKlXxF~PdBbGvD|PhFb\\zLdz@|Ktw@n[l|BnEz[vO|kAvt@lkFtQxrA~Kfr@|OngA|]veCba@duCtGnd@fTj}AjJxx@tEvh@pBrXpAxQnDtf@fIbiAbAfN`Cj]bE~f@bFve@hFxb@dOjdAfOrdAxJrq@lR~pAjEzYz@nFxI`j@tA`JlBnMtD|VjRfpAdM`{@|Ml}@~o@xlEfMd{@\\vBz@vFtAfH`@rBjD~NzAtFhEhMft@ztBtGpSxD`OrCtN~BnNpAvLfAtMj@`NLvLF`KE|hA?tH?jEAdzA?fG?pFAxvA?jA?zFAhFCbPEnb@I`o@A`G@lFDdyA?jF?fGBjwA?r@?`G?bGKxxA?tF_E?g@?{U?{CAaQCY?kEAsD?Y?oVAwOAeFAS?gF?yE?Y@}S@?zF?d[xHC", + 1e6); + final StringBuilder sb = new StringBuilder("http://127.0.0.1:8111/add_way?way="); + for (int i = 0; i < shape1.length; i += 2) { + sb.append(shape1[i]).append(',').append(shape1[i + 1]); + if (i + 2 < shape1.length) { + sb.append(';'); + } + } + var con = URI.create(sb.toString()).toURL().openConnection(); + con.connect(); + Logging.error(Objects.toString(con.getContent())); + } +} \ No newline at end of file diff --git a/valhalla.patch b/valhalla.patch new file mode 100644 index 0000000..22bece7 --- /dev/null +++ b/valhalla.patch @@ -0,0 +1,13 @@ +diff --git a/scripts/valhalla_build_extract b/scripts/valhalla_build_extract +index 236822513..0ccafabf6 100755 +--- a/scripts/valhalla_build_extract ++++ b/scripts/valhalla_build_extract +@@ -95,7 +95,7 @@ class TileResolver: + tar.addfile(tar_member, self._tar_obj.extractfile(tar_member.name)) + else: + tar.add(str(self.path.joinpath(t)), arcname=t) +- tar_member = tar.getmember(str(t)) ++ #tar_member = tar.getmember(str(t)) + + + description = "Builds a tar extract from the tiles in mjolnir.tile_dir to the path specified in mjolnir.tile_extract."