rewrite it in Haskell
This commit is contained in:
parent
e816bce69c
commit
02b29877f4
11 changed files with 609 additions and 7016 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
|||
build/*
|
||||
dist-newstyle/*
|
||||
|
|
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Revision history for bahnhof.name
|
||||
|
||||
## 0.2.0.0 -- 2023-11-14
|
||||
|
||||
* Rewrite it in Haskell
|
||||
* New feature: query the overpass API for platform information
|
||||
|
||||
## 0.1.0.0 -- 2023-05-11
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
||||
* Basic search of stations, implemented in gleam
|
287
LICENSE
Normal file
287
LICENSE
Normal file
|
@ -0,0 +1,287 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
14
Readme.md
Normal file
14
Readme.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# bahnhof.name
|
||||
|
||||
This repository contains the sources for [bahnhof.name](https://bahnhof.name),
|
||||
a simple web server which gives helpful information about railway stations
|
||||
in Germany, from both static files & by querying osm for information which
|
||||
are harder/impossible to get via Open Data from DB AG directly.
|
||||
|
||||
## A note on packaging
|
||||
|
||||
Building and packaging this is generally simple as long as you use `cabal`;
|
||||
however, the `fuzzyfind` package seems a little out of date. Yet it compiles
|
||||
fine even with newer dependencies, so you may want to disregard its version
|
||||
bounds if that makes things easier for you. An example package done with Nix can
|
||||
be found on [my nixfiles](https://stuebinm.eu/git/nixfiles/tree/pkgs/overlay.nix).
|
255
app/Main.hs
Normal file
255
app/Main.hs
Normal file
|
@ -0,0 +1,255 @@
|
|||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Char
|
||||
import Data.Csv hiding (lookup)
|
||||
import Data.Function ((&))
|
||||
import Data.Functor ((<&>))
|
||||
import Data.FuzzySet
|
||||
import Data.List
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Ord
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock
|
||||
import Data.Tuple (swap)
|
||||
import Data.Vector (Vector)
|
||||
import qualified Data.Vector as V
|
||||
import qualified Network.HTTP.Client as Client
|
||||
import qualified Network.HTTP.Client.OpenSSL as Client
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import Network.Wai.Middleware.RequestLogger
|
||||
import Text.FuzzyFind (Alignment (score),
|
||||
bestMatch)
|
||||
|
||||
csvOptions, tsvOptions :: DecodeOptions
|
||||
csvOptions = defaultDecodeOptions { decDelimiter = fromIntegral (ord ';') }
|
||||
tsvOptions = defaultDecodeOptions { decDelimiter = fromIntegral (ord '\t') }
|
||||
|
||||
data MatchResult a b
|
||||
= Exact a
|
||||
| Fuzzy b
|
||||
| None
|
||||
deriving Show
|
||||
|
||||
findStationName :: T.Text -> FuzzySet -> MatchResult (Double, Text) (Double, Text)
|
||||
findStationName query set = case sorted of
|
||||
[exact] -> Exact exact
|
||||
_ -> case maybeHbf of
|
||||
station:_ -> Fuzzy station
|
||||
_ -> case results of
|
||||
station:_ -> Fuzzy station
|
||||
_ -> None
|
||||
where
|
||||
sorted = results
|
||||
& fmap (\(_, match) -> (fromIntegral . maybe 0 score . bestMatch (T.unpack query) $ T.unpack match, match))
|
||||
& sortOn (Down . fst)
|
||||
results = get set query
|
||||
maybeHbf = filter (T.isInfixOf "Hbf" . snd) sorted
|
||||
|
||||
data Platform = Platform
|
||||
{ osmType :: Text
|
||||
, osmId :: Text
|
||||
, ref :: Maybe Text
|
||||
, localRef :: Maybe Text
|
||||
} deriving Show
|
||||
|
||||
instance FromRecord Platform where
|
||||
parseRecord v =
|
||||
Platform <$>
|
||||
v .! 0 <*>
|
||||
v .! 1 <*>
|
||||
v .! 2 <*>
|
||||
v .! 3
|
||||
|
||||
data Answer
|
||||
= Redirect Text
|
||||
| Plaintext Text
|
||||
| Html Text
|
||||
| Notfound
|
||||
| Unimplemented
|
||||
|
||||
maybeAnswer :: (a -> Answer) -> Maybe a -> Answer
|
||||
maybeAnswer = maybe Notfound
|
||||
|
||||
|
||||
newtype Ril100 = Ril100 { unRil100 :: Text }
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
data AppData = AppData
|
||||
{ ril100map :: DoubleMap Ril100 Text
|
||||
, leitpunktMap :: DoubleMap Ril100 Text
|
||||
, ril100set :: FuzzySet
|
||||
, ril100BaseUrl :: Text
|
||||
, leitpunktBaseUrl :: Text
|
||||
, clientManager :: Client.Manager
|
||||
, platformCache :: TVar (Map Ril100 (UTCTime, Answer))
|
||||
, cacheTime :: NominalDiffTime
|
||||
}
|
||||
|
||||
app :: AppData -> Application
|
||||
app AppData{..} request respond = mkAnswer >>= (respond . toResponse)
|
||||
where
|
||||
mkAnswer :: IO Answer
|
||||
mkAnswer = case pathInfo request of
|
||||
[] -> pure helptext
|
||||
["favicon.ico"] -> pure Notfound
|
||||
["cache"] -> do
|
||||
cache <- readTVarIO platformCache
|
||||
now <- getCurrentTime
|
||||
M.toList cache
|
||||
& fmap (\(ril100, (age, _)) -> (T.pack . show) (unRil100 ril100, now `diffUTCTime` age))
|
||||
& T.unlines
|
||||
& (pure . Plaintext)
|
||||
[query]
|
||||
| T.all isUpper query && host `elem` ["leitpunkt"]
|
||||
-> lookupName query leitpunktMap
|
||||
>>= (`lookupCode` ril100map)
|
||||
& maybeAnswer Plaintext & pure
|
||||
| T.all isUpper query
|
||||
-> lookupCode (Ril100 query) ril100map
|
||||
& maybeAnswer Plaintext & pure
|
||||
| host `elem` ["leitpunkt"]
|
||||
-> pure $ case findStationName query ril100set of
|
||||
None -> Notfound
|
||||
Exact (_,match) -> lookupName match ril100map
|
||||
>>= (`lookupCode` leitpunktMap)
|
||||
& maybeAnswer Plaintext
|
||||
Fuzzy (_,match) -> Redirect (leitpunktBaseUrl <> "/" <> match)
|
||||
| otherwise
|
||||
-> pure $ case findStationName query ril100set of
|
||||
None -> Notfound
|
||||
Exact (_,match) -> lookupName match ril100map
|
||||
& maybeAnswer (Plaintext . unRil100)
|
||||
Fuzzy (_,match) -> Redirect (ril100BaseUrl <> "/" <> match)
|
||||
[query, segment] | segment `elem` ["gleis", "track", "tracks", "gleise", "platform", "platforms", "fetch"]
|
||||
-> case queriedRil100 query of
|
||||
None -> pure Notfound
|
||||
Fuzzy url -> pure (Redirect url)
|
||||
Exact ril100 -> do
|
||||
maybeCache <- readTVarIO platformCache <&> M.lookup ril100
|
||||
now <- getCurrentTime
|
||||
case maybeCache of
|
||||
Just (age, answer)
|
||||
| now `diffUTCTime` age < cacheTime && segment /= "fetch" -> pure answer
|
||||
_ -> do
|
||||
let overpassQuery = " \
|
||||
\[out:csv(::type, ::id, ref, local_ref;false)][timeout:25];\n\
|
||||
\node[~\"railway:ref|railway:ref:parent\"~\"^"<>encodeUtf8 (unRil100 ril100)<>"$\"][operator~\"^(DB |Deutsch)\"];\n\
|
||||
\rel[public_transport~\"stop_area|stop_area_group\"](bn) -> .a;\n\
|
||||
\rel[public_transport~\"stop_area|stop_area_group\"](br.a) -> .b;\n\
|
||||
\(.a;.b;);\n\
|
||||
\nwr[railway=platform](>>);\n\
|
||||
\out;\n"
|
||||
let req = "https://overpass-api.de/api/interpreter"
|
||||
{ Client.requestBody = Client.RequestBodyBS overpassQuery
|
||||
, Client.method = "POST"}
|
||||
putStrLn $ "looking up platforms for " <> show ril100
|
||||
response <- Client.httpLbs req clientManager
|
||||
case decodeWith tsvOptions NoHeader (Client.responseBody response) of
|
||||
Left _ -> pure Notfound
|
||||
Right (platforms :: Vector Platform) -> do
|
||||
let answer = Html $ T.concat (renderPlatform <$> V.toList platforms)
|
||||
now <- getCurrentTime
|
||||
atomically $ do
|
||||
cache <- readTVar platformCache
|
||||
writeTVar platformCache (M.insert ril100 (now, answer) cache)
|
||||
pure answer
|
||||
where
|
||||
getRef (Just ref) _ = Just ref
|
||||
getRef Nothing (Just ref) = Just ref
|
||||
getRef _ _ = Nothing
|
||||
renderPlatform Platform{..} = case getRef ref localRef of
|
||||
Just ref -> "<a href=\"https://osm.org/"<>osmType<>"/"<>osmId<>"\">"<>ref<>"</a><br>"
|
||||
Nothing -> ""
|
||||
_ -> pure Notfound
|
||||
queriedRil100 :: Text -> MatchResult Ril100 Text
|
||||
queriedRil100 query = if
|
||||
| T.all isUpper query && host `elem` ["leitpunkt"]
|
||||
-> lookupName query leitpunktMap
|
||||
& maybe None Exact
|
||||
| T.all isUpper query
|
||||
-> Exact (Ril100 query)
|
||||
| host `elem` ["leitpunkt"]
|
||||
-> case findStationName query ril100set of
|
||||
None -> None
|
||||
Exact (_,match) -> lookupName match ril100map
|
||||
& maybe None Exact
|
||||
Fuzzy (_,match) -> Fuzzy (leitpunktBaseUrl <> "/" <> match)
|
||||
| otherwise
|
||||
-> case findStationName query ril100set of
|
||||
None -> None
|
||||
Exact (_,match) -> lookupName match ril100map
|
||||
& maybe None Exact
|
||||
Fuzzy (_,match) -> Fuzzy (ril100BaseUrl <> "/" <> match)
|
||||
helptext = Plaintext "no help yet"
|
||||
host = head (BS.split (fromIntegral (ord '.')) rawHost)
|
||||
where rawHost = case lookup "x-forwarded-host" $ requestHeaders request of
|
||||
Nothing -> fromMaybe "" $ requestHeaderHost request
|
||||
Just some -> some
|
||||
toResponse :: Answer -> Response
|
||||
toResponse ans = case ans of
|
||||
Redirect uri -> responseLBS
|
||||
status302 [("Location", encodeUtf8 uri)] ""
|
||||
Plaintext msg -> responseLBS
|
||||
status200 (mkHeaders "text/plain") (LB.fromStrict $ encodeUtf8 msg)
|
||||
Html markup -> responseLBS
|
||||
status200 (mkHeaders "text/html") (LB.fromStrict $ encodeUtf8 markup)
|
||||
Notfound -> responseLBS
|
||||
status404 (mkHeaders "text/plain") "??"
|
||||
Unimplemented -> responseLBS
|
||||
status404 (mkHeaders "text/plain") "Sorry, this is still under construction"
|
||||
mkHeaders contentType =
|
||||
[ ("Content-Type", contentType<>"; charset=utf8")
|
||||
, ("x-data-by", "CC-BY 4.0 DB Netz AG https://data.deutschebahn.com/dataset/data-betriebsstellen.html")
|
||||
, ("x-data-by", "OpenStreetMap Contributors https://www.openstreetmap.org/copyright/")
|
||||
, ("x-sources-at", "https://stuebinm.eu/git/bahnhof.name")
|
||||
]
|
||||
|
||||
|
||||
data DoubleMap code long = DoubleMap { there :: Map code long, back :: Map long code }
|
||||
lookupCode :: Ord code => code -> DoubleMap code long -> Maybe long
|
||||
lookupCode code maps = M.lookup code (there maps)
|
||||
lookupName :: Ord long => long -> DoubleMap code long -> Maybe code
|
||||
lookupName name maps = M.lookup name (back maps)
|
||||
mkDoubleMap :: (Ord code, Ord long) => Vector (code, long) -> DoubleMap code long
|
||||
mkDoubleMap tuplesvec = DoubleMap (M.fromList tuples) (M.fromList (fmap swap tuples))
|
||||
where tuples = V.toList tuplesvec
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
Right (betriebsstellen :: V.Vector [Text]) <-
|
||||
LB.readFile "data/DBNetz-Betriebsstellenverzeichnis-Stand2021-10.csv"
|
||||
<&> decodeWith csvOptions HasHeader
|
||||
<&> fmap (V.filter (\line -> line !! 4 `notElem` ["BUSH", "LGR", "ÜST", "BFT", "ABZW", "BK", "AWAN", "ANST", "LGR"]))
|
||||
Right (leitpunkte :: V.Vector [Text]) <-
|
||||
LB.readFile "data/leitpunkte.csv"
|
||||
<&> decodeWith csvOptions HasHeader
|
||||
|
||||
putStrLn "building Index ..."
|
||||
let ril100set = addMany (emptySet 5 6 False) (V.toList (V.map (!! 2) betriebsstellen))
|
||||
putStrLn (seq ril100set "done")
|
||||
|
||||
let ril100map = mkDoubleMap $ fmap (\line -> (Ril100 (line !! 1), line !! 2)) betriebsstellen
|
||||
let leitpunktMap = mkDoubleMap $ fmap (\line -> (Ril100 (line !! 2), line !! 0)) leitpunkte
|
||||
|
||||
let ril100BaseUrl = "https://ril100.bahnhof.name"
|
||||
let leitpunktBaseUrl = "https://leitpunkt.bahnhof.name"
|
||||
let cacheTime = 3600 * 24 * 7 -- one week
|
||||
platformCache <- newTVarIO mempty
|
||||
|
||||
Client.withOpenSSL $ do
|
||||
clientManager <- Client.newOpenSSLManager
|
||||
putStrLn "Starting Server"
|
||||
run 8080 (logStdoutDev (app AppData{..}))
|
41
bahnhof-name.cabal
Normal file
41
bahnhof-name.cabal
Normal file
|
@ -0,0 +1,41 @@
|
|||
cabal-version: 3.0
|
||||
name: bahnhof-name
|
||||
-- PVP summary: +-+------- breaking API changes
|
||||
-- | | +----- non-breaking API additions
|
||||
-- | | | +--- code changes with no API change
|
||||
version: 0.2.0.0
|
||||
synopsis: Serve information on German railway stations
|
||||
license: EUPL-1.2
|
||||
license-file: LICENSE
|
||||
author: stuebinm
|
||||
maintainer: stuebinm@disroot.org
|
||||
build-type: Simple
|
||||
extra-doc-files: CHANGELOG.md
|
||||
extra-source-files: data/*
|
||||
|
||||
common warnings
|
||||
ghc-options: -Wall
|
||||
|
||||
executable bahnhof-name
|
||||
import: warnings
|
||||
|
||||
main-is: Main.hs
|
||||
hs-source-dirs: app
|
||||
build-depends: base ^>=4.16.4.0
|
||||
, fuzzyset
|
||||
, fuzzyfind
|
||||
, text
|
||||
, vector
|
||||
, cassava
|
||||
, bytestring
|
||||
, wai
|
||||
, wai-extra
|
||||
, warp
|
||||
, http-types
|
||||
, containers
|
||||
, http-client
|
||||
, http-client-openssl
|
||||
, stm
|
||||
, time
|
||||
default-language: GHC2021
|
||||
ghc-options: -threaded -with-rtsopts=--nonmoving-gc
|
6518
data/platforms.tsv
6518
data/platforms.tsv
File diff suppressed because it is too large
Load diff
21
gleam.toml
21
gleam.toml
|
@ -1,21 +0,0 @@
|
|||
name = "bahnhofname"
|
||||
version = "0.1.0"
|
||||
|
||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||
# your project to the Hex package manager.
|
||||
#
|
||||
# licences = ["Apache-2.0"]
|
||||
# description = "A Gleam library..."
|
||||
# repository = { type = "github", user = "username", repo = "project" }
|
||||
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = "~> 0.19"
|
||||
gleam_http = "~> 3.0"
|
||||
mist = "~> 0.4"
|
||||
gleam_hackney = "~> 1.0"
|
||||
haystack = "~> 0.1"
|
||||
gleam_erlang = "~> 0.22"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = "~> 0.6"
|
|
@ -1,34 +0,0 @@
|
|||
# This file was generated by Gleam
|
||||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "certifi", version = "2.12.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "EE68D85DF22E554040CDB4BE100F33873AC6051387BAF6A8F6CE82272340FF1C" },
|
||||
{ name = "decimal", version = "2.1.1", build_tools = ["mix"], requirements = [], otp_app = "decimal", source = "hex", outer_checksum = "53CFE5F497ED0E7771AE1A475575603D77425099BA5FAEF9394932B35020FFCC" },
|
||||
{ name = "gleam_erlang", version = "0.22.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "367D8B41A7A86809928ED1E7E55BFD0D46D7C4CF473440190F324AFA347109B4" },
|
||||
{ name = "gleam_hackney", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "hackney"], otp_app = "gleam_hackney", source = "hex", outer_checksum = "CA69AD9061C4A8775A7BD445DE33ECEFD87379AF8E5B028F3DD0216BECA5DD0B" },
|
||||
{ name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" },
|
||||
{ name = "gleam_otp", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "ED7381E90636E18F5697FD7956EECCA635A3B65538DC2BE2D91A38E61DCE8903" },
|
||||
{ name = "gleam_stdlib", version = "0.31.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6D1BC5B4D4179B9FEE866B1E69FE180AC2CE485AD90047C0B32B2CA984052736" },
|
||||
{ name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" },
|
||||
{ name = "glisten", version = "0.9.1", build_tools = ["gleam"], requirements = ["gleam_otp", "gleam_erlang", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "91809C44C52456D96C8317A19246DE1C06ED494C40D282CD9380565E879A52C4" },
|
||||
{ name = "hackney", version = "1.20.1", build_tools = ["rebar3"], requirements = ["mimerl", "certifi", "ssl_verify_fun", "metrics", "idna", "unicode_util_compat", "parse_trans"], otp_app = "hackney", source = "hex", outer_checksum = "FE9094E5F1A2A2C0A7D10918FEE36BFEC0EC2A979994CFF8CFE8058CD9AF38E3" },
|
||||
{ name = "haystack", version = "0.1.0", build_tools = ["mix"], requirements = ["stemmer", "jason"], otp_app = "haystack", source = "hex", outer_checksum = "27A582513EF933C1B11345B96F8D41EE137D03B25312BD85068FFE8FEC503635" },
|
||||
{ name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" },
|
||||
{ name = "jason", version = "1.4.1", build_tools = ["mix"], requirements = ["decimal"], otp_app = "jason", source = "hex", outer_checksum = "FBB01ECDFD565B56261302F7E1FCC27C4FB8F32D56EAB74DB621FC154604A7A1" },
|
||||
{ name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" },
|
||||
{ name = "mimerl", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323" },
|
||||
{ name = "mist", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang", "gleam_http", "gleam_otp", "glisten"], otp_app = "mist", source = "hex", outer_checksum = "7CDD0396D9A556F1069D83E9AF2B24388AAC478B9B4846615C6D4797E1D3C6A3" },
|
||||
{ name = "parse_trans", version = "3.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A" },
|
||||
{ name = "ssl_verify_fun", version = "1.1.7", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8" },
|
||||
{ name = "stemmer", version = "1.1.0", build_tools = ["mix"], requirements = [], otp_app = "stemmer", source = "hex", outer_checksum = "0CB5FAF73476B84500E371FF39FD9A494F60AB31D991689C1CD53B920556228F" },
|
||||
{ name = "unicode_util_compat", version = "0.7.0", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
gleam_erlang = { version = "~> 0.22" }
|
||||
gleam_hackney = { version = "~> 1.0" }
|
||||
gleam_http = { version = "~> 3.0" }
|
||||
gleam_stdlib = { version = "~> 0.19" }
|
||||
gleeunit = { version = "~> 0.6" }
|
||||
haystack = { version = "~> 0.1" }
|
||||
mist = { version = "~> 0.4" }
|
|
@ -1,430 +0,0 @@
|
|||
import gleam/http/response.{Response}
|
||||
import gleam/http/request.{Request, get_header}
|
||||
import gleam/http.{Get}
|
||||
import gleam/bit_builder
|
||||
import gleam/erlang/process
|
||||
import gleam/erlang/atom
|
||||
import gleam/erlang/file
|
||||
import gleam/io
|
||||
import gleam/int
|
||||
import gleam/string
|
||||
import gleam/bit_string
|
||||
import gleam/list
|
||||
import gleam/map.{Map}
|
||||
import gleam/uri
|
||||
import gleam/hackney
|
||||
import gleam/pair.{swap}
|
||||
import gleam/result
|
||||
import mist
|
||||
|
||||
const ds100_domain = "ds100.bahnhof.name"
|
||||
|
||||
const ril100_domain = "ril100.bahnhof.name"
|
||||
|
||||
const leitpunkt_domain = "leitpunkt.bahnhof.name"
|
||||
|
||||
const domain = "bahnhof.name"
|
||||
|
||||
const proto = "https://"
|
||||
|
||||
type Index
|
||||
|
||||
type Field
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index", "new")
|
||||
fn index_new(a: atom.Atom) -> Index
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index", "ref")
|
||||
fn index_ref(a: Index, b: Field) -> Index
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index", "field")
|
||||
fn index_field(a: Index, b: Field) -> Index
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index.Field", "term")
|
||||
fn field_term(a: String) -> Field
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index.Field", "new")
|
||||
fn field_new(a: String) -> Field
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Index", "add")
|
||||
fn index_add(a: Index, b: List(a)) -> Index
|
||||
|
||||
@external(erlang, "Elixir.IO", "inspect")
|
||||
pub fn inspect(a: a) -> a
|
||||
|
||||
type Query
|
||||
|
||||
type Clause
|
||||
|
||||
type Expression
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query", "new")
|
||||
fn query_new() -> Query
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query", "clause")
|
||||
fn query_clause(a: Query, b: Clause) -> Query
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query", "run")
|
||||
fn query_run(a: Query, b: Index) -> List(Map(atom.Atom, String))
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query.Clause", "new")
|
||||
fn clause_new(a: atom.Atom) -> Clause
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query.Clause", "expressions")
|
||||
fn query_expressions(a: Clause, b: List(Expression)) -> Clause
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Query.Expression", "new")
|
||||
fn query_expression_new(
|
||||
a: atom.Atom,
|
||||
b: List(#(atom.Atom, String)),
|
||||
) -> Expression
|
||||
|
||||
@external(erlang, "Elixir.Haystack.Tokenizer", "tokenize")
|
||||
fn tokenize(a: String) -> List(Map(atom.Atom, String))
|
||||
|
||||
type IdKind {
|
||||
DS100
|
||||
Leitpunkt
|
||||
}
|
||||
|
||||
type Matched(t) {
|
||||
Exact(t)
|
||||
Fuzzy(t, t)
|
||||
Failed
|
||||
}
|
||||
|
||||
fn unpercent(encoded: String) -> String {
|
||||
let #([head], chunks) =
|
||||
encoded
|
||||
|> string.split(on: "%")
|
||||
|> list.split(at: 1)
|
||||
|
||||
let assert Ok(res) =
|
||||
chunks
|
||||
|> list.map(fn(str) {
|
||||
case string.length(str) < 2 {
|
||||
True -> bit_string.from_string(str)
|
||||
False -> {
|
||||
let assert Ok(codepoint) =
|
||||
str
|
||||
|> string.slice(at_index: 0, length: 2)
|
||||
|> int.base_parse(16)
|
||||
<<codepoint:8, string.drop_left(str, 2):utf8>>
|
||||
}
|
||||
}
|
||||
})
|
||||
|> list.prepend(bit_string.from_string(head))
|
||||
|> bit_string.concat
|
||||
|> bit_string.to_string
|
||||
|> result.map(fn(str) { string.replace(str, "_", " ") })
|
||||
res
|
||||
}
|
||||
|
||||
/// Looks up a query in a Map by exact value, no fuzzy matching.
|
||||
fn lookup_exact(query: String, lookup: Map(String, String)) -> #(Int, String) {
|
||||
case map.get(lookup, query) {
|
||||
Ok(result) -> #(200, result)
|
||||
_ -> #(404, "??")
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up a station by its name, with fuzzy matching.
|
||||
fn lookup_fuzzy(
|
||||
query: String,
|
||||
kind: IdKind,
|
||||
fuzzy: fn(String, IdKind) -> Matched(String),
|
||||
) -> #(Int, String) {
|
||||
case fuzzy(query, kind) {
|
||||
Exact(res) -> #(200, res)
|
||||
Fuzzy(res, _) -> #(302, res)
|
||||
Failed -> #(404, "??")
|
||||
}
|
||||
}
|
||||
|
||||
fn if_not(res: #(Int, t), fallback: fn() -> #(Int, t)) -> #(Int, t) {
|
||||
inspect(case res {
|
||||
#(200, _) -> res
|
||||
_ -> fallback()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn lookup_station(
|
||||
request: Request(t),
|
||||
ds100_to_name: Map(String, String),
|
||||
leitpunkt_to_name: Map(String, String),
|
||||
lookup_platform: fn(String) -> String,
|
||||
fuzzy: fn(String, IdKind) -> Matched(String),
|
||||
) -> Response(mist.ResponseData) {
|
||||
let #(#(code, text), is_html) = case request {
|
||||
// blackhole favicon.ico requests instead of using the index
|
||||
Request(method: Get, path: "/favicon.ico", ..) -> #(#(404, ""), False)
|
||||
Request(method: Get, path: "/help", ..)
|
||||
| Request(method: Get, path: "/", ..) -> #(#(
|
||||
200,
|
||||
"ril100 → Name: " <> proto <> ril100_domain <> "/HG\n" <>
|
||||
"Name → ril100: " <> proto <> ril100_domain <> "/Göttingen\n\n" <>
|
||||
"Leitpunkt → Name: " <> proto <> leitpunkt_domain <> "/GOE\n" <>
|
||||
"Name → Leitpunkt: " <> proto <> leitpunkt_domain <> "/Göttingen\n\n" <>
|
||||
"Fuzzy:" <> proto <> domain <> "/...",
|
||||
), False)
|
||||
Request(method: Get, path: "/" <> path, ..) -> {
|
||||
let raw_query = unpercent(path)
|
||||
let show_platforms = string.ends_with(raw_query, "/gleis")
|
||||
|| string.ends_with(raw_query, "/bahnsteig")
|
||||
|| string.ends_with(raw_query, "/platforms")
|
||||
|| string.ends_with(raw_query, "/tracks")
|
||||
|| string.ends_with(raw_query, "/platform")
|
||||
|| string.ends_with(raw_query, "/track")
|
||||
let query = raw_query
|
||||
|> string.replace("/gleis","")
|
||||
|> string.replace("/bahnsteig","")
|
||||
|> string.replace("/platforms","")
|
||||
|> string.replace("/tracks","")
|
||||
|> string.replace("/platform","")
|
||||
|> string.replace("/track","")
|
||||
case #(show_platforms, get_header(request, "x-forwarded-host")) {
|
||||
#(False, Ok(domain)) if domain == leitpunkt_domain ->
|
||||
query
|
||||
|> lookup_exact(leitpunkt_to_name)
|
||||
|> if_not(fn() { lookup_fuzzy(query, Leitpunkt, fuzzy) })
|
||||
|> pair.new(False)
|
||||
#(False, Ok(domain)) if domain == ril100_domain || domain == ds100_domain ->
|
||||
query
|
||||
|> lookup_exact(ds100_to_name)
|
||||
|> if_not(fn() { lookup_fuzzy(query, DS100, fuzzy) })
|
||||
|> pair.new(False)
|
||||
#(True, Ok(domain)) if domain == leitpunkt_domain -> {
|
||||
let query = case map.get(leitpunkt_to_name, query) {
|
||||
Ok(name) -> name
|
||||
_ -> query
|
||||
}
|
||||
case fuzzy(query, DS100) {
|
||||
Exact(code) -> #(200, lookup_platform(code))
|
||||
Fuzzy(_, code) -> #(200, lookup_platform(code))
|
||||
_ -> #(404, "")
|
||||
} |> pair.new(True)
|
||||
}
|
||||
#(True, Ok(domain)) if domain == ril100_domain || domain == ds100_domain ->
|
||||
case lookup_exact(query, ds100_to_name) {
|
||||
#(200,_) -> #(200, lookup_platform(query))
|
||||
_ -> case fuzzy(query, DS100) {
|
||||
Exact(code) -> #(200, lookup_platform(code))
|
||||
Fuzzy(_, code) -> #(200, lookup_platform(code))
|
||||
_ -> #(404, "")
|
||||
}
|
||||
} |> pair.new(True)
|
||||
_ -> {
|
||||
let by_ds100 = lookup_exact(query, ds100_to_name)
|
||||
let by_lp = lookup_exact(query, leitpunkt_to_name)
|
||||
case #(by_ds100.0, by_lp.0) {
|
||||
#(200, _) -> #(302, proto <> ril100_domain <> "/" <> path)
|
||||
#(_, 200) -> #(302, proto <> leitpunkt_domain <> "/" <> path)
|
||||
_ -> #(302, proto <> ril100_domain <> "/" <> path)
|
||||
} |> pair.new(False)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ -> #(#(404, "intended usage is e.g. curl " <> proto <> domain <> "/FF"), False)
|
||||
}
|
||||
let body = text
|
||||
|> bit_builder.from_string
|
||||
|> mist.Bytes
|
||||
|
||||
let content_type = case is_html {
|
||||
True -> "text/html; charset=utf8"
|
||||
False -> "text/plain; charset=utf8"
|
||||
}
|
||||
|
||||
response.new(code)
|
||||
|> response.prepend_header(
|
||||
"x-data-source",
|
||||
"https://data.deutschebahn.com/dataset/data-betriebsstellen.html",
|
||||
)
|
||||
|> response.prepend_header(
|
||||
"x-sources-at",
|
||||
"https://stuebinm.eu/git/bahnhof.name",
|
||||
)
|
||||
|> response.prepend_header("content-type", content_type)
|
||||
|> fn(a) {
|
||||
case code == 302 {
|
||||
True -> response.prepend_header(a, "location", text)
|
||||
_ -> a
|
||||
}
|
||||
}
|
||||
|> response.set_body(body)
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let assert Ok(bahn_ril100) = file.read("data/DBNetz-Betriebsstellenverzeichnis-Stand2021-10.csv")
|
||||
|
||||
let ds100s =
|
||||
read_csv(bahn_ril100, ";")
|
||||
|> list.filter_map(fn(fields) {
|
||||
case fields {
|
||||
[_, ds100, name, ..] -> Ok(#(name, ds100))
|
||||
_ -> Error(fields)
|
||||
}
|
||||
})
|
||||
let assert Ok(leitpunkte_raw) = file.read("data/leitpunkte.csv")
|
||||
let leitpunkte =
|
||||
read_csv(leitpunkte_raw, ";")
|
||||
|> list.filter_map(fn(fields) {
|
||||
case fields {
|
||||
[lp, name, _ds100] -> Ok(#(name, lp))
|
||||
_ -> Error(fields)
|
||||
}
|
||||
})
|
||||
let assert Ok(platforms_raw) = file.read("data/platforms.tsv")
|
||||
let platforms = read_csv(platforms_raw, "\t")
|
||||
|
||||
let name_to_ds100 = map.from_list(ds100s)
|
||||
let name_to_leitpunkt = map.from_list(leitpunkte)
|
||||
let ds100_to_name = map.from_list(list.map(ds100s, swap))
|
||||
let leitpunkt_to_name = map.from_list(list.map(leitpunkte, swap))
|
||||
let ds100index =
|
||||
index_new(atom.create_from_string("ds100"))
|
||||
|> index_ref(field_term("id"))
|
||||
|> index_field(field_new("name"))
|
||||
|> index_add(
|
||||
ds100s
|
||||
|> list.map(fn(tuple) {
|
||||
case tuple {
|
||||
#(name, ds100) -> map.from_list([#("id", ds100), #("name", name)])
|
||||
}
|
||||
}),
|
||||
)
|
||||
let leitpunkt_index =
|
||||
index_new(atom.create_from_string("leitpunkt"))
|
||||
|> index_ref(field_term("id"))
|
||||
|> index_field(field_new("name"))
|
||||
|> index_add(
|
||||
leitpunkte
|
||||
|> list.map(fn(tuple) {
|
||||
case tuple {
|
||||
#(name, leitpunkt) ->
|
||||
map.from_list([#("id", leitpunkt), #("name", name)])
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
let ref = atom.create_from_string("ref")
|
||||
let fuzzy = fn(searchterm: String, kind: IdKind) -> List(String) {
|
||||
let query = query_new()
|
||||
let index = case kind {
|
||||
DS100 -> ds100index
|
||||
Leitpunkt -> leitpunkt_index
|
||||
}
|
||||
let match = atom.create_from_string("match")
|
||||
let field = atom.create_from_string("field")
|
||||
let term = atom.create_from_string("term")
|
||||
let expressions =
|
||||
tokenize(inspect(searchterm))
|
||||
|> list.filter_map(fn(a) { map.get(a, atom.create_from_string("v")) })
|
||||
|> list.map(fn(token) {
|
||||
query_expression_new(match, [#(field, "name"), #(term, token)])
|
||||
})
|
||||
let clause =
|
||||
query_expressions(clause_new(atom.create_from_string("all")), expressions)
|
||||
let query = query_clause(query, clause)
|
||||
|
||||
let matches =
|
||||
query_run(query, index)
|
||||
|> list.filter_map(fn(a) { map.get(a, ref) })
|
||||
|
||||
inspect(matches)
|
||||
case list.length(matches) > 5 {
|
||||
True -> {
|
||||
let query = query_new()
|
||||
let clause =
|
||||
query_expressions(
|
||||
clause_new(atom.create_from_string("all")),
|
||||
[
|
||||
query_expression_new(match, [#(field, "name"), #(term, "hbf")]),
|
||||
..expressions
|
||||
],
|
||||
)
|
||||
let query = query_clause(query, clause)
|
||||
let narrow =
|
||||
query_run(query, index)
|
||||
|> list.filter_map(fn(a) { map.get(a, ref) })
|
||||
case narrow {
|
||||
[] -> matches
|
||||
_ -> narrow
|
||||
}
|
||||
}
|
||||
_ -> matches
|
||||
}
|
||||
}
|
||||
|
||||
let exact_then_fuzzy = fn(searchterm: String, kind: IdKind) -> Matched(String) {
|
||||
let #(stations, ids) = case kind {
|
||||
DS100 -> #(name_to_ds100, ds100_to_name)
|
||||
Leitpunkt -> #(name_to_leitpunkt, leitpunkt_to_name)
|
||||
}
|
||||
case map.get(stations, searchterm) {
|
||||
Ok(id) -> Exact(id)
|
||||
_ -> {
|
||||
let results =
|
||||
fuzzy(searchterm, kind)
|
||||
|> list.filter_map(fn(res) { map.get(ids, string.uppercase(res)) })
|
||||
case results {
|
||||
[res] -> {
|
||||
let assert Ok(station) = map.get(stations, res)
|
||||
Fuzzy(res, station)
|
||||
}
|
||||
[res, ..] -> {
|
||||
let assert Ok(station) = map.get(stations, res)
|
||||
Fuzzy(res, station)
|
||||
}
|
||||
_ -> Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lookup_platform = fn(ds100: String) -> String {
|
||||
inspect(ds100)
|
||||
platforms
|
||||
|> list.filter(fn(a) { list.first(a) == Ok(ds100) })
|
||||
|> list.map(fn(line) { case line {
|
||||
[_code,osmid,osmtype,info] -> "<a href=\"https://osm.org/"<>osmtype<>"/"<>osmid<>"\">"<>info<>"</a>"
|
||||
}})
|
||||
|> string.join("<br>\n")
|
||||
|> inspect
|
||||
}
|
||||
|
||||
io.println("compiled indices, starting server …")
|
||||
|
||||
let assert Ok(_) =
|
||||
fn(req: Request(mist.Connection)) -> Response(mist.ResponseData) {
|
||||
lookup_station(req, ds100_to_name, leitpunkt_to_name, lookup_platform, exact_then_fuzzy)
|
||||
}
|
||||
|> mist.new
|
||||
|> mist.port(2345)
|
||||
|> mist.start_http
|
||||
|
||||
process.sleep_forever()
|
||||
}
|
||||
|
||||
fn fetch_data() -> Result(String, hackney.Error) {
|
||||
let assert Ok(uri) =
|
||||
uri.parse(
|
||||
"https://download-data.deutschebahn.com/static/datasets/betriebsstellen/DBNetz-Betriebsstellenverzeichnis-Stand2021-10.csv",
|
||||
)
|
||||
let assert Ok(request) = request.from_uri(uri)
|
||||
io.println("got response")
|
||||
let assert Ok(response) = hackney.send(request)
|
||||
|
||||
// some ü are corrupted for some reason
|
||||
Ok(string.replace(response.body, "<EFBFBD>", "ü"))
|
||||
}
|
||||
|
||||
fn read_csv(contents, sep) -> List(List(String)) {
|
||||
contents
|
||||
// the file doesn't use quotes, so this is fine
|
||||
|> string.split(on: "\n")
|
||||
// drop CSV header
|
||||
|> list.drop(1)
|
||||
|> list.map(fn(a) { string.split(a, on: sep) })
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import gleeunit
|
||||
import gleeunit/should
|
||||
|
||||
pub fn main() {
|
||||
gleeunit.main()
|
||||
}
|
||||
|
||||
// gleeunit test functions end in `_test`
|
||||
pub fn hello_world_test() {
|
||||
1
|
||||
|> should.equal(1)
|
||||
}
|
Loading…
Reference in a new issue