Initial commit

This commit is contained in:
Hubert Van De Walle 2020-09-26 13:28:46 +02:00
commit d81a830e3f
29 changed files with 7083 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
target/
.idea/
*.iml
*.ipr
*.iws

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM openjdk:14-alpine as jdkbuilder
RUN apk add --no-cache binutils
ENV MODULES java.base,java.compiler,java.desktop,java.logging,java.management,java.naming,java.security.jgss,java.xml,jdk.crypto.ec,jdk.unsupported
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
FROM maven:3.6.3-jdk-14 as builder
WORKDIR /app
COPY pom.xml .
RUN mvn verify clean --fail-never
COPY src/main src/main
RUN mvn package
FROM alpine
ENV APPLICATION_USER app
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY --from=builder /app/target/c2c*.jar /app/app.jar
COPY --from=jdkbuilder /myjdk /myjdk
WORKDIR /app
EXPOSE 4000
CMD ["/myjdk/bin/java", "-server", "-XX:+UseG1GC", "-XX:+UseStringDeduplication", "-jar", "app.jar"]

227
pom.xml Normal file
View File

@ -0,0 +1,227 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>be.simplenotes.c2c</groupId>
<artifactId>c2c</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>14</java.version>
<kotlin.version>1.4.10</kotlin.version>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>be.simplenotes.c2c/C2cKt</main.class>
<openhtml.version>1.0.4</openhtml.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId>
<version>3.261.0</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-contract</artifactId>
<version>3.261.0</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-format-jackson</artifactId>
<version>3.261.0</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-server-apache</artifactId>
<version>3.261.0</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-client-apache</artifactId>
<version>3.261.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-core</artifactId>
<version>${openhtml.version}</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>${openhtml.version}</version>
</dependency>
<dependency>
<groupId>com.mitchellbosecke</groupId>
<artifactId>pebble</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-common</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.0.0-M4</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${main.class}</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>com.github.ben-manes.caffeine:caffeine</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains.kotlin:kotlin-reflect</artifact>
<includes>
<include>**</include>
</includes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

102
src/main/kotlin/C2c.kt Normal file
View File

@ -0,0 +1,102 @@
package be.simplenotes.c2c
import be.simplenotes.c2c.api.Api
import be.simplenotes.c2c.api.ApiUrls
import be.simplenotes.c2c.pdf.PdfCreator
import be.simplenotes.c2c.pdf.StreamFactory
import be.simplenotes.c2c.routes.IndexRoute
import be.simplenotes.c2c.routes.PdfRoute
import be.simplenotes.c2c.routes.RouteRoute
import be.simplenotes.c2c.routes.SearchRoute
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.ClasspathLoader
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder
import org.http4k.client.ApacheClient
import org.http4k.contract.ContractRoute
import org.http4k.contract.contract
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.core.Filter
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.then
import org.http4k.filter.RequestFilters
import org.http4k.filter.ServerFilters
import org.http4k.format.Jackson
import org.http4k.server.asServer
import org.koin.core.context.startKoin
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.slf4j.LoggerFactory
import java.io.PrintWriter
import java.io.StringWriter
val module = module {
single {
ApacheClient(
HttpClientBuilder
.create()
.setRedirectStrategy(DefaultRedirectStrategy())
.build()
)
}
single { ApiUrls(get()) }
single { Jackson.mapper }
single { Api(get(), get()) }
single { PdfCreator(get()) }
single { StreamFactory(get()) }
single {
PebbleEngine
.Builder()
.loader(ClasspathLoader().apply {
prefix = "views/"
suffix = ".twig"
})
.cacheActive(false)
.build()
}
}
val routes = module {
single(named<SearchRoute>()) { SearchRoute(get()).searchRoute() } bind ContractRoute::class
single(named<RouteRoute>()) { RouteRoute(get()).routeRoute() } bind ContractRoute::class
single(named<PdfRoute>()) { PdfRoute(get(), get()).route() } bind ContractRoute::class
single(named<IndexRoute>()) { IndexRoute(get()).route() } bind ContractRoute::class
}
fun main() {
val koin = startKoin {
modules(module, routes)
}.koin
val appRoutes = koin.getAll<ContractRoute>()
val app = contract {
renderer = OpenApi3(ApiInfo("Camp2Camp", "1.0-SNAPSHOT"), Jackson)
descriptionPath = "/api/swagger.json"
routes.all.addAll(appRoutes)
}
val logger = LoggerFactory.getLogger("Camp2Camp")
val loggingFilter = RequestFilters.Tap {
logger.info("${it.method} ${it.uri}")
}
val catchAll = Filter { next ->
{
try {
next(it)
} catch (e: Exception) {
val sw = StringWriter()
e.printStackTrace(PrintWriter(sw))
logger.error(sw.toString())
Response(Status.INTERNAL_SERVER_ERROR)
}
}
}
catchAll.then(ServerFilters.GZip()).then(loggingFilter).then(app).asServer(CustomApacheServer(4000)).start()
logger.info("Listening on http://localhost:4000")
}

View File

@ -0,0 +1,33 @@
package be.simplenotes.c2c
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap
import org.apache.hc.core5.http.io.SocketConfig
import org.http4k.core.HttpHandler
import org.http4k.server.Http4kRequestHandler
import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig
class CustomApacheServer(val port: Int) : ServerConfig {
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
val handler = Http4kRequestHandler(httpHandler)
val server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setSocketConfig(SocketConfig.custom()
.setTcpNoDelay(true)
.setSoKeepAlive(true)
.setSoReuseAddress(true)
.setBacklogSize(1000)
.build())
.apply {
register("*", handler)
registerVirtual("dev.simplenotes.be", "*", handler) // TODO: find a way to put a wildcard
}.create()
override fun start() = apply { server.start() }
override fun stop() = apply { server.stop() }
override fun port(): Int = port
}
}

View File

@ -0,0 +1,44 @@
package be.simplenotes.c2c.api
import be.simplenotes.c2c.extractors.extractRoute
import be.simplenotes.c2c.extractors.extractRoutes
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.benmanes.caffeine.cache.Caffeine
import org.checkerframework.checker.nullness.qual.Nullable
import org.http4k.format.Jackson
import org.slf4j.LoggerFactory
import java.util.concurrent.TimeUnit
class Api(private val apiUrls: ApiUrls, private val mapper: ObjectMapper = Jackson.mapper) {
private val searchCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(15, TimeUnit.MINUTES)
.build<String, SearchResults>()
fun cachedSearch(terms: String) = searchCache[terms, ::search]!!
private fun search(terms: String): SearchResults {
val root = mapper.readTree(apiUrls.search(terms))
val routes = root["routes"]["documents"].map(::extractRoutes)
return SearchResults(routes)
}
private val routeCache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.expireAfterWrite(15, TimeUnit.MINUTES)
.build<String, Route>()
private val logger = LoggerFactory.getLogger("API")
fun getRoute(id: String): Route? {
val res = routeCache.get(id) {
apiUrls.getRoute(it)?.let { json ->
extractRoute(mapper.readTree(json))
}
}
logger.info("Cache: ${routeCache.stats()}")
return res
}
}

View File

@ -0,0 +1,19 @@
package be.simplenotes.c2c.api
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.urlEncoded
class ApiUrls(private val client: HttpHandler) {
private val baseUrl = "https://api.camptocamp.org"
private fun get(url: String): String? {
val res = client(Request(Method.GET, "$baseUrl$url"))
return if(res.status.successful) res.bodyString()
else null
}
fun search(terms: String) = get("/search?q=${terms.urlEncoded()}&t=w,r&limit=7")
fun getRoute(id: String): String? = get("/routes/${id}?cook=fr")
fun getOuting(id: String) = get("/outings/${id}?cook=fr")
}

View File

@ -0,0 +1,85 @@
package be.simplenotes.c2c.api
data class SearchResults(val routes: List<RouteResult>)
data class RouteResult(
val id: String,
val title: String,
val summary: String?,
val activities: List<Activity>,
)
data class Route(
val id: String,
val title: String,
val summary: String?,
val routeHistory: String,
val description: String,
val remarks: String,
val gear: String,
val activities: List<Activity>,
val height: Height,
val rating: Rating,
val associations: Associations,
)
data class Associations(
val outings: List<Outing>,
val images: List<Image>,
val waypoints: List<Waypoint>,
)
data class Image(
val id: String,
val square: String,
val medium: String,
val big: String,
val full: String,
val title: String,
)
data class Outing(
val id: String,
val title: String,
val dateStart: String,
val dateEnd: String,
val condition: String?,
val quality: String,
val activities: List<Activity>,
)
data class Waypoint(
val id: String,
val title: String,
val elevation: String,
)
data class Height(
val heightDiffDifficulties: String?,
val up: String?,
val down: String?,
val min: String?,
val max: String?,
)
data class Rating(
val global: String?,
val free: String?,
val required: String?,
val engagement: String?,
val equipmentQuality: String?,
)
enum class Activity {
SKITOURING,
SNOW_ICE_MIXED,
MOUNTAIN_CLIMBING,
ROCK_CLIMBING,
ICE_CLIMBING,
HIKING,
SNOWSHOEING,
PARAGLIDING,
MOUNTAIN_BIKING,
VIA_FERRATA,
SLACKLINING,
}

View File

@ -0,0 +1,8 @@
package be.simplenotes.c2c.extractors
import be.simplenotes.c2c.api.Activity
import com.fasterxml.jackson.databind.JsonNode
fun JsonNode.activities(): List<Activity> = map {
Activity.valueOf(it.asText().toUpperCase())
}

View File

@ -0,0 +1,7 @@
package be.simplenotes.c2c.extractors
import com.fasterxml.jackson.databind.JsonNode
fun extractLocale(locales: JsonNode): JsonNode = locales.find { locale ->
locale["lang"].asText() == "fr"
} ?: locales[0]

View File

@ -0,0 +1,85 @@
package be.simplenotes.c2c.extractors
import be.simplenotes.c2c.api.*
import com.fasterxml.jackson.databind.JsonNode
fun extractRoutes(node: JsonNode): RouteResult {
val locale = extractLocale(node["locales"])
return RouteResult(
id = node["document_id"].asText(),
title = locale["title_prefix"].asText() + " : " + locale["title"].asText(),
summary = locale["summary"].asText(null),
activities = node["activities"].activities(),
)
}
fun extractRoute(root: JsonNode): Route {
val locale = extractLocale(root["locales"])
return Route(
root["document_id"].asText(),
locale["title_prefix"].asText() + " : " + locale["title"].asText(),
locale["summary"].asText(null),
locale["route_history"].asText(),
locale["description"].asText(),
locale["remarks"].asText(),
locale["gear"].asText(),
root["activities"].activities(),
Height(
root["height_diff_difficulties"].asText(null),
root["height_diff_up"].asText(null),
root["height_diff_down"].asText(null),
root["elevation_min"].asText(null),
root["elevation_max"].asText(null),
),
Rating(
root["global_rating"].asText(null),
root["rock_free_rating"].asText(null),
root["rock_required_rating"].asText(null),
root["engagement_rating"].asText(null),
root["equipment_rating"].asText(null),
),
Associations(
extractOutings(root["associations"]["recent_outings"]["documents"]),
extractImages(root["associations"]["images"]),
extractWaypoints(root["associations"]["waypoints"]),
)
)
}
fun extractOutings(jsonNode: JsonNode) = jsonNode.map { node ->
val locale = extractLocale(node["locales"])
Outing(
id = node["document_id"].asText(),
title = locale["title"].asText(),
dateStart = node["date_start"].asText(),
dateEnd = node["date_end"].asText(),
condition = node["condition_rating"].asText(null),
quality = node["quality"].asText(),
activities = node["activities"].activities()
)
}
fun extractImages(jsonNode: JsonNode) = jsonNode.map { node ->
val locale = extractLocale(node["locales"])
val filename = node["filename"].asText()
Image(
id = node["document_id"].asText(),
title = locale["title"].asText(),
square = "https://media.camptocamp.org/c2corg-active/${filename.replace(".", "SI.").replace(".svg", ".jpg")}",
medium = "https://media.camptocamp.org/c2corg-active/${filename.replace(".", "MI.").replace(".svg", ".jpg")}",
big = "https://media.camptocamp.org/c2corg-active/${filename.replace(".", "BI.").replace(".svg", ".jpg")}",
full = "https://media.camptocamp.org/c2corg-active/$filename",
)
}
fun extractWaypoints(jsonNode: JsonNode) = jsonNode.map { node ->
val locale = extractLocale(node["locales"])
Waypoint(
id = node["document_id"].asText(),
title = locale["title"].asText(),
elevation = node["elevation"].asText(),
)
}

View File

@ -0,0 +1,80 @@
package be.simplenotes.c2c.pdf
import be.simplenotes.c2c.api.Route
import be.simplenotes.c2c.templates.render
import com.github.benmanes.caffeine.cache.Caffeine
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.ClasspathLoader
import com.openhtmltopdf.extend.FSStream
import com.openhtmltopdf.extend.FSStreamFactory
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import com.openhtmltopdf.util.XRLog
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
import org.jsoup.Jsoup
import org.jsoup.helper.W3CDom
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit
class StreamFactory(private val client: HttpHandler) : FSStreamFactory {
private val logger = LoggerFactory.getLogger(javaClass)
private val imgCache = Caffeine.newBuilder()
.maximumSize(15)
.expireAfterWrite(15, TimeUnit.MINUTES)
.build<String, ByteArray>()
override fun getUrl(url: String) = object : FSStream {
override fun getStream(): InputStream? {
val bytes = imgCache.get(url) {
logger.info("Downloading $url")
val res = client(Request(Method.GET, url))
if (res.status.successful) res.body.stream.readAllBytes()
else null
}
return bytes?.inputStream()
}
override fun getReader() = null
}
}
class PdfCreator(private val streamFactory: StreamFactory) {
private val engine = PebbleEngine
.Builder()
.loader(ClasspathLoader().apply {
prefix = "templates/"
suffix = ".twig"
})
.cacheActive(true)
.build()
fun create(route: Route): ByteArrayInputStream {
val html = engine.render("index", mapOf("route" to route))
XRLog.setLoggingEnabled(false)
val document = W3CDom().fromJsoup(Jsoup.parse(html))
val out = ByteArrayOutputStream()
PdfRendererBuilder()
.withW3cDocument(document, "")
.useFastMode()
.toStream(out)
.useProtocolsStreamImplementation(streamFactory, "http", "https")
.buildPdfRenderer().apply {
layout()
createPDF()
close()
}
out.close()
return ByteArrayInputStream(out.toByteArray())
}
}

View File

@ -0,0 +1,24 @@
package be.simplenotes.c2c.routes
import be.simplenotes.c2c.templates.render
import com.mitchellbosecke.pebble.PebbleEngine
import org.http4k.contract.ContractRoute
import org.http4k.contract.bindContract
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
class IndexRoute(private val engine: PebbleEngine) {
fun route(): ContractRoute {
val spec = "/" bindContract Method.GET
val route: HttpHandler = {
Response(Status.OK).body(engine.render("index"))
}
return spec to route
}
}

View File

@ -0,0 +1,38 @@
package be.simplenotes.c2c.routes
import be.simplenotes.c2c.api.Api
import be.simplenotes.c2c.pdf.PdfCreator
import org.http4k.contract.ContractRoute
import org.http4k.contract.div
import org.http4k.contract.meta
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.lens.Path
// TODO: rename..
class PdfRoute(private val api: Api, private val pdfCreator: PdfCreator) {
fun route(): ContractRoute {
val spec = "/route" / Path.of("id", "Route's ID") / "pdf" meta {
summary = "Route details"
} bindContract Method.GET
fun route(id: String, pdf: String): HttpHandler = {
when (val route = api.getRoute(id)) {
null -> Response(Status.NOT_FOUND)
else -> {
val bytes = pdfCreator.create(route)
Response(Status.OK)
.body(bytes)
.header("Content-Type", "application/pdf")
.header("Content-Disposition", "attachment; filename=\"out.pdf\"")
}
}
}
return spec to ::route
}
}

View File

@ -0,0 +1,31 @@
package be.simplenotes.c2c.routes
import be.simplenotes.c2c.api.Api
import be.simplenotes.c2c.api.Route
import org.http4k.contract.ContractRoute
import org.http4k.contract.div
import org.http4k.contract.meta
import org.http4k.core.*
import org.http4k.format.Jackson.auto
import org.http4k.lens.Path
// TODO: rename..
class RouteRoute(private val api: Api) {
fun routeRoute(): ContractRoute {
val responseLens = Body.auto<Route>("Route").toLens()
val spec = "/route" / Path.of("id", "Route's ID") meta {
summary = "Route details"
} bindContract Method.GET
fun route(id: String): HttpHandler = {
api.getRoute(id)?.let {
Response(Status.OK).with(responseLens of it)
} ?: Response(Status.NOT_FOUND)
}
return spec to ::route
}
}

View File

@ -0,0 +1,38 @@
package be.simplenotes.c2c.routes
import be.simplenotes.c2c.api.Activity
import be.simplenotes.c2c.api.Api
import be.simplenotes.c2c.api.RouteResult
import be.simplenotes.c2c.api.SearchResults
import org.http4k.contract.ContractRoute
import org.http4k.contract.Tag
import org.http4k.contract.meta
import org.http4k.core.*
import org.http4k.format.Jackson.auto
import org.http4k.lens.Query
class SearchRoute(private val api: Api) {
fun searchRoute(): ContractRoute {
val query = Query.required("q", "Search terms")
val responseLens = Body.auto<SearchResults>("Search results").toLens()
val spec = "/search" meta {
summary = "Search routes and waypoints"
tags += Tag("Query")
returning(Status.OK, responseLens to SearchResults(listOf(RouteResult(
id = "57103",
title = "Tour de Bavon : Thor",
summary = "Belle et longue voie équipée sur des dalles compactes qui sort sous le sommet. ",
activities = listOf(Activity.ROCK_CLIMBING),
))))
} bindContract Method.GET
val search: HttpHandler = { req: Request ->
Response(Status.OK).with(responseLens of api.cachedSearch(query(req)))
}
return spec to search
}
}

View File

@ -0,0 +1,11 @@
package be.simplenotes.c2c.templates
import com.mitchellbosecke.pebble.PebbleEngine
import java.io.StringWriter
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()): String {
val template = getTemplate(name)
val writer = StringWriter()
template.evaluate(writer, args)
return writer.toString()
}

View File

@ -0,0 +1,15 @@
<configuration>
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<withJansi>true</withJansi>
<encoder>
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
<logger level="INFO" name="com.mitchellbosecke.pebble"/>
<logger level="INFO" name="org.apache.hc.client5"/>
<logger level="INFO" name="io.mockk"/>
</configuration>

View File

@ -0,0 +1,10 @@
<h1>{{ route.title }}</h1>
<p>{{ route.summary }}</p>
<p>{{ route.description }}</p>
{% for img in route.associations.images %}
<figure>
<img src="{{ img.medium }}"
alt="{{ img.title }}">
<figcaption>{{ img.title }}</figcaption>
</figure>
{% endfor %}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="">
<title>Title</title>
</head>
<body>
Hello
</body>
</html>

1
src/test/kotlin/Empty.kt Normal file
View File

@ -0,0 +1 @@
package be.simplenotes.c2c

View File

@ -0,0 +1,85 @@
package be.simplenotes.c2c.api
import be.simplenotes.c2c.extractors.extractLocale
import be.simplenotes.c2c.extractors.extractRoute
import com.fasterxml.jackson.module.kotlin.readValue
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.http4k.format.Jackson
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ApiTest {
private val urls = mockk<ApiUrls>()
private val api = Api(urls)
private val mapper = Jackson.mapper
@BeforeEach
fun beforeEach() {
clearMocks(urls)
}
@ParameterizedTest
@CsvSource("bavon", "portalet", "test")
fun parseRoutes(fileName: String) {
val file = javaClass.getResource("/search/$fileName").readText()
val (terms, input, output) = file.split("---", limit = 3).map { it.trim() }
every { urls.search(terms) } returns input
val results = api.cachedSearch(terms)
val expectedRoutes = mapper.readValue<List<RouteResult>>(output)
assertThat(results.routes).containsExactlyInAnyOrderElementsOf(expectedRoutes)
}
@ParameterizedTest
@CsvSource("57103")
fun route(id: String) {
val input = javaClass.getResource("/routes/$id-in.json").readText()
every { urls.getRoute(id) } returns input
// test
val root = mapper.readTree(input)
val route = extractRoute(root)
root["cooked"].forEach {
println(it.asText())
}
//
}
@ParameterizedTest
@CsvSource("bavon")
fun outing(id: String) {
val input = javaClass.getResource("/outings/$id-in.json").readText()
every { urls.getOuting(id) } returns input
//
val json = urls.getOuting(id)
val root = mapper.readTree(json)
//
val locale = extractLocale(root["locales"])
val doc = mapOf(
"id" to root["document_id"].asText(null),
"title" to locale["title"].asText(null),
"weather" to locale["weather"].asText(null),
"conditions_levels" to locale["conditions_levels"].asText(null),
"route_description" to locale["route_description"].asText(null),
"summary" to locale["summary"].asText(null),
"version" to locale["version"].asText(null),
"access_comment" to locale["access_comment"].asText(null),
"timing" to locale["timing"].asText(null),
"participants" to locale["participants"].asText(null),
"hut_comment" to locale["hut_comment"].asText(null),
"topic_id" to locale["topic_id"].asText(null),
"description" to locale["description"].asText(null),
"conditions" to locale["conditions"].asText(null),
)
println(doc.entries.joinToString("\n"))
}
}

View File

@ -0,0 +1,387 @@
{
"area":{
"letter":"a",
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "area_type", "properties": {"url":"atyp"}},
{"id": "quality", "properties": {"url":"qa"}}
]
},
"article":{
"letter":"c",
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "activities", "properties": {"url":"act"}},
{"id": "article_categories", "properties": {"url":"acat"}},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "article_type", "properties": {"url":"atyp"}},
{"id": "articles", "properties": {"url":"c", "associationEditorOrder": 1}},
{"id": "books", "properties": {"url":"b", "associationEditorOrder": 4}},
{"id": "outings", "properties": {"url":"o", "associationEditorOrder": 5}},
{"id": "routes", "properties": {"url":"r", "associationEditorOrder": 3}},
{"id": "waypoints", "properties": {"url":"w", "associationEditorOrder": 2}},
{"id": "images", "properties":{"associationEditorOrder": 8}},
{"id": "users", "properties":{"associationEditorOrder": 7}},
{"id": "xreports", "properties":{"associationEditorOrder": 6}}
]
},
"book":{
"letter":"b",
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "author"},
{"id": "editor"},
{"id": "activities", "properties": {"url":"act"}},
{"id": "url"},
{"id": "isbn"},
{"id": "book_types", "properties": {"url":"btyp"}},
{"id": "publication_date"},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "langs"},
{"id": "nb_pages"},
{"id": "articles", "properties": {"url":"c"}},
{"id": "routes", "properties": {"url":"r"}},
{"id": "waypoints", "properties": {"url":"w"}},
{"id": "images"}
]
},
"image":{
"letter":"i",
"geoLocalized":true,
"fields":[
{"id": "title", "properties": {"required":false}},
{"id": "summary"},
{"id": "description"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "activities", "properties": {"url":"act"}},
{"id": "author"},
{"id": "camera_name"},
{"id": "date_time"},
{"id": "elevation", "properties": {"url":"ialt"}},
{"id": "height"},
{"id": "image_categories", "properties": {"url":"cat"}},
{"id": "image_type", "properties": {"url":"ityp"}},
{"id": "iso_speed"},
{"id": "file_size"},
{"id": "filename"},
{"id": "exposure_time"},
{"id": "focal_length"},
{"id": "fnumber"},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "width"},
{"id": "areas", "properties":{"associationEditorOrder": 6}},
{"id": "articles", "properties": {"url":"c", "associationEditorOrder": 5}},
{"id": "books", "properties": {"url":"b", "associationEditorOrder": 7}},
{"id": "outings", "properties":{"associationEditorOrder": 3}},
{"id": "routes", "properties": {"url":"r", "associationEditorOrder": 1}},
{"id": "users", "properties":{"associationEditorOrder": 8}},
{"id": "waypoints", "properties": {"url":"w", "associationEditorOrder": 2}},
{"id": "xreports", "properties":{"associationEditorOrder": 4}},
{"id": "images", "properties":{"associationEditorOrder": 9}}
]
},
"map":{
"letter":"m",
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "code"},
{"id": "scale"},
{"id": "editor"}
]
},
"outing":{
"letter":"o",
"geoLocalized":true,
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "conditions_levels", "properties": {"activities": ["skitouring", "snowshoeing", "ice_climbing", "snow_ice_mixed"]}},
{"id": "participants"},
{"id": "access_comment"},
{"id": "weather"},
{"id": "timing"},
{"id": "conditions"},
{"id": "hut_comment"},
{"id": "route_description"},
{"id": "avalanches", "properties": {"activities":["ice_climbing", "skitouring", "snowshoeing", "snow_ice_mixed"]}},
{"id": "lang", "properties": {"url":"l"}},
{"id": "access_condition"},
{"id": "activities", "properties": {"url":"act", "required":true}},
{"id": "avalanche_signs", "properties": {"url":"avdate", "activities":["ice_climbing", "skitouring", "snowshoeing", "snow_ice_mixed"]}},
{"id": "condition_rating", "properties": {"url":"ocond"}},
{"id": "date_end"},
{"id": "date_start"},
{"id": "disable_comments"},
{"id": "elevation_access", "properties": {"url":"oparka"}},
{"id": "elevation_down_snow", "properties": {"url":"swld", "activities":["ice_climbing", "mountain_climbing", "skitouring", "snow_ice_mixed", "snowshoeing"]}},
{"id": "elevation_max", "properties": {"url":"oalt"}},
{"id": "elevation_min"},
{"id": "elevation_up_snow", "properties": {"url":"swlu", "activities":["ice_climbing", "mountain_climbing", "skitouring", "snow_ice_mixed", "snowshoeing"]}},
{"id": "engagement_rating", "properties": {"url":"erat", "activities":["mountain_climbing", "snow_ice_mixed"]}},
{"id": "equipment_rating", "properties": {"url":"prat", "activities":["rock_climbing"]}},
{"id": "frequentation", "properties": {"url":"ofreq"}},
{"id": "glacier_rating", "properties": {"url":"oglac", "activities":["mountain_climbing", "skitouring", "snow_ice_mixed", "snowshoeing"]}},
{"id": "global_rating", "properties": {"url":"grat", "activities":["mountain_climbing", "rock_climbing", "snow_ice_mixed"]}},
{"id": "height_diff_difficulties", "properties": {"url":"dhei", "activities":["mountain_climbing", "snow_ice_mixed"]}},
{"id": "height_diff_down"},
{"id": "height_diff_up", "properties": {"url":"odif"}},
{"id": "hiking_rating", "properties": {"url":"hrat", "activities":["hiking"]}},
{"id": "hut_status"},
{"id": "ice_rating", "properties": {"url":"irat", "activities":["ice_climbing"]}},
{"id": "labande_global_rating", "properties": {"url":"lrat", "activities":["skitouring"]}},
{"id": "length_total", "properties": {"url":"olen"}},
{"id": "lift_status"},
{"id": "mtb_down_rating", "properties": {"url":"mbdr", "activities":["mountain_biking"]}},
{"id": "mtb_up_rating", "properties": {"url":"mbur", "activities":["mountain_biking"]}},
{"id": "partial_trip"},
{"id": "participant_count"},
{"id": "public_transport", "properties": {"url":"owpt"}},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "rock_free_rating", "properties": {"url":"frat", "activities":["rock_climbing"]}},
{"id": "ski_rating", "properties": {"url":"trat", "activities":["skitouring"]}},
{"id": "snow_quality", "properties": {"url":"swqual", "activities":["ice_climbing", "mountain_climbing", "skitouring", "snow_ice_mixed", "snowshoeing"]}},
{"id": "snow_quantity", "properties": {"url":"swquan", "activities":["ice_climbing", "mountain_climbing", "skitouring", "snow_ice_mixed", "snowshoeing"]}},
{"id": "snowshoe_rating", "properties": {"url":"wrat", "activities":["snowshoeing"]}},
{"id": "via_ferrata_rating", "properties": {"url":"krat", "activities":["via_ferrata"]}},
{"id": "articles", "properties": {"url":"c", "associationEditorOrder": 5}},
{"id": "images", "properties": {"associationEditorOrder": 3}},
{"id": "routes", "properties": {"url":"r", "required":true, "associationEditorOrder": 1}},
{"id": "users", "properties": {"url":"u", "required":true, "associationEditorOrder": 2}},
{"id": "xreports", "properties": {"url":"x", "associationEditorOrder": 4}}
]
},
"profile":{
"letter":"u",
"geoLocalized":true,
"fields":[
{"id": "summary"},
{"id": "description"},
{"id": "lang"},
{"id": "activities", "properties": {"url":null, "required":false}},
{"id": "categories", "properties": {"url":null}},
{"id": "name"},
{"id": "images"}
]
},
"route":{
"letter":"r",
"geoLocalized":true,
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "route_history"},
{"id": "description"},
{"id": "slackline_anchor1", "properties": {"activities":["slacklining"]}},
{"id": "slackline_anchor2", "properties": {"activities":["slacklining"]}},
{"id": "slope", "properties": {"activities":["snow_ice_mixed", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "remarks"},
{"id": "gear"},
{"id": "external_resources"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "activities", "properties": {"url":"act"}},
{"id": "aid_rating", "properties": {"url":"arat", "activities":["rock_climbing", "mountain_climbing"]}},
{"id": "climbing_outdoor_type", "properties": {"url":"crtyp", "activities":["rock_climbing"]}},
{"id": "configuration", "properties": {"url":"conf", "activities":["rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_climbing", "snowshoeing", "skitouring"]}},
{"id": "difficulties_height", "properties": {"url":"ralt", "activities":["rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_climbing", "ice_climbing"]}},
{"id": "durations", "properties": {"url":"time", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "elevation_max", "properties": {"url":"rmaxa", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "elevation_min", "properties": {"url":"rmina", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "engagement_rating", "properties": {"url":"erat", "activities":["rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_climbing", "ice_climbing"]}},
{"id": "equipment_rating", "properties": {"url":"prat", "activities":["rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_climbing", "ice_climbing"]}},
{"id": "exposition_rock_rating", "properties": {"url":"rexpo", "activities":["rock_climbing", "mountain_climbing"]}},
{"id": "glacier_gear", "properties": {"url":"glac", "activities":["hiking", "rock_climbing", "snow_ice_mixed", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "global_rating", "properties": {"url":"grat", "activities":["rock_climbing", "snow_ice_mixed", "mountain_climbing", "ice_climbing"]}},
{"id": "height_diff_access", "properties": {"url":"rappr", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "height_diff_difficulties", "properties": {"url":"dhei", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "height_diff_down", "properties": {"url":"ddif", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "height_diff_up", "properties": {"url":"hdif", "activities":["hiking", "rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_biking", "mountain_climbing", "ice_climbing", "snowshoeing", "skitouring"]}},
{"id": "hiking_mtb_exposition", "properties": {"url":"hexpo", "activities":["hiking", "mountain_biking"]}},
{"id": "hiking_rating", "properties": {"url":"hrat", "activities":["hiking"]}},
{"id": "ice_rating", "properties": {"url":"irat", "activities":["snow_ice_mixed", "ice_climbing"]}},
{"id": "labande_global_rating", "properties": {"url":"lrat", "activities":["skitouring"]}},
{"id": "labande_ski_rating", "properties": {"url":"srat", "activities":["skitouring"]}},
{"id": "lift_access"},
{"id": "main_waypoint_id"},
{"id": "mixed_rating", "properties": {"url":"mrat", "activities":["snow_ice_mixed", "ice_climbing"]}},
{"id": "mtb_down_rating", "properties": {"url":"mbdr", "activities":["mountain_biking"]}},
{"id": "mtb_height_diff_portages", "properties": {"url":"mbpush", "activities":["mountain_biking"]}},
{"id": "mtb_length_asphalt", "properties": {"url":"mbroad", "activities":["mountain_biking"]}},
{"id": "mtb_length_trail", "properties": {"url":"mbtrack", "activities":["mountain_biking"]}},
{"id": "mtb_up_rating", "properties": {"url":"mbur", "activities":["mountain_biking"]}},
{"id": "orientations", "properties": {"url":"fac"}},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "risk_rating", "properties": {"url":"orrat", "activities":["rock_climbing", "snow_ice_mixed", "mountain_climbing", "ice_climbing"]}},
{"id": "rock_free_rating", "properties": {"url":"frat", "activities":["rock_climbing", "mountain_climbing"]}},
{"id": "rock_required_rating", "properties": {"url":"rrat", "activities":["rock_climbing", "mountain_climbing"]}},
{"id": "rock_types", "properties": {"url":"rock", "activities":["rock_climbing", "via_ferrata", "snow_ice_mixed", "mountain_climbing"]}},
{"id": "route_length", "properties": {"url":"rlen", "activities":["hiking", "slacklining", "via_ferrata", "snow_ice_mixed", "mountain_biking", "snowshoeing", "skitouring"]}},
{"id": "route_types", "properties": {"url":"rtyp"}},
{"id": "ski_exposition", "properties": {"url":"sexpo", "activities":["skitouring"]}},
{"id": "ski_rating", "properties": {"url":"trat", "activities":["skitouring"]}},
{"id": "slackline_height", "properties": {"activities":["slacklining"]}},
{"id": "slackline_type", "properties": {"url":"sltyp", "activities":["slacklining"]}},
{"id": "snowshoe_rating", "properties": {"url":"wrat", "activities":["snowshoeing"]}},
{"id": "via_ferrata_rating", "properties": {"url":"krat", "activities":["via_ferrata"]}},
{"id": "articles", "properties": {"url":"c", "associationEditorOrder": 4}},
{"id": "books", "properties": {"url":"b", "associationEditorOrder": 2}},
{"id": "images", "properties": {"associationEditorOrder": 5}},
{"id": "routes", "properties": {"url":"r", "associationEditorOrder": 3}},
{"id": "waypoints", "properties": {"url":"w", "associationEditorOrder": 1}},
{"id": "xreports", "properties": {"url":"x", "associationEditorOrder": 6}}
]
},
"waypoint":{
"letter":"w",
"geoLocalized":true,
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "access", "properties": {"waypoint_types":["access", "climbing_indoor", "climbing_outdoor", "hut", "local_product", "slackline_spot"]}},
{"id": "access_period", "properties": {"waypoint_types":["access", "camp_site", "climbing_outdoor", "gite", "hut", "local_product"]}},
{"id": "lang", "properties": {"url":"l"}},
{"id": "access_time", "properties": {"url":"tappt", "waypoint_types":["climbing_outdoor", "slackline_spot"]}},
{"id": "best_periods", "properties": {"url":"period", "waypoint_types":["climbing_outdoor", "slackline_spot"]}},
{"id": "blanket_unstaffed", "properties": {"waypoint_types":["hut", "shelter"]}},
{"id": "capacity", "properties": {"url":"hucap", "waypoint_types":["bivouac", "camp_site", "gite", "hut", "shelter"]}},
{"id": "capacity_staffed", "properties": {"url":"hscap", "waypoint_types":["camp_site", "gite", "hut"]}},
{"id": "children_proof", "properties": {"url":"chil", "waypoint_types":["climbing_outdoor"]}},
{"id": "climbing_indoor_types", "properties": {"url":"ctin", "waypoint_types":["climbing_indoor"]}},
{"id": "climbing_outdoor_types", "properties": {"url":"ctout", "waypoint_types":["climbing_outdoor"]}},
{"id": "climbing_rating_max", "properties": {"url":"tmaxr", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "climbing_rating_median", "properties": {"url":"tmedr", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "climbing_rating_min", "properties": {"url":"tminr", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "climbing_styles", "properties": {"url":"tcsty", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "custodianship", "properties": {"url":"hsta", "waypoint_types":["camp_site", "gite", "hut"]}},
{"id": "elevation", "properties": {"url":"walt"}},
{"id": "elevation_min", "properties": {"waypoint_types":["access"]}},
{"id": "equipment_ratings", "properties": {"url":"anchq", "waypoint_types":["climbing_outdoor"]}},
{"id": "gas_unstaffed", "properties": {"waypoint_types":["hut", "shelter"]}},
{"id": "ground_types", "properties": {"waypoint_types":["paragliding_landing", "paragliding_takeoff"]}},
{"id": "heating_unstaffed", "properties": {"waypoint_types":["hut", "shelter"]}},
{"id": "height_max", "properties": {"url":"tmaxh", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "height_median", "properties": {"url":"tmedh", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "height_min", "properties": {"url":"tminh", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "length", "properties": {"url":"len", "waypoint_types":["paragliding_landing", "paragliding_takeoff"]}},
{"id": "lift_access", "properties": {"url":"plift", "waypoint_types":["access"]}},
{"id": "maps_info"},
{"id": "matress_unstaffed", "properties": {"waypoint_types":["hut", "shelter"]}},
{"id": "orientations", "properties": {"url":"wfac", "waypoint_types":["climbing_outdoor", "paragliding_landing", "paragliding_takeoff", "slackline_spot"]}},
{"id": "paragliding_rating", "properties": {"url":"pgrat", "waypoint_types":["paragliding_landing", "paragliding_takeoff"]}},
{"id": "parking_fee", "properties": {"waypoint_types":["access"]}},
{"id": "phone", "properties": {"waypoint_types":["camp_site", "climbing_indoor", "gite", "hut", "local_product"]}},
{"id": "phone_custodian", "properties": {"waypoint_types":["camp_site", "gite", "hut"]}},
{"id": "product_types", "properties": {"url":"ftyp", "waypoint_types":["local_product"]}},
{"id": "prominence", "properties": {"url":"prom", "waypoint_types":["summit"]}},
{"id": "public_transportation_rating", "properties": {"url":"tp", "waypoint_types":["access"]}},
{"id": "public_transportation_types", "properties": {"url":"tpty", "waypoint_types":["access"]}},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "rain_proof", "properties": {"url":"rain", "waypoint_types":["climbing_outdoor"]}},
{"id": "rock_types", "properties": {"url":"wrock", "waypoint_types":["climbing_outdoor"]}},
{"id": "routes_quantity", "properties": {"url":"rqua", "waypoint_types":["climbing_indoor", "climbing_outdoor"]}},
{"id": "slackline_length_max", "properties": {"waypoint_types":["slackline_spot"]}},
{"id": "slackline_length_min", "properties": {"waypoint_types":["slackline_spot"]}},
{"id": "slackline_types", "properties": {"waypoint_types":["slackline_spot"]}},
{"id": "waypoint_slope", "properties": {"waypoint_types":["paragliding_landing", "paragliding_takeoff"]}},
{"id": "snow_clearance_rating", "properties": {"url":"psnow", "waypoint_types":["access"]}},
{"id": "url", "properties": {"waypoint_types":["camp_site", "climbing_indoor", "climbing_outdoor", "gite", "hut", "local_product", "weather_station", "webcam"]}},
{"id": "waypoint_type", "properties": {"url":"wtyp"}},
{"id": "weather_station_types", "properties": {"waypoint_types": ["weather_station"], "url":"whtyp"}},
{"id": "articles", "properties": {"url":"c", "associationEditorOrder": 3}},
{"id": "books", "properties": {"url":"b", "associationEditorOrder": 2}},
{"id": "images", "properties": {"associationEditorOrder": 4}},
{"id": "waypoints", "properties": {"url":"w", "associationEditorOrder": 1}},
{"id": "waypoints", "properties": {"name": "waypoint_children", "url":"w", "associationEditorOrder": 1}}
]
},
"xreport":{
"letter":"x",
"geoLocalized":true,
"fields":[
{"id": "title"},
{"id": "summary"},
{"id": "description"},
{"id": "place"},
{"id": "route_study"},
{"id": "conditions"},
{"id": "training"},
{"id": "motivations"},
{"id": "group_management"},
{"id": "risk"},
{"id": "time_management"},
{"id": "safety"},
{"id": "reduce_impact"},
{"id": "increase_impact"},
{"id": "modifications"},
{"id": "other_comments"},
{"id": "lang", "properties": {"url":"l"}},
{"id": "elevation", "properties": {"url":"xalt"}},
{"id": "date"},
{"id": "event_type", "properties": {"url":"xtyp"}},
{"id": "event_activity", "properties": {"url":"act"}},
{"id": "nb_participants", "properties": {"url":"xpar"}},
{"id": "nb_impacted", "properties": {"url":"ximp"}},
{"id": "rescue"},
{"id": "avalanche_level"},
{"id": "avalanche_slope"},
{"id": "severity", "properties": {"url":"xsev"}},
{"id": "author_status"},
{"id": "activity_rate"},
{"id": "age"},
{"id": "gender"},
{"id": "previous_injuries"},
{"id": "autonomy"},
{"id": "qualification"},
{"id": "supervision"},
{"id": "disable_comments"},
{"id": "anonymous"},
{"id": "quality", "properties": {"url":"qa"}},
{"id": "articles", "properties": {"url":"c", "helper": null, "associationEditorOrder": 6}},
{"id": "images", "properties": {"associationEditorOrder":5}},
{"id": "outings", "properties": {"url":"o", "helper": null, "associationEditorOrder": 1}},
{"id": "routes", "properties": {"url":"r", "helper": null, "associationEditorOrder": 3}},
{"id": "users", "properties": {"associationEditorOrder":2}},
{"id": "waypoints", "properties": {"associationEditorOrder":4}}
]
}
}

View File

@ -0,0 +1,727 @@
{
"disable_comments": null,
"document_id": 547170,
"areas": [
{
"type": "a",
"available_langs": null,
"document_id": 14067,
"version": 10,
"locales": [
{
"version": 1,
"title": "Suisse",
"lang": "fr"
}
],
"area_type": "country",
"protected": false
},
{
"type": "a",
"available_langs": null,
"document_id": 14384,
"version": 9,
"locales": [
{
"version": 12,
"title": "Valais",
"lang": "fr"
}
],
"area_type": "admin_limits",
"protected": false
},
{
"type": "a",
"available_langs": null,
"document_id": 14437,
"version": 6,
"locales": [
{
"version": 7,
"title": "Valais W - Alpes Pennines W",
"lang": "fr"
}
],
"area_type": "range",
"protected": false
}
],
"version": 2,
"frequentation": "quiet",
"geometry": {
"version": 1,
"geom": "{\"coordinates\": [796963.8418227625, 5777640.64308189], \"type\": \"Point\"}",
"geom_detail": null
},
"associations": {
"articles": [],
"xreports": [],
"users": [
{
"forum_username": "Yayo",
"document_id": 737,
"name": "Yayo",
"areas": [
{
"document_id": 14274,
"available_langs": null,
"type": "a",
"version": 11,
"locales": [
{
"version": 5,
"title": "France",
"lang": "fr"
}
],
"area_type": "country",
"protected": false
},
{
"document_id": 14335,
"available_langs": null,
"type": "a",
"version": 3,
"locales": [
{
"version": 3,
"title": "Haute-Garonne",
"lang": "fr"
}
],
"area_type": "admin_limits",
"protected": false
}
],
"activities": [
"mountain_climbing",
"snow_ice_mixed",
"hiking",
"paragliding",
"snowshoeing",
"skitouring",
"rock_climbing",
"ice_climbing"
],
"available_langs": [
"fr"
],
"type": "u",
"version": 3,
"locales": [
{
"version": 77,
"lang": "fr"
}
],
"protected": false,
"categories": [
"amateur"
]
},
{
"forum_username": "etoile",
"document_id": 106794,
"name": "etoile",
"areas": [],
"activities": [
"skitouring",
"snow_ice_mixed",
"mountain_climbing",
"hiking",
"snowshoeing",
"rock_climbing",
"ice_climbing"
],
"available_langs": [
"fr"
],
"type": "u",
"version": 2,
"locales": [
{
"version": 1,
"lang": "fr"
}
],
"protected": false,
"categories": [
"amateur"
]
}
],
"routes": [
{
"orientations": [
"SE"
],
"document_id": 57103,
"equipment_rating": "P1",
"version": 4,
"activities": [
"rock_climbing"
],
"geometry": {
"has_geom_detail": false,
"version": 11,
"geom": "{\"coordinates\": [796963.8418227625, 5777640.64308189], \"type\": \"Point\"}"
},
"available_langs": [
"es",
"en",
"fr"
],
"aid_rating": null,
"risk_rating": null,
"areas": [
{
"document_id": 14067,
"available_langs": null,
"type": "a",
"version": 10,
"locales": [
{
"version": 1,
"title": "Suisse",
"lang": "fr"
}
],
"area_type": "country",
"protected": false
},
{
"document_id": 14384,
"available_langs": null,
"type": "a",
"version": 9,
"locales": [
{
"version": 12,
"title": "Valais",
"lang": "fr"
}
],
"area_type": "admin_limits",
"protected": false
},
{
"document_id": 14437,
"available_langs": null,
"type": "a",
"version": 6,
"locales": [
{
"version": 7,
"title": "Valais W - Alpes Pennines W",
"lang": "fr"
}
],
"area_type": "range",
"protected": false
}
],
"exposition_rock_rating": null,
"height_diff_down": null,
"elevation_min": null,
"height_diff_difficulties": 400,
"engagement_rating": null,
"locales": [
{
"lang": "fr",
"version": 22,
"title_prefix": "Tour de Bavon",
"title": "Thor",
"summary": "Belle et longue voie \u00e9quip\u00e9e sur des dalles compactes qui sort sous le sommet. "
}
],
"protected": false,
"rock_required_rating": "5c",
"rock_free_rating": "6a",
"type": "r",
"elevation_max": 2476,
"height_diff_up": null,
"global_rating": "TD-",
"quality": "medium"
}
],
"images": [
{
"document_id": 547972,
"type": "i",
"areas": [],
"filename": "1407166923_293646121.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "Le pont sur l'A (1750m) \u00e0 partir duquel il faut rester en rive gauche.",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"type": "i",
"areas": [],
"author": null,
"filename": "1407166942_1005392824.jpg",
"available_langs": [
"fr"
],
"document_id": 547973,
"version": 1,
"locales": [
{
"version": 1,
"title": "Qd vs passez au pied de cette dalle attirante, ne cherchez pas, THOR n'est PAS L\u00e0...",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 547974,
"type": "i",
"areas": [],
"filename": "1407166959_1186093463.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : d\u00e9part de la voie dans la dalle blanche, imm\u00e9diatement \u00e0 droite de la rampe d'herbe oblique",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"type": "i",
"areas": [],
"author": null,
"filename": "1407526184_1731628825.jpg",
"available_langs": [
"fr"
],
"document_id": 548914,
"version": 1,
"locales": [
{
"version": 1,
"title": "La dalle d'attaque, blanche, tout \u00e2 gauche",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548915,
"type": "i",
"areas": [],
"filename": "1407526245_461968093.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: L1, grande dalle, des \u00e9cailles pos\u00e9es sur le r\u00e9glettes, R1 \u00e0 D",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548916,
"type": "i",
"areas": [],
"filename": "1407526253_1591812939.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: L4 sur le vague \u00e9peron au centre, \u00e0 droite de la cavit\u00e9",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548937,
"type": "i",
"areas": [],
"filename": "1407526536_793924939.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : L5",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"type": "i",
"areas": [],
"author": null,
"filename": "1407526546_1198758627.jpg",
"available_langs": [
"fr"
],
"document_id": 548938,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: vue plongeante depuis R4",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548939,
"type": "i",
"areas": [],
"filename": "1407526557_1468063167.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : L6",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548940,
"type": "i",
"areas": [],
"filename": "1407526566_1671042872.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : L7",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548941,
"type": "i",
"areas": [],
"filename": "1407526578_453362453.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: L8, sur de grandes dalles, vers la grande fissure",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548942,
"type": "i",
"areas": [],
"filename": "1407526591_223027668.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : Tr\u00e8s belle L9, sur un caillou correct!",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548943,
"type": "i",
"areas": [],
"filename": "1407526601_981616281.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: fin de L9 avec un petit di\u00e8dre raide",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"type": "i",
"areas": [],
"author": null,
"filename": "1407526615_1928563952.jpg",
"available_langs": [
"fr"
],
"document_id": 548944,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR : L10",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548945,
"type": "i",
"areas": [],
"filename": "1407526622_1455324122.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: \u00c9toile sortie ! (R10)",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"type": "i",
"areas": [],
"author": null,
"filename": "1407526644_995429802.jpg",
"available_langs": [
"fr"
],
"document_id": 548946,
"version": 1,
"locales": [
{
"version": 1,
"title": "Edelweiss",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548947,
"type": "i",
"areas": [],
"filename": "1407526652_629783242.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "Duo d'Edelweiss",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
},
{
"document_id": 548948,
"type": "i",
"areas": [],
"filename": "1407526661_962365898.jpg",
"available_langs": [
"fr"
],
"author": null,
"version": 1,
"locales": [
{
"version": 1,
"title": "THOR: pentes de descente, tirer vers les arbres",
"lang": "fr"
}
],
"geometry": {
"version": 1,
"geom": null
},
"protected": false
}
]
},
"access_condition": null,
"partial_trip": null,
"available_langs": [
"fr"
],
"height_diff_down": null,
"equipment_rating": "P1",
"elevation_min": null,
"public_transport": null,
"participant_count": null,
"elevation_access": null,
"elevation_max": 2476,
"date_start": "2014-07-31",
"locales": [
{
"weather": "Bouch\u00e9 le matin, nuages 50m au dessus de la base des dalles puis \u00e9claircies. ",
"conditions_levels": null,
"route_description": null,
"summary": null,
"version": 4,
"access_comment": null,
"timing": "Approche : 2h30 avec la recherche au pied de la mauvaise dalle. \r\nVoie : 6h",
"participants": null,
"title": "Tour de Bavon\u00a0: Thor",
"lang": "fr",
"hut_comment": null,
"topic_id": null,
"description": "Jolie voie mais le caillou demande de l'attention. Selon \u00c9toile : \"\u00e7a manque de colle entre les dalles de schistes!!\"\r\n\r\nPour l'approche, on est mont\u00e9 trop t\u00f4t \u00e0 droite (300m trop au N), vers les premi\u00e8res grandes dalles au-dessus du chemin, faute de visibilit\u00e9. On a cherch\u00e9 les spits une bonne heure en longeant la paroi. On est redescendu puis on a poursuivi le long du ruisseau jusqu'au pied de la bonne rampe. Indication : poursuivre dans le fond du vallon jusqu'\u00e0 passer un replat avec 3 \u00e9normes blocs. Poursuivre 100m puis obliquer a droite vers une dalle blanche surmont\u00e9e d'un grand toit sombre. (Mise a jour topo a venir)\r\n\r\n[TROUV\u00c9](http://m.camptocamp.org/forums/viewforum.php?id=78) : un brin de rappel au pied de la paroi 300m au N de la voie. ",
"conditions": "## Rocher\r\nSec dans l'ensemble sauf L2, tremp\u00e9e sous le 2\u00e8me point. \r\nPas mal de sections au rocher moyen, dont le ressaut final de L2 et du d\u00e9but de L5, bien p\u00e9teux. Bcp de vigilance n\u00e9cessaire tout le long pour ne pas partir avec une \u00e9caille. \r\nBcp de cailloux pos\u00e9s \u00e9galement et qui n'attendent que le passage de la corde pour se d\u00e9loger. \r\n\r\n## Equipement\r\n\u00c9quip\u00e9 a\u00e9r\u00e9 mais bien \u00e9quip\u00e9.\r\n\r\n\r\n"
}
],
"protected": false,
"rock_free_rating": "6a",
"date_end": "2014-07-31",
"type": "o",
"condition_rating": "good",
"activities": [
"rock_climbing"
],
"hut_status": null,
"height_diff_up": null,
"global_rating": "TD-",
"length_total": null,
"quality": "medium",
"cooked": {
"weather": "<p>Bouch\u00e9 le matin, nuages 50m au dessus de la base des dalles puis \u00e9claircies. </p>",
"conditions_levels": null,
"route_description": null,
"summary": null,
"version": 4,
"access_comment": null,
"timing": "<p>Approche&nbsp;: 2h30&nbsp;avec la recherche au pied de la mauvaise dalle. <br>\nVoie&nbsp;: 6h</p>",
"participants": null,
"title": "Tour de Bavon\u00a0: Thor",
"lang": "fr",
"hut_comment": null,
"topic_id": null,
"description": "<p>Jolie voie mais le caillou demande de l'attention. Selon \u00c9toile&nbsp;: \"\u00e7a manque de colle entre les dalles de schistes!!\"</p>\n<p>Pour l'approche, on est mont\u00e9 trop t\u00f4t \u00e0 droite (300m trop au N), vers les premi\u00e8res grandes dalles au-dessus du chemin, faute de visibilit\u00e9. On a cherch\u00e9 les spits une bonne heure en longeant la paroi. On est redescendu puis on a poursuivi le long du ruisseau jusqu'au pied de la bonne rampe. Indication&nbsp;: poursuivre dans le fond du vallon jusqu'\u00e0 passer un replat avec 3 \u00e9normes blocs. Poursuivre 100m puis obliquer a droite vers une dalle blanche surmont\u00e9e d'un grand toit sombre. (Mise a jour topo a venir)</p>\n<p><a href=\"http://m.camptocamp.org/forums/viewforum.php?id=78\">TROUV\u00c9</a> : un brin de rappel au pied de la paroi 300m au N de la voie. </p>",
"conditions": "<h3 id=\"rocher\">Rocher</h3>\n<p>Sec dans l'ensemble sauf L2, tremp\u00e9e sous le 2\u00e8me point. <br>\nPas mal de sections au rocher moyen, dont le ressaut final de L2&nbsp;et du d\u00e9but de L5, bien p\u00e9teux. Bcp de vigilance n\u00e9cessaire tout le long pour ne pas partir avec une \u00e9caille. <br>\nBcp de cailloux pos\u00e9s \u00e9galement et qui n'attendent que le passage de la corde pour se d\u00e9loger. </p>\n<h3 id=\"equipement\">Equipement</h3>\n<p>\u00c9quip\u00e9 a\u00e9r\u00e9 mais bien \u00e9quip\u00e9.</p>"
},
"lift_status": null
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
{
"img_count": 0,
"geometry": {
"has_geom_detail": false,
"version": 1,
"geom": "{\"coordinates\": [796963.8418227625, 5777640.64308189], \"type\": \"Point\"}"
},
"equipment_rating": "P1",
"elevation_max": 2476,
"date_start": "2015-09-06",
"locales": [
{
"lang": "fr",
"title": "Tour de Bavon : Thor",
"version": 1,
"summary": null
}
],
"author": {
"user_id": 253878,
"name": "Mireille Vuadens"
},
"protected": false,
"rock_free_rating": "6a",
"date_end": "2015-09-06",
"height_diff_up": null,
"condition_rating": "excellent",
"areas": [
{
"document_id": 14437,
"available_langs": null,
"type": "a",
"version": 6,
"locales": [
{
"version": 7,
"title": "Valais W - Alpes Pennines W",
"lang": "fr"
}
],
"area_type": "range",
"protected": false
},
{
"document_id": 14384,
"available_langs": null,
"type": "a",
"version": 9,
"locales": [
{
"version": 12,
"title": "Valais",
"lang": "fr"
}
],
"area_type": "admin_limits",
"protected": false
},
{
"document_id": 14067,
"available_langs": null,
"type": "a",
"version": 10,
"locales": [
{
"version": 1,
"title": "Suisse",
"lang": "fr"
}
],
"area_type": "country",
"protected": false
}
],
"activities": [
"rock_climbing"
],
"available_langs": [
"fr"
],
"document_id": 673121,
"global_rating": "TD-",
"quality": "medium",
"version": 2,
"type": "o"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff