mirror of
https://github.com/DerTyp7/osm-routing-backend-python.git
synced 2025-10-28 12:02:10 +01:00
moved from GitLab to GitHub manually
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.db
|
||||
__pycache__/
|
||||
1
INSTALL.bat
Normal file
1
INSTALL.bat
Normal file
@@ -0,0 +1 @@
|
||||
pip -R install requirements.txt
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# OSM-Routing-Backend with Python
|
||||
A WSGI Server to calculate a route based on an OSM-File.<br/>
|
||||
Used for the backend of an OpenLayers-Visualization.<br/>
|
||||
Currently it only connects crossing nodes of streets/way, so the sampling of the route isn't that good.
|
||||
|
||||
## Install
|
||||
### Install Python
|
||||
Download & install Python(>=3.6) -> <a href="https://www.python.org/downloads/">Python Downloads</a>.
|
||||
### Install Python packages
|
||||
Run the "INSTALL.bat" <br/>
|
||||
OR<br/>
|
||||
run the following command in the project folder:
|
||||
`pip -R install requirements.txt`
|
||||
### Download an OSM-File
|
||||
1. Download an OSM-File of your region. Keep in mind that big OSM-Files can take a long time to process.<br>
|
||||
Example download sites: <a>https://download.geofabrik.de/</a>, <a>https://www.openstreetmap.org/export</a>.<br>
|
||||
2. Name your OSM-File "map.osm" and override the current "map.osm" in the project folder.
|
||||
Alternatively you could just change the "osmPath" variable in the "server.py" file.
|
||||
|
||||
## Run
|
||||
### Development Mode
|
||||
Run the "START.bat" <br/>
|
||||
OR<br/>
|
||||
run the following command in the project folder:
|
||||
`set FLASK_APP=server && flask run -p 5555 --with-threads`
|
||||
### Production Mode
|
||||
To run the server in production mode, you must deploy the server correctly on your system. <br/>
|
||||
<a href="https://flask.palletsprojects.com/en/2.0.x/tutorial/deploy/">Flask Documentation | Deploy</a>
|
||||
|
||||
## Usage
|
||||
The Server has those following URLs which all respond in a JSON-Format<br/>
|
||||
<b>If you have any problems try to delete your "map.db" and let the server regenerate it!</b>
|
||||
### ATTENTION: First Run
|
||||
When you run the server for the first time and <b>no "map.db" got generated yet</b>, it may take a while before the server starts.<br>
|
||||
Thats because the "map.db" has to get generated and this can take a <b>LONG</b> time based on your OSM-File & your computer.
|
||||
|
||||
### Get route based on ways
|
||||
`http://localhost:5555/getRoute/%fromWay%/%toWay%/` <br>
|
||||
<b>fromWay & toWay</b>: AN <a href="https://wiki.openstreetmap.org/wiki/Way">OSM wayID</a>. <br><br>
|
||||
This responses with a JSON-File this following structure: <br>
|
||||

|
||||
### Find ways based on name
|
||||
`http://localhost:5555/search/%query%/%limit%/`<br>
|
||||
<b>query</b>: The street/way name with autocomplete. Example: http://localhost:5555/search/F/%limit%/ -> gives you "Foster Street", "Friday Street" etc.
|
||||
<b>limit</b>: The limit of results which should come back.
|
||||
This responses with a JSON-File this following structure: <br>
|
||||

|
||||
|
||||
2
START.bat
Normal file
2
START.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
set FLASK_APP=server
|
||||
flask run -p 5555 --with-threads
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask==2.0.2
|
||||
flask-cors==3.0.10
|
||||
151
routing/route.py
Normal file
151
routing/route.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from sql.handle_sql import SqlHandler
|
||||
from server import osmPath, dbPath
|
||||
|
||||
# Global variables
|
||||
sql = None
|
||||
visitedWays = [] # [VisitedWay[wayId, parentWayId], [],...]
|
||||
endFound = False
|
||||
|
||||
# VisitedWays
|
||||
def pushIntoVisitedWays(way, parentWay = ""): # Appends a new VisitedWay to the visitedWay Array
|
||||
global visitedWays
|
||||
|
||||
# "new" visited way should not already exists in array (no duplicates)
|
||||
if not any(way in visitedWay for visitedWay in visitedWays):
|
||||
if parentWay != "": # check if parent is given
|
||||
visitedWays.append([way, parentWay])
|
||||
else:
|
||||
visitedWays.append([way, ""])
|
||||
|
||||
|
||||
def getOpenVisitedWays(): # Get all visited ways which have unchecked sideways.
|
||||
result = []
|
||||
openSideWays = 0 # counts how many sideways are found per vistedWay
|
||||
|
||||
for vWay in visitedWays:
|
||||
openSideWays = 0
|
||||
currentSideWays = sql.getSideWaysOfWay(vWay[0]) #
|
||||
for sideWay in currentSideWays:
|
||||
if not any(sideWay in visitedWay for visitedWay in visitedWays): # if sideway is not in visitedWays: Array
|
||||
openSideWays += 1
|
||||
|
||||
if openSideWays > 0: # if visited way has unvistited sideways
|
||||
result.append(vWay[0])
|
||||
|
||||
return result
|
||||
|
||||
def getVisitedWaybyWay(way): # Find a VisitedWay by it's way (visitedWay[0])
|
||||
result = ""
|
||||
|
||||
for visitedWay in visitedWays:
|
||||
if visitedWay[0] == way:
|
||||
result = visitedWay
|
||||
return result
|
||||
|
||||
# Reverse and build route
|
||||
def reverseRoute(endWay):
|
||||
route = []
|
||||
reversed = False
|
||||
|
||||
currentVisitedWay = getVisitedWaybyWay(int(endWay)) # Get the visitedWay object of "endway: int" from the visitedWay-array
|
||||
while reversed == False:
|
||||
if currentVisitedWay[1] != "": # if visited sideway has a parent way (visitedWays: Array[[way,parentway], ...])
|
||||
route.append(currentVisitedWay)
|
||||
currentVisitedWay = getVisitedWaybyWay(currentVisitedWay[1]) # set currentVisitedWay to parentWay
|
||||
else:
|
||||
reversed = True
|
||||
return route
|
||||
|
||||
def getRouteWayOfRoute(route):
|
||||
routeWays = []
|
||||
routeCrossingNodes = []
|
||||
counter = 0
|
||||
|
||||
for vWay in route: # Get all crossingpoints
|
||||
crossingPoint = sql.getCrossingPointOfWays(vWay[0], vWay[1])
|
||||
prevCrossingPoint = ""
|
||||
if counter > 0:
|
||||
prevCrossingPoint = routeCrossingNodes[counter-1][0]
|
||||
else:
|
||||
prevCrossingPoint = sql.getNodesOfWay(vWay[0])[0]
|
||||
|
||||
|
||||
routeWays.append({'way': vWay[0], 'startNode': prevCrossingPoint, 'endNode': crossingPoint[0]})
|
||||
routeCrossingNodes.append(crossingPoint)
|
||||
counter += 1
|
||||
return routeWays
|
||||
|
||||
def buildRoute(endWay):
|
||||
routeNodes = []
|
||||
coordinates = []
|
||||
|
||||
reversedRoute = reverseRoute(endWay)
|
||||
routeWays = getRouteWayOfRoute(reversedRoute)
|
||||
|
||||
for routeWay in routeWays:
|
||||
nodesBetweenNodes = sql.getNodesBetweenNodes(routeWay)
|
||||
for node in nodesBetweenNodes:
|
||||
routeNodes.append(node)
|
||||
|
||||
for node in routeNodes: # get all coordinates of the crossinpoints
|
||||
coordinates.append({'lon':sql.getLocOfNode(node)[0], 'lat': sql.getLocOfNode(node)[1] })
|
||||
|
||||
return coordinates
|
||||
|
||||
# Search for destination
|
||||
def checkSideWaysOfWay(way, endWay):
|
||||
global endFound
|
||||
global visitedWays
|
||||
|
||||
sideWays = sql.getSideWaysOfWay(way)
|
||||
|
||||
for sideWay in sideWays:
|
||||
|
||||
if str(sideWay) == str(endWay):
|
||||
pushIntoVisitedWays(sideWay, way)
|
||||
endFound = True
|
||||
return
|
||||
else:
|
||||
pushIntoVisitedWays(sideWay, way)
|
||||
|
||||
def searchRoute(startWay, endWay): # Main
|
||||
global sql
|
||||
global visitedWays
|
||||
global endFound
|
||||
|
||||
sql = SqlHandler(dbPath, osmPath)
|
||||
visitedWays = []
|
||||
endFound = False
|
||||
counter = 0
|
||||
|
||||
sideWays = sql.getSideWaysOfWay(startWay)
|
||||
|
||||
pushIntoVisitedWays(startWay)
|
||||
checkSideWaysOfWay(startWay, endWay)
|
||||
|
||||
for sideWay in sideWays:
|
||||
checkSideWaysOfWay(sideWay, endWay)
|
||||
|
||||
while endFound == False:
|
||||
if len(sideWays) and counter >= len(sideWays):
|
||||
counter = 0
|
||||
sideWays = getOpenVisitedWays()
|
||||
else:
|
||||
checkSideWaysOfWay(sideWays[counter], endWay)
|
||||
counter += 1
|
||||
|
||||
result = buildRoute(endWay)
|
||||
sql.conn.close()
|
||||
del sql
|
||||
return result
|
||||
|
||||
def searchRouteByCoordinates(fromWayLat, fromWayLon, toWayLat, toWayLon): # Main
|
||||
global sql
|
||||
sql = SqlHandler(dbPath, osmPath)
|
||||
fromNode = sql.getNodeByCoordinates(fromWayLat, fromWayLon)
|
||||
toNode = sql.getNodeByCoordinates(toWayLat, toWayLon)
|
||||
|
||||
fromWay = sql.getWaysOfNode(fromNode)[0]
|
||||
toWay = sql.getWaysOfNode(toNode)[0]
|
||||
del sql
|
||||
return searchRoute(fromWay, toWay)
|
||||
BIN
screenshots/findWayJson.png
Normal file
BIN
screenshots/findWayJson.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
screenshots/getRouteJson.png
Normal file
BIN
screenshots/getRouteJson.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
29
server.py
Normal file
29
server.py
Normal file
@@ -0,0 +1,29 @@
|
||||
osmPath = "map.osm"
|
||||
dbPath = "map.db"
|
||||
|
||||
from flask import Flask, jsonify
|
||||
from flask_cors import CORS, cross_origin
|
||||
from routing.route import searchRoute
|
||||
from sql.handle_sql import SqlHandler
|
||||
from utils.search import search
|
||||
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
cors = CORS(app)
|
||||
app.config['CORS_HEADERS'] = 'Content-Type: application/json'
|
||||
|
||||
# First init of database
|
||||
sql = SqlHandler(dbPath, osmPath)
|
||||
del sql
|
||||
|
||||
# http://127.0.0.1:5555/getRoute/36891304/17021250/
|
||||
@app.route("/getRoute/<fromWay>/<toWay>/")
|
||||
@cross_origin()
|
||||
def getRouteReq(fromWay, toWay):
|
||||
return jsonify(nodes=searchRoute(fromWay, toWay))
|
||||
|
||||
@app.route("/search/<wayName>/<limit>")
|
||||
def searchReq(wayName, limit):
|
||||
return jsonify(ways=search(wayName, limit))
|
||||
96
sql/handle_sql.py
Normal file
96
sql/handle_sql.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Handles the communication to the SQL-Database
|
||||
FORCE_NEW_DB = False # Forces to always regenerate the .db file
|
||||
import sqlite3, os, math
|
||||
from sql.init_sql import initSql
|
||||
|
||||
class SqlHandler:
|
||||
def __init__(self, _path, _osmPath):
|
||||
self.path = _path
|
||||
self.osmPath = _osmPath
|
||||
|
||||
if FORCE_NEW_DB and os.path.isfile(self.path):
|
||||
os.remove(self.path)
|
||||
|
||||
if not os.path.isfile(self.path):
|
||||
initSql(self.path, self.osmPath)
|
||||
|
||||
self.connectToDatabase()
|
||||
|
||||
def connectToDatabase(self):
|
||||
self.conn = sqlite3.connect(self.path)
|
||||
self.cur = self.conn.cursor()
|
||||
|
||||
def getNodesOfWay(self, wayId):
|
||||
result = []
|
||||
|
||||
for row in self.cur.execute('SELECT nodeId FROM nodes LEFT JOIN node_way ON nodes.id = node_way.nodeId WHERE node_way.wayId=' + str(wayId)):
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getWaysOfNode(self, nodeId):
|
||||
result = []
|
||||
|
||||
for row in self.cur.execute('SELECT wayId FROM ways LEFT JOIN node_way ON ways.id = node_way.wayId WHERE node_way.nodeId=' + str(nodeId)):
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getSideWaysOfWay(self, wayId):
|
||||
result = []
|
||||
|
||||
for row in self.cur.execute('SELECT sideWayId FROM way_sideway LEFT JOIN ways ON ways.id=way_sideway.wayId WHERE ways.id=' + str(wayId)):
|
||||
if not row[0] in result:
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getCrossingPointOfWays(self, firstWay, secondWay):
|
||||
result = []
|
||||
|
||||
for row in self.cur.execute('SELECT node FROM crossingpoints WHERE firstWay=? AND secondWay=?', (str(firstWay), str(secondWay))):
|
||||
if not row[0] in result:
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getLocOfNode(self, node):
|
||||
for row in self.cur.execute('SELECT lon, lat FROM nodes WHERE nodes.id="' + str(node) + '"'):
|
||||
return [row[0], row[1]]
|
||||
|
||||
def getNodeByCoordinates(self, lat, lon):
|
||||
for row in self.cur.execute('SELECT id FROM nodes WHERE nodes.lon=? AND nodes.lat=?', (lon, lat)):
|
||||
return row[0]
|
||||
return
|
||||
|
||||
def getNameOfWay(self, wayId):
|
||||
for row in self.cur.execute('SELECT name FROM ways WHERE id="' + str(wayId) + '"'):
|
||||
return row[0]
|
||||
|
||||
def getWaysByNameWildcard(self, name):
|
||||
result = []
|
||||
for row in self.cur.execute('SELECT id, name FROM ways WHERE name LIKE "' + name + '%"'):
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getNodesBetweenNodes(self, routeWay):
|
||||
nodes = []
|
||||
|
||||
startNodeIndex = ""
|
||||
endNodeIndex = ""
|
||||
|
||||
for row in self.cur.execute('SELECT id FROM node_way WHERE wayId=? AND nodeId=?', (routeWay['way'], routeWay['startNode'])):
|
||||
startNodeIndex = int(row[0])
|
||||
|
||||
for row in self.cur.execute('SELECT id FROM node_way WHERE wayId=? AND nodeId=?', (routeWay['way'], routeWay['endNode'])):
|
||||
endNodeIndex = int(row[0])
|
||||
|
||||
if startNodeIndex > endNodeIndex:
|
||||
tempNodes = []
|
||||
for row in self.cur.execute('SELECT nodeId FROM node_way WHERE id <=? AND id >=?', (startNodeIndex, endNodeIndex)):
|
||||
tempNodes.append(row[0])
|
||||
nodes = tempNodes[::-1]
|
||||
else:
|
||||
for row in self.cur.execute('SELECT nodeId FROM node_way WHERE id >=? AND id <=?', (startNodeIndex, endNodeIndex)):
|
||||
nodes.append(row[0])
|
||||
|
||||
return nodes
|
||||
|
||||
def __del__(self):
|
||||
self.conn.close()
|
||||
153
sql/init_sql.py
Normal file
153
sql/init_sql.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# Intilializes/Creates the database with all tables and table contents
|
||||
import sqlite3, os
|
||||
|
||||
def createNode(conn, line):
|
||||
nodeId = str(line.split('id="')[1].split('"')[0])
|
||||
nodeLon = str(line.split('lon="')[1].split('"')[0])
|
||||
nodeLat = str(line.split('lat="')[1].split('"')[0])
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute('INSERT INTO nodes(id, lon, lat) VALUES (?, ?, ?)', (nodeId, nodeLon, nodeLat))
|
||||
|
||||
def createWay(conn, lines):
|
||||
wayId = ""
|
||||
wayNodes = []
|
||||
wayName = ""
|
||||
isHighWay = False
|
||||
|
||||
|
||||
for line in lines:
|
||||
if "<way" in line:
|
||||
wayId = str(line.split('id="')[1].split('"')[0])
|
||||
elif "<nd ref=" in line:
|
||||
wayNodes.append(str(line.split('ref="')[1].split('"')[0]))
|
||||
elif '<tag k="name" ' in line:
|
||||
wayName = str(line.split('v="')[1].split('"')[0])
|
||||
elif '<tag k="highway" ' in line:
|
||||
isHighWay = True
|
||||
|
||||
if isHighWay:
|
||||
cur = conn.cursor()
|
||||
cur.execute('INSERT INTO ways(id, name) VALUES (?, ?)', (wayId, wayName))
|
||||
|
||||
for nodeId in wayNodes:
|
||||
createNodeWayJunction(conn, wayId, nodeId)
|
||||
|
||||
def createNodeWayJunction(conn, wayId, nodeId):
|
||||
cur = conn.cursor()
|
||||
cur.execute('INSERT INTO node_way(wayId, nodeId) VALUES (?, ?)', (wayId, nodeId))
|
||||
|
||||
def createWaySideWayJunction(conn, wayId, sideWayId):
|
||||
cur = conn.cursor()
|
||||
cur.execute('INSERT INTO way_sideway(wayId, sideWayId) VALUES (?, ?)', (wayId, sideWayId))
|
||||
|
||||
def parse_way_sideWay_junction(conn):
|
||||
print("Parse sideways")
|
||||
cur = conn.cursor()
|
||||
|
||||
for wayRow in cur.execute('SELECT id FROM ways'):
|
||||
currentWayId = wayRow[0]
|
||||
nodesOfWay = getNodesOfWay(conn, currentWayId)
|
||||
|
||||
for node in nodesOfWay:
|
||||
waysOfNode = getWaysOfNode(conn, node)
|
||||
if len(waysOfNode) > 1:
|
||||
for sideWay in waysOfNode:
|
||||
if sideWay != currentWayId:
|
||||
createCrossingpoint(conn, node, sideWay, currentWayId)
|
||||
createWaySideWayJunction(conn, currentWayId, sideWay)
|
||||
conn.commit()
|
||||
|
||||
def parse_osm_to_sql(conn, osm_path):
|
||||
print("Parsing nodes and ways of the OSM file into the database.")
|
||||
file = open(osm_path, "r", encoding="utf-8")
|
||||
wayLines = []
|
||||
|
||||
for line in file.readlines():
|
||||
if "<node" in line:
|
||||
createNode(conn, line)
|
||||
elif "<way " in line:
|
||||
wayLines.append(line)
|
||||
elif "</way>" in line:
|
||||
createWay(conn, wayLines)
|
||||
wayLines = []
|
||||
elif len(wayLines) > 0:
|
||||
wayLines.append(line)
|
||||
|
||||
conn.commit()
|
||||
|
||||
def createCrossingpoint(conn, node, currentWayId, sideWay):
|
||||
cur = conn.cursor()
|
||||
cur.execute('INSERT INTO crossingpoints(firstWay, secondWay, node) VALUES (?, ?, ?)', (sideWay, currentWayId, node))
|
||||
|
||||
|
||||
def createDatabase(path):
|
||||
print("Generate database structure")
|
||||
conn = sqlite3.connect(path)
|
||||
cur = conn.cursor()
|
||||
cur.execute('''CREATE TABLE "nodes" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"lon" TEXT NOT NULL,
|
||||
"lat" TEXT NOT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);''')
|
||||
|
||||
cur.execute('''CREATE TABLE "ways" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"name" TEXT,
|
||||
PRIMARY KEY("id")
|
||||
);''')
|
||||
|
||||
cur.execute('''CREATE TABLE "node_way" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"wayId" INTEGER NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id"),
|
||||
FOREIGN KEY("wayId") REFERENCES "ways"("id"),
|
||||
FOREIGN KEY("nodeId") REFERENCES "nodes"("id")
|
||||
);''')
|
||||
|
||||
cur.execute('''CREATE TABLE "way_sideway" (
|
||||
"wayId" INTEGER NOT NULL,
|
||||
"sideWayId" INTEGER NOT NULL,
|
||||
FOREIGN KEY("wayId") REFERENCES "ways"("id"),
|
||||
FOREIGN KEY("sideWayId") REFERENCES "ways"("id")
|
||||
);''')
|
||||
|
||||
cur.execute('''CREATE TABLE "crossingpoints" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"node" INTEGER NOT NULL,
|
||||
"firstWay" INTEGER NOT NULL,
|
||||
"secondWay" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id"),
|
||||
FOREIGN KEY("node") REFERENCES "nodes"("id"),
|
||||
FOREIGN KEY("firstWay") REFERENCES "ways"("id"),
|
||||
FOREIGN KEY("secondWay") REFERENCES "ways"("id")
|
||||
);''')
|
||||
|
||||
return conn
|
||||
|
||||
# Getters
|
||||
def getNodesOfWay(conn, wayId):
|
||||
cur = conn.cursor()
|
||||
result = []
|
||||
|
||||
for row in cur.execute('SELECT nodeId FROM nodes LEFT JOIN node_way ON nodes.id = node_way.nodeId WHERE node_way.wayId=' + str(wayId)):
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
def getWaysOfNode(conn, nodeId):
|
||||
cur = conn.cursor()
|
||||
result = []
|
||||
|
||||
for row in cur.execute('SELECT wayId FROM ways LEFT JOIN node_way ON ways.id = node_way.wayId WHERE node_way.nodeId=' + str(nodeId)):
|
||||
result.append(row[0])
|
||||
return result
|
||||
|
||||
# INIT
|
||||
def initSql(path, osmPath):
|
||||
print("Initializing database. This may take a while.")
|
||||
conn = createDatabase(path)
|
||||
parse_osm_to_sql(conn, osmPath)
|
||||
parse_way_sideWay_junction(conn)
|
||||
print("Done: Initializing database")
|
||||
24
utils/search.py
Normal file
24
utils/search.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sql.handle_sql import SqlHandler
|
||||
from server import osmPath, dbPath
|
||||
|
||||
# Initalize global variables
|
||||
sql = ""
|
||||
|
||||
def search(wayName, limit):
|
||||
global sql
|
||||
formattedWays = []
|
||||
limit = int(limit)
|
||||
sql = SqlHandler(dbPath, osmPath)
|
||||
ways = sql.getWaysByNameWildcard(wayName)
|
||||
|
||||
for way in ways:
|
||||
formattedWays.append({'id': way, 'name': sql.getNameOfWay(way) + " (" + str(way) + ")"})
|
||||
|
||||
|
||||
|
||||
del sql
|
||||
|
||||
if len(formattedWays) >= limit:
|
||||
return formattedWays[:limit]
|
||||
else:
|
||||
return formattedWays
|
||||
Reference in New Issue
Block a user