chore: add Kotlin directory structure, update registry.db and gitignore

Añade estructura inicial kotlin/functions/, actualiza registry.db con todos
los cambios indexados, y ajusta .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 23:47:27 +02:00
parent dede5e00af
commit 73d181f2b0
13 changed files with 1005 additions and 1 deletions
@@ -0,0 +1,102 @@
package infra
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
data class GeocodedLocation(
val displayName: String,
val street: String,
val houseNumber: String,
val neighbourhood: String,
val city: String,
val state: String,
val country: String,
val postcode: String,
val lat: Double,
val lon: Double,
val osmType: String,
val osmId: Long
)
/**
* nominatimReverseGeocode — Convierte coordenadas lat/lon a una dirección estructurada
* usando la API pública de Nominatim (OpenStreetMap).
*
* @param lat Latitud en grados decimales.
* @param lon Longitud en grados decimales.
* @param lang Código de idioma ISO 639-1 para la respuesta. Por defecto "es".
* @return GeocodedLocation con los campos normalizados de la dirección.
* @throws RuntimeException si la petición HTTP falla o el servidor retorna error.
*/
fun nominatimReverseGeocode(lat: Double, lon: Double, lang: String = "es"): GeocodedLocation {
val url = URL(
"https://nominatim.openstreetmap.org/reverse" +
"?format=jsonv2" +
"&lat=$lat" +
"&lon=$lon" +
"&accept-language=$lang" +
"&zoom=18"
)
val conn = url.openConnection() as HttpURLConnection
conn.setRequestProperty("User-Agent", "fn_registry/1.0")
conn.connectTimeout = 5_000
conn.readTimeout = 5_000
conn.requestMethod = "GET"
val responseCode = conn.responseCode
if (responseCode != HttpURLConnection.HTTP_OK) {
conn.disconnect()
throw RuntimeException(
"nominatimReverseGeocode: HTTP $responseCode para lat=$lat, lon=$lon"
)
}
val body = try {
conn.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
} catch (e: Exception) {
conn.disconnect()
throw RuntimeException(
"nominatimReverseGeocode: error leyendo respuesta — ${e.message}", e
)
} finally {
conn.disconnect()
}
val data = JSONObject(body)
val address = data.optJSONObject("address") ?: JSONObject()
val city = when {
address.has("city") && address.getString("city").isNotEmpty() ->
address.getString("city")
address.has("town") && address.getString("town").isNotEmpty() ->
address.getString("town")
address.has("village") && address.getString("village").isNotEmpty() ->
address.getString("village")
else -> ""
}
val neighbourhood = when {
address.has("neighbourhood") && address.getString("neighbourhood").isNotEmpty() ->
address.getString("neighbourhood")
address.has("suburb") && address.getString("suburb").isNotEmpty() ->
address.getString("suburb")
else -> ""
}
return GeocodedLocation(
displayName = data.optString("display_name", ""),
street = address.optString("road", ""),
houseNumber = address.optString("house_number", ""),
neighbourhood = neighbourhood,
city = city,
state = address.optString("state", ""),
country = address.optString("country", ""),
postcode = address.optString("postcode", ""),
lat = data.optString("lat", lat.toString()).toDoubleOrNull() ?: lat,
lon = data.optString("lon", lon.toString()).toDoubleOrNull() ?: lon,
osmType = data.optString("osm_type", ""),
osmId = data.optLong("osm_id", 0L)
)
}
@@ -0,0 +1,58 @@
---
name: nominatim_reverse_geocode
kind: function
lang: kt
domain: infra
version: "1.0.0"
purity: impure
signature: "fun nominatimReverseGeocode(lat: Double, lon: Double, lang: String = \"es\"): GeocodedLocation"
description: "Reverse geocoding usando Nominatim (OpenStreetMap). Convierte coordenadas lat/lon a una dirección estructurada (calle, ciudad, país, etc.) para Android/Kotlin sin dependencias externas."
tags: [geocoding, nominatim, openstreetmap, location, address, infra, android, kotlin]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "org.json.JSONObject"
- "java.net.HttpURLConnection"
- "java.net.URL"
tested: false
tests: []
test_file_path: ""
file_path: "kotlin/functions/infra/nominatim_reverse_geocode.kt"
params:
- name: lat
desc: "Latitud en grados decimales (-90.0 a 90.0)."
- name: lon
desc: "Longitud en grados decimales (-180.0 a 180.0)."
- name: lang
desc: "Código de idioma ISO 639-1 para la respuesta de Nominatim. Por defecto \"es\" (español)."
output: "GeocodedLocation con los campos normalizados: displayName, street, houseNumber, neighbourhood, city, state, country, postcode, lat, lon, osmType, osmId."
---
## Ejemplo
```kotlin
val loc = nominatimReverseGeocode(40.4168, -3.7038)
println(loc.city) // "Madrid"
println(loc.street) // "Calle Mayor"
println(loc.country) // "España"
println(loc.displayName) // cadena completa de Nominatim
```
## Comportamiento
- Realiza GET a `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=...&lon=...&accept-language=...&zoom=18`.
- Cabecera `User-Agent: fn_registry/1.0` obligatoria por política de uso de Nominatim.
- Timeout de 5 segundos tanto para conexión como para lectura.
- Usa solo `java.net.HttpURLConnection` y `org.json.JSONObject` — disponibles en el Android SDK sin dependencias externas.
- Resolución de `city`: prueba `address.city``address.town``address.village``""`.
- Resolución de `neighbourhood`: prueba `address.neighbourhood``address.suburb``""`.
- Lanza `RuntimeException` si el HTTP status no es 200 o si falla la lectura de la respuesta.
## Notas
Función paralela a `nominatim_reverse_geocode_py_infra` pero adaptada para Android/Kotlin.
No llamar desde el hilo principal de Android — usar `Dispatchers.IO` o un `ExecutorService`.
El campo `zoom=18` maximiza el detalle de la dirección (nivel de número de portal).
+101
View File
@@ -0,0 +1,101 @@
package infra
import org.json.JSONArray
import org.json.JSONObject
import java.io.ConnectException
import java.net.HttpURLConnection
import java.net.URL
data class OllamaChatResponse(
val content: String,
val model: String,
val totalDurationMs: Long,
val evalCount: Int
)
/**
* Sends a chat completion request to a local Ollama instance.
*
* POSTs to `$baseUrl/api/chat` with the given messages and returns the
* generated content together with timing and token-count metrics.
*
* @param messages List of role/content maps, e.g. [{"role":"user","content":"hi"}].
* @param model Ollama model tag to use.
* @param baseUrl Base URL of the Ollama server.
* @param temperature Sampling temperature (0.0 1.0).
* @param maxTokens Maximum number of tokens to generate.
* @return OllamaChatResponse with content, model name, duration and eval count.
* @throws RuntimeException if Ollama is not reachable or the server returns an error.
*/
fun ollamaChat(
messages: List<Map<String, String>>,
model: String = "llama3.1:8b",
baseUrl: String = "http://localhost:11434",
temperature: Double = 0.7,
maxTokens: Int = 1024
): OllamaChatResponse {
val messagesArray = JSONArray().apply {
for (msg in messages) {
put(JSONObject(msg as Map<*, *>))
}
}
val options = JSONObject().apply {
put("temperature", temperature)
put("num_predict", maxTokens)
}
val body = JSONObject().apply {
put("model", model)
put("messages", messagesArray)
put("stream", false)
put("options", options)
}.toString().toByteArray(Charsets.UTF_8)
val url = URL("$baseUrl/api/chat")
val conn: HttpURLConnection
try {
conn = url.openConnection() as HttpURLConnection
} catch (e: ConnectException) {
throw RuntimeException("Ollama no está corriendo en $baseUrl", e)
}
try {
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
conn.setRequestProperty("Accept", "application/json")
conn.connectTimeout = 60_000
conn.readTimeout = 60_000
conn.doOutput = true
try {
conn.outputStream.use { it.write(body) }
} catch (e: ConnectException) {
throw RuntimeException("Ollama no está corriendo en $baseUrl", e)
}
val statusCode = conn.responseCode
if (statusCode != HttpURLConnection.HTTP_OK) {
val errorBody = conn.errorStream?.bufferedReader()?.readText() ?: ""
throw RuntimeException("Ollama devolvió HTTP $statusCode: $errorBody")
}
val responseText = conn.inputStream.bufferedReader().readText()
val json = JSONObject(responseText)
val content = json.getJSONObject("message").getString("content")
val returnedModel = json.optString("model", model)
val totalDurationNs = json.optLong("total_duration", 0L)
val evalCount = json.optInt("eval_count", 0)
return OllamaChatResponse(
content = content,
model = returnedModel,
totalDurationMs = totalDurationNs / 1_000_000L,
evalCount = evalCount
)
} finally {
conn.disconnect()
}
}
+68
View File
@@ -0,0 +1,68 @@
---
name: ollama_chat
kind: function
lang: kt
domain: infra
version: "1.0.0"
purity: impure
signature: "fun ollamaChat(messages: List<Map<String, String>>, model: String = \"llama3.1:8b\", baseUrl: String = \"http://localhost:11434\", temperature: Double = 0.7, maxTokens: Int = 1024): OllamaChatResponse"
description: "Envía una solicitud de chat completion a un servidor Ollama local. Retorna el contenido generado junto a métricas de duración y tokens evaluados."
tags: [ollama, llm, chat, inference, http, android, kotlin]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "org.json.JSONObject"
- "org.json.JSONArray"
- "java.net.HttpURLConnection"
- "java.net.URL"
tested: false
tests: []
test_file_path: ""
file_path: "kotlin/functions/infra/ollama_chat.kt"
params:
- name: messages
desc: "Lista de mapas role/content en formato OpenAI/Ollama. Ej: [{\"role\":\"user\",\"content\":\"Hola\"}]."
- name: model
desc: "Tag del modelo Ollama a usar. Por defecto 'llama3.1:8b'."
- name: baseUrl
desc: "URL base del servidor Ollama. Por defecto 'http://localhost:11434'."
- name: temperature
desc: "Temperatura de muestreo entre 0.0 (determinista) y 1.0 (creativo). Por defecto 0.7."
- name: maxTokens
desc: "Número máximo de tokens a generar. Por defecto 1024."
output: "OllamaChatResponse con content (texto generado), model (nombre del modelo), totalDurationMs (duración total en milisegundos) y evalCount (tokens evaluados)."
---
## Ejemplo
```kotlin
val messages = listOf(
mapOf("role" to "system", "content" to "Eres un asistente útil."),
mapOf("role" to "user", "content" to "¿Cuál es la capital de Francia?")
)
val response = ollamaChat(
messages = messages,
model = "llama3.1:8b",
temperature = 0.3
)
println(response.content) // "La capital de Francia es París."
println(response.totalDurationMs) // 1234
println(response.evalCount) // 42
```
## Notas
Usa `java.net.HttpURLConnection` y `org.json.JSONObject` del Android SDK — sin dependencias externas adicionales. Timeout de 60 segundos tanto en conexión como en lectura.
La respuesta de Ollama incluye `total_duration` en nanosegundos; se convierte a milisegundos dividiendo entre 1_000_000.
Si Ollama no está corriendo lanza `RuntimeException("Ollama no está corriendo en $baseUrl")`. Si el servidor responde con un código HTTP distinto de 200, lanza `RuntimeException` con el código y el cuerpo del error.
El campo `stream` se fija a `false` para recibir la respuesta completa en una sola llamada.
```
---
@@ -0,0 +1,146 @@
package infra
import org.json.JSONObject
import org.json.JSONArray
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
data class POI(
val id: Long,
val lat: Double,
val lon: Double,
val name: String,
val category: String,
val tags: Map<String, String>
)
private val CATEGORY_TAG_MAP: Map<String, String> = mapOf(
"restaurant" to "amenity=restaurant",
"cafe" to "amenity=cafe",
"bar" to "amenity=bar",
"museum" to "tourism=museum",
"monument" to "historic=monument",
"park" to "leisure=park",
"library" to "amenity=library",
"theatre" to "amenity=theatre",
"cinema" to "amenity=cinema",
"gallery" to "tourism=gallery",
"historic" to "historic",
"tourism" to "tourism",
"shop" to "shop",
"hotel" to "tourism=hotel",
"pharmacy" to "amenity=pharmacy",
"hospital" to "amenity=hospital"
)
/** Builds the Overpass QL node clause for a single category tag expression. */
private fun nodeClause(tagExpr: String, radiusM: Int, lat: Double, lon: Double): String {
return if (tagExpr.contains('=')) {
val (key, value) = tagExpr.split('=', limit = 2)
"""node["$key"="$value"](around:$radiusM,$lat,$lon);"""
} else {
// Wildcard: match any value for the key
"""node["$tagExpr"](around:$radiusM,$lat,$lon);"""
}
}
/**
* OverpassNearbyPois queries the Overpass API (OpenStreetMap) to retrieve POIs
* near the given coordinates within the specified radius.
*
* Uses only the Android SDK (java.net.HttpURLConnection + org.json) — no external
* dependencies. Throws RuntimeException on HTTP or parse errors.
*
* @param lat Latitude of the center point (WGS84)
* @param lon Longitude of the center point (WGS84)
* @param radiusM Search radius in metres (default: 500)
* @param categories Subset of supported categories to filter by, or null for all
* @return List of POIs found, may be empty
*/
fun overpassNearbyPois(
lat: Double,
lon: Double,
radiusM: Int = 500,
categories: List<String>? = null
): List<POI> {
val resolved: Map<String, String> = if (categories == null) {
CATEGORY_TAG_MAP
} else {
CATEGORY_TAG_MAP.filterKeys { it in categories }
}
if (resolved.isEmpty()) {
throw RuntimeException("No valid categories resolved from: $categories")
}
val nodeClauses = resolved.values.joinToString("\n ") { tagExpr ->
nodeClause(tagExpr, radiusM, lat, lon)
}
val query = "[timeout:10][out:json];\n(\n $nodeClauses\n);\nout body;"
val endpoint = "https://overpass-api.de/api/interpreter"
val encodedData = "data=" + URLEncoder.encode(query, "UTF-8")
val conn = (URL(endpoint).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = 10_000
readTimeout = 10_000
doOutput = true
setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
setRequestProperty("Accept", "application/json")
}
try {
conn.outputStream.use { it.write(encodedData.toByteArray(Charsets.UTF_8)) }
val code = conn.responseCode
if (code != 200) {
val err = conn.errorStream?.bufferedReader()?.readText() ?: ""
throw RuntimeException("Overpass API returned HTTP $code: $err")
}
val body = conn.inputStream.bufferedReader().readText()
val root = JSONObject(body)
val elements: JSONArray = root.optJSONArray("elements")
?: return emptyList()
val tagExprByCategory: Map<String, String> = resolved.entries
.associate { (cat, tagExpr) -> tagExpr to cat }
val pois = mutableListOf<POI>()
for (i in 0 until elements.length()) {
val el = elements.getJSONObject(i)
if (el.optString("type") != "node") continue
val id = el.getLong("id")
val eLat = el.getDouble("lat")
val eLon = el.getDouble("lon")
val tagsObj: JSONObject = el.optJSONObject("tags") ?: JSONObject()
// Build tags map
val tagsMap = mutableMapOf<String, String>()
for (key in tagsObj.keys()) {
tagsMap[key] = tagsObj.getString(key)
}
// Determine category from the first matching tag expression
val category = tagExprByCategory.entries.firstOrNull { (tagExpr, _) ->
if (tagExpr.contains('=')) {
val (k, v) = tagExpr.split('=', limit = 2)
tagsMap[k] == v
} else {
tagsMap.containsKey(tagExpr)
}
}?.value ?: "unknown"
val name = tagsMap["name"] ?: category.takeIf { it != "unknown" } ?: "Unknown"
pois.add(POI(id = id, lat = eLat, lon = eLon, name = name, category = category, tags = tagsMap))
}
return pois
} finally {
conn.disconnect()
}
}
@@ -0,0 +1,99 @@
---
name: overpass_nearby_pois
kind: function
lang: kt
domain: infra
version: "1.0.0"
purity: impure
signature: "fun overpassNearbyPois(lat: Double, lon: Double, radiusM: Int = 500, categories: List<String>? = null): List<POI>"
description: "Consulta la Overpass API (OpenStreetMap) para obtener POIs cercanos a una coordenada. Soporta 16 categorias mapeadas a tags OSM (amenity, tourism, historic, leisure, shop). Sin dependencias externas: solo Android SDK (HttpURLConnection + org.json)."
tags: [overpass, openstreetmap, osm, poi, geospatial, location, android, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "org.json.JSONObject"
- "org.json.JSONArray"
- "java.net.HttpURLConnection"
- "java.net.URL"
- "java.net.URLEncoder"
tested: false
tests: []
test_file_path: ""
file_path: "kotlin/functions/infra/overpass_nearby_pois.kt"
params:
- name: lat
desc: "Latitud del punto central en WGS84 (ej: 40.4168 para Madrid)"
- name: lon
desc: "Longitud del punto central en WGS84 (ej: -3.7038 para Madrid)"
- name: radiusM
desc: "Radio de busqueda en metros. Default 500. Valores grandes aumentan el tiempo de respuesta."
- name: categories
desc: "Lista de categorias a filtrar: restaurant, cafe, bar, museum, monument, park, library, theatre, cinema, gallery, historic, tourism, shop, hotel, pharmacy, hospital. Null devuelve todas."
output: "Lista de POI con id OSM, coordenadas (lat/lon), nombre, categoria y mapa completo de tags OSM. Lanza RuntimeException si la API devuelve error HTTP o la respuesta no es JSON valido."
---
## Ejemplo
```kotlin
// Restaurantes y museos en un radio de 300 metros
val pois = overpassNearbyPois(
lat = 40.4168,
lon = -3.7038,
radiusM = 300,
categories = listOf("restaurant", "museum")
)
pois.forEach { poi ->
println("${poi.name} (${poi.category}) — lat=${poi.lat}, lon=${poi.lon}")
}
// Todos los POIs en 500 metros (null = todas las categorias)
val all = overpassNearbyPois(lat = 48.8566, lon = 2.3522)
println("Paris: ${all.size} POIs encontrados")
```
## Categorias soportadas
| Categoria | Tag OSM |
|--------------|----------------------|
| restaurant | amenity=restaurant |
| cafe | amenity=cafe |
| bar | amenity=bar |
| museum | tourism=museum |
| monument | historic=monument |
| park | leisure=park |
| library | amenity=library |
| theatre | amenity=theatre |
| cinema | amenity=cinema |
| gallery | tourism=gallery |
| historic | historic (wildcard) |
| tourism | tourism (wildcard) |
| shop | shop (wildcard) |
| hotel | tourism=hotel |
| pharmacy | amenity=pharmacy |
| hospital | amenity=hospital |
## Query Overpass QL generada
```
[timeout:10][out:json];
(
node["amenity"="restaurant"](around:500,40.4168,-3.7038);
node["tourism"="museum"](around:500,40.4168,-3.7038);
);
out body;
```
## Notas
- Usa `java.net.HttpURLConnection` con POST y `data=<query_url_encoded>`.
- Timeouts: 10 segundos de conexion y lectura. La query QL tambien declara `[timeout:10]`.
- El tipo `POI` se define en el mismo archivo como `data class`.
- El mapa `tags` contiene todos los tags OSM del nodo, no solo el de la categoria.
- Si un nodo no tiene tag `name`, el fallback es la categoria y luego "Unknown".
- Las categorias wildcard (historic, tourism, shop) usan `node["key"](around:...)` sin valor.
- Lanza `RuntimeException` en caso de categorias invalidas, error HTTP, o JSON malformado.
- Compatible con Android SDK >= 19 (API level 19 tiene org.json integrado).
---