mcp-swiss 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/geodata.js +83 -29
- package/dist/modules/transport.js +52 -5
- package/package.json +1 -1
- package/src/modules/geodata.ts +136 -34
- package/src/modules/transport.ts +128 -10
package/dist/modules/geodata.js
CHANGED
|
@@ -4,6 +4,45 @@ exports.geodataTools = void 0;
|
|
|
4
4
|
exports.handleGeodata = handleGeodata;
|
|
5
5
|
const http_js_1 = require("../utils/http.js");
|
|
6
6
|
const BASE = "https://api3.geo.admin.ch";
|
|
7
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
|
+
function slimSearchResult(r) {
|
|
9
|
+
const a = r.attrs;
|
|
10
|
+
return {
|
|
11
|
+
label: a.label,
|
|
12
|
+
lat: a.lat,
|
|
13
|
+
lon: a.lon,
|
|
14
|
+
type: a.origin,
|
|
15
|
+
detail: a.detail,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function slimSolarResult(r) {
|
|
19
|
+
const a = r.attributes;
|
|
20
|
+
return {
|
|
21
|
+
buildingId: a.building_id,
|
|
22
|
+
roofSurface: a.df_nummer,
|
|
23
|
+
class: a.klasse,
|
|
24
|
+
classText: a.klasse_text,
|
|
25
|
+
area_m2: a.flaeche,
|
|
26
|
+
orientation: a.ausrichtung,
|
|
27
|
+
tilt_deg: a.neigung,
|
|
28
|
+
electricityYield_kWh: a.stromertrag,
|
|
29
|
+
winterYield_kWh: a.stromertrag_winterhalbjahr,
|
|
30
|
+
summerYield_kWh: a.stromertrag_sommerhalbjahr,
|
|
31
|
+
financialReturn_CHF: a.finanzertrag,
|
|
32
|
+
radiation_kWh_m2: a.gstrahlung,
|
|
33
|
+
egid: a.gwr_egid,
|
|
34
|
+
label: a.label,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function slimIdentifyResult(r) {
|
|
38
|
+
return {
|
|
39
|
+
layer: r.layerName,
|
|
40
|
+
layerId: r.layerBodId,
|
|
41
|
+
id: r.featureId,
|
|
42
|
+
attributes: r.attributes,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
7
46
|
exports.geodataTools = [
|
|
8
47
|
{
|
|
9
48
|
name: "geocode",
|
|
@@ -77,24 +116,7 @@ exports.geodataTools = [
|
|
|
77
116
|
},
|
|
78
117
|
},
|
|
79
118
|
];
|
|
80
|
-
//
|
|
81
|
-
function wgs84ToLV95(lat, lng) {
|
|
82
|
-
// Approx conversion for API calls that need Swiss coords
|
|
83
|
-
const phiPrime = (lat * 3600 - 169028.66) / 10000;
|
|
84
|
-
const lambdaPrime = (lng * 3600 - 26782.5) / 10000;
|
|
85
|
-
const E = 2600072.37
|
|
86
|
-
+ 211455.93 * lambdaPrime
|
|
87
|
-
- 10938.51 * lambdaPrime * phiPrime
|
|
88
|
-
- 0.36 * lambdaPrime * phiPrime ** 2
|
|
89
|
-
- 44.54 * lambdaPrime ** 3;
|
|
90
|
-
const N = 1200147.07
|
|
91
|
-
+ 308807.95 * phiPrime
|
|
92
|
-
+ 3745.25 * lambdaPrime ** 2
|
|
93
|
-
+ 76.63 * phiPrime ** 2
|
|
94
|
-
- 194.56 * lambdaPrime ** 2 * phiPrime
|
|
95
|
-
+ 119.79 * phiPrime ** 3;
|
|
96
|
-
return { x: E, y: N };
|
|
97
|
-
}
|
|
119
|
+
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
98
120
|
async function handleGeodata(name, args) {
|
|
99
121
|
switch (name) {
|
|
100
122
|
case "geocode":
|
|
@@ -106,44 +128,70 @@ async function handleGeodata(name, args) {
|
|
|
106
128
|
limit: 10,
|
|
107
129
|
});
|
|
108
130
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
109
|
-
return JSON.stringify(
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
count: data.results.length,
|
|
133
|
+
results: data.results.map(slimSearchResult),
|
|
134
|
+
});
|
|
110
135
|
}
|
|
111
136
|
case "reverse_geocode": {
|
|
112
137
|
const lat = args.lat;
|
|
113
138
|
const lng = args.lng;
|
|
114
|
-
const { x, y } = wgs84ToLV95(lat, lng);
|
|
115
|
-
const extent = `${x - 100},${y - 100},${x + 100},${y + 100}`;
|
|
116
139
|
const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/api/SearchServer`, {
|
|
117
140
|
searchText: `${lat},${lng}`,
|
|
118
141
|
type: "locations",
|
|
119
142
|
sr: 4326,
|
|
120
143
|
limit: 5,
|
|
121
144
|
});
|
|
122
|
-
void extent; // extent used in identify, not reverse geocode search
|
|
123
145
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
124
|
-
return JSON.stringify(
|
|
146
|
+
return JSON.stringify({
|
|
147
|
+
count: data.results.length,
|
|
148
|
+
results: data.results.map(slimSearchResult),
|
|
149
|
+
});
|
|
125
150
|
}
|
|
126
151
|
case "get_solar_potential": {
|
|
127
152
|
const lat = args.lat;
|
|
128
153
|
const lng = args.lng;
|
|
129
|
-
|
|
154
|
+
// Use tight tolerance to get only the closest building
|
|
155
|
+
const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
|
|
130
156
|
const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
131
157
|
geometry: `${lng},${lat}`,
|
|
132
158
|
geometryType: "esriGeometryPoint",
|
|
133
159
|
layers: "all:ch.bfe.solarenergie-eignung-daecher",
|
|
134
160
|
mapExtent: extent,
|
|
135
161
|
imageDisplay: "500,500,96",
|
|
136
|
-
tolerance:
|
|
162
|
+
tolerance: 10,
|
|
137
163
|
sr: 4326,
|
|
138
164
|
returnGeometry: false,
|
|
139
165
|
});
|
|
140
166
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
141
|
-
|
|
167
|
+
const roofs = data.results.map(slimSolarResult);
|
|
168
|
+
// Group by building and summarize
|
|
169
|
+
const buildingMap = new Map();
|
|
170
|
+
for (const r of roofs) {
|
|
171
|
+
const id = r.buildingId;
|
|
172
|
+
if (!buildingMap.has(id))
|
|
173
|
+
buildingMap.set(id, []);
|
|
174
|
+
buildingMap.get(id).push(r);
|
|
175
|
+
}
|
|
176
|
+
const buildings = [...buildingMap.entries()].map(([buildingId, surfaces]) => ({
|
|
177
|
+
buildingId,
|
|
178
|
+
totalArea_m2: Math.round(surfaces.reduce((s, r) => s + (r.area_m2 ?? 0), 0)),
|
|
179
|
+
totalElectricity_kWh: Math.round(surfaces.reduce((s, r) => s + (r.electricityYield_kWh ?? 0), 0)),
|
|
180
|
+
totalFinancialReturn_CHF: Math.round(surfaces.reduce((s, r) => s + (r.financialReturn_CHF ?? 0), 0)),
|
|
181
|
+
roofSurfaces: surfaces.length,
|
|
182
|
+
bestClass: Math.min(...surfaces.map((r) => r.class).filter((c) => c != null)),
|
|
183
|
+
surfaces: surfaces.slice(0, 5), // Cap at 5 surfaces per building
|
|
184
|
+
}));
|
|
185
|
+
return JSON.stringify({
|
|
186
|
+
count: buildings.length,
|
|
187
|
+
buildings: buildings.slice(0, 10), // Cap at 10 buildings
|
|
188
|
+
source: "Swiss Federal Office of Energy (BFE)",
|
|
189
|
+
});
|
|
142
190
|
}
|
|
143
191
|
case "identify_location": {
|
|
144
192
|
const lat = args.lat;
|
|
145
193
|
const lng = args.lng;
|
|
146
|
-
const extent = `${lng - 0.
|
|
194
|
+
const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
|
|
147
195
|
const layers = args.layers ? `all:${args.layers}` : "all";
|
|
148
196
|
const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
149
197
|
geometry: `${lng},${lat}`,
|
|
@@ -156,7 +204,10 @@ async function handleGeodata(name, args) {
|
|
|
156
204
|
returnGeometry: false,
|
|
157
205
|
});
|
|
158
206
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
159
|
-
return JSON.stringify(
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
count: data.results.length,
|
|
209
|
+
results: data.results.slice(0, 20).map(slimIdentifyResult),
|
|
210
|
+
});
|
|
160
211
|
}
|
|
161
212
|
case "get_municipality": {
|
|
162
213
|
const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/api/SearchServer`, {
|
|
@@ -166,7 +217,10 @@ async function handleGeodata(name, args) {
|
|
|
166
217
|
limit: 5,
|
|
167
218
|
});
|
|
168
219
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
169
|
-
return JSON.stringify(
|
|
220
|
+
return JSON.stringify({
|
|
221
|
+
count: data.results.length,
|
|
222
|
+
results: data.results.map(slimSearchResult),
|
|
223
|
+
});
|
|
170
224
|
}
|
|
171
225
|
default:
|
|
172
226
|
throw new Error(`Unknown geodata tool: ${name}`);
|
|
@@ -4,6 +4,46 @@ exports.transportTools = void 0;
|
|
|
4
4
|
exports.handleTransport = handleTransport;
|
|
5
5
|
const http_js_1 = require("../utils/http.js");
|
|
6
6
|
const BASE = "https://transport.opendata.ch/v1";
|
|
7
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
|
+
function slimBoardEntry(entry) {
|
|
9
|
+
return {
|
|
10
|
+
line: `${entry.category}${entry.number}`,
|
|
11
|
+
to: entry.to,
|
|
12
|
+
departure: entry.stop.departure,
|
|
13
|
+
delay: entry.stop.delay,
|
|
14
|
+
platform: entry.stop.platform,
|
|
15
|
+
operator: entry.operator,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function slimConnection(conn) {
|
|
19
|
+
return {
|
|
20
|
+
from: conn.from.station?.name,
|
|
21
|
+
to: conn.to.station?.name,
|
|
22
|
+
departure: conn.from.departure,
|
|
23
|
+
arrival: conn.to.arrival,
|
|
24
|
+
duration: conn.duration,
|
|
25
|
+
transfers: conn.transfers,
|
|
26
|
+
sections: conn.sections.map((s) => ({
|
|
27
|
+
type: s.journey ? "journey" : "walk",
|
|
28
|
+
line: s.journey ? `${s.journey.category}${s.journey.number}` : undefined,
|
|
29
|
+
from: s.departure.station?.name,
|
|
30
|
+
departure: s.departure.departure,
|
|
31
|
+
platform: s.departure.platform,
|
|
32
|
+
to: s.arrival.station?.name,
|
|
33
|
+
arrival: s.arrival.arrival,
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function slimStation(s) {
|
|
38
|
+
return {
|
|
39
|
+
id: s.id,
|
|
40
|
+
name: s.name,
|
|
41
|
+
lat: s.coordinate?.x,
|
|
42
|
+
lon: s.coordinate?.y,
|
|
43
|
+
distance: s.distance,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
7
47
|
exports.transportTools = [
|
|
8
48
|
{
|
|
9
49
|
name: "search_stations",
|
|
@@ -75,6 +115,7 @@ exports.transportTools = [
|
|
|
75
115
|
},
|
|
76
116
|
},
|
|
77
117
|
];
|
|
118
|
+
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
78
119
|
async function handleTransport(name, args) {
|
|
79
120
|
switch (name) {
|
|
80
121
|
case "search_stations": {
|
|
@@ -85,7 +126,7 @@ async function handleTransport(name, args) {
|
|
|
85
126
|
type: args.type,
|
|
86
127
|
});
|
|
87
128
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
88
|
-
return JSON.stringify(data.stations
|
|
129
|
+
return JSON.stringify(data.stations.map(slimStation));
|
|
89
130
|
}
|
|
90
131
|
case "get_connections": {
|
|
91
132
|
const url = (0, http_js_1.buildUrl)(`${BASE}/connections`, {
|
|
@@ -97,7 +138,7 @@ async function handleTransport(name, args) {
|
|
|
97
138
|
isArrivalTime: args.isArrivalTime ? 1 : undefined,
|
|
98
139
|
});
|
|
99
140
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
100
|
-
return JSON.stringify(data.connections
|
|
141
|
+
return JSON.stringify(data.connections.map(slimConnection));
|
|
101
142
|
}
|
|
102
143
|
case "get_departures": {
|
|
103
144
|
const url = (0, http_js_1.buildUrl)(`${BASE}/stationboard`, {
|
|
@@ -107,7 +148,10 @@ async function handleTransport(name, args) {
|
|
|
107
148
|
type: "departure",
|
|
108
149
|
});
|
|
109
150
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
110
|
-
return JSON.stringify({
|
|
151
|
+
return JSON.stringify({
|
|
152
|
+
station: data.station?.name,
|
|
153
|
+
departures: data.stationboard.map(slimBoardEntry),
|
|
154
|
+
});
|
|
111
155
|
}
|
|
112
156
|
case "get_arrivals": {
|
|
113
157
|
const url = (0, http_js_1.buildUrl)(`${BASE}/stationboard`, {
|
|
@@ -117,7 +161,10 @@ async function handleTransport(name, args) {
|
|
|
117
161
|
type: "arrival",
|
|
118
162
|
});
|
|
119
163
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
120
|
-
return JSON.stringify({
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
station: data.station?.name,
|
|
166
|
+
arrivals: data.stationboard.map(slimBoardEntry),
|
|
167
|
+
});
|
|
121
168
|
}
|
|
122
169
|
case "get_nearby_stations": {
|
|
123
170
|
const url = (0, http_js_1.buildUrl)(`${BASE}/locations`, {
|
|
@@ -126,7 +173,7 @@ async function handleTransport(name, args) {
|
|
|
126
173
|
type: "station",
|
|
127
174
|
});
|
|
128
175
|
const data = await (0, http_js_1.fetchJSON)(url);
|
|
129
|
-
return JSON.stringify(data.stations
|
|
176
|
+
return JSON.stringify(data.stations.map(slimStation));
|
|
130
177
|
}
|
|
131
178
|
default:
|
|
132
179
|
throw new Error(`Unknown transport tool: ${name}`);
|
package/package.json
CHANGED
package/src/modules/geodata.ts
CHANGED
|
@@ -2,6 +2,91 @@ import { fetchJSON, buildUrl } from "../utils/http.js";
|
|
|
2
2
|
|
|
3
3
|
const BASE = "https://api3.geo.admin.ch";
|
|
4
4
|
|
|
5
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
interface SearchResult {
|
|
8
|
+
id: number;
|
|
9
|
+
weight: number;
|
|
10
|
+
attrs: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SearchResponse {
|
|
14
|
+
results: SearchResult[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SolarAttributes {
|
|
18
|
+
building_id: number;
|
|
19
|
+
klasse: number;
|
|
20
|
+
klasse_text: string;
|
|
21
|
+
flaeche: number;
|
|
22
|
+
ausrichtung: string;
|
|
23
|
+
neigung: number;
|
|
24
|
+
stromertrag: number;
|
|
25
|
+
stromertrag_winterhalbjahr: number;
|
|
26
|
+
stromertrag_sommerhalbjahr: number;
|
|
27
|
+
finanzertrag: number;
|
|
28
|
+
gstrahlung: number;
|
|
29
|
+
gwr_egid: number;
|
|
30
|
+
df_nummer: number;
|
|
31
|
+
label: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface IdentifyResult {
|
|
35
|
+
layerBodId: string;
|
|
36
|
+
layerName: string;
|
|
37
|
+
featureId: number;
|
|
38
|
+
id: number;
|
|
39
|
+
attributes: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IdentifyResponse {
|
|
43
|
+
results: IdentifyResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function slimSearchResult(r: SearchResult) {
|
|
49
|
+
const a = r.attrs;
|
|
50
|
+
return {
|
|
51
|
+
label: a.label,
|
|
52
|
+
lat: a.lat,
|
|
53
|
+
lon: a.lon,
|
|
54
|
+
type: a.origin,
|
|
55
|
+
detail: a.detail,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function slimSolarResult(r: IdentifyResult) {
|
|
60
|
+
const a = r.attributes as unknown as SolarAttributes;
|
|
61
|
+
return {
|
|
62
|
+
buildingId: a.building_id,
|
|
63
|
+
roofSurface: a.df_nummer,
|
|
64
|
+
class: a.klasse,
|
|
65
|
+
classText: a.klasse_text,
|
|
66
|
+
area_m2: a.flaeche,
|
|
67
|
+
orientation: a.ausrichtung,
|
|
68
|
+
tilt_deg: a.neigung,
|
|
69
|
+
electricityYield_kWh: a.stromertrag,
|
|
70
|
+
winterYield_kWh: a.stromertrag_winterhalbjahr,
|
|
71
|
+
summerYield_kWh: a.stromertrag_sommerhalbjahr,
|
|
72
|
+
financialReturn_CHF: a.finanzertrag,
|
|
73
|
+
radiation_kWh_m2: a.gstrahlung,
|
|
74
|
+
egid: a.gwr_egid,
|
|
75
|
+
label: a.label,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function slimIdentifyResult(r: IdentifyResult) {
|
|
80
|
+
return {
|
|
81
|
+
layer: r.layerName,
|
|
82
|
+
layerId: r.layerBodId,
|
|
83
|
+
id: r.featureId,
|
|
84
|
+
attributes: r.attributes,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
89
|
+
|
|
5
90
|
export const geodataTools = [
|
|
6
91
|
{
|
|
7
92
|
name: "geocode",
|
|
@@ -76,24 +161,7 @@ export const geodataTools = [
|
|
|
76
161
|
},
|
|
77
162
|
];
|
|
78
163
|
|
|
79
|
-
//
|
|
80
|
-
function wgs84ToLV95(lat: number, lng: number): { x: number; y: number } {
|
|
81
|
-
// Approx conversion for API calls that need Swiss coords
|
|
82
|
-
const phiPrime = (lat * 3600 - 169028.66) / 10000;
|
|
83
|
-
const lambdaPrime = (lng * 3600 - 26782.5) / 10000;
|
|
84
|
-
const E = 2600072.37
|
|
85
|
-
+ 211455.93 * lambdaPrime
|
|
86
|
-
- 10938.51 * lambdaPrime * phiPrime
|
|
87
|
-
- 0.36 * lambdaPrime * phiPrime ** 2
|
|
88
|
-
- 44.54 * lambdaPrime ** 3;
|
|
89
|
-
const N = 1200147.07
|
|
90
|
-
+ 308807.95 * phiPrime
|
|
91
|
-
+ 3745.25 * lambdaPrime ** 2
|
|
92
|
-
+ 76.63 * phiPrime ** 2
|
|
93
|
-
- 194.56 * lambdaPrime ** 2 * phiPrime
|
|
94
|
-
+ 119.79 * phiPrime ** 3;
|
|
95
|
-
return { x: E, y: N };
|
|
96
|
-
}
|
|
164
|
+
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
97
165
|
|
|
98
166
|
export async function handleGeodata(name: string, args: Record<string, unknown>): Promise<string> {
|
|
99
167
|
switch (name) {
|
|
@@ -105,48 +173,76 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
|
|
|
105
173
|
sr: 4326,
|
|
106
174
|
limit: 10,
|
|
107
175
|
});
|
|
108
|
-
const data = await fetchJSON<
|
|
109
|
-
return JSON.stringify(
|
|
176
|
+
const data = await fetchJSON<SearchResponse>(url);
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
count: data.results.length,
|
|
179
|
+
results: data.results.map(slimSearchResult),
|
|
180
|
+
});
|
|
110
181
|
}
|
|
111
182
|
|
|
112
183
|
case "reverse_geocode": {
|
|
113
184
|
const lat = args.lat as number;
|
|
114
185
|
const lng = args.lng as number;
|
|
115
|
-
const { x, y } = wgs84ToLV95(lat, lng);
|
|
116
|
-
const extent = `${x - 100},${y - 100},${x + 100},${y + 100}`;
|
|
117
186
|
const url = buildUrl(`${BASE}/rest/services/api/SearchServer`, {
|
|
118
187
|
searchText: `${lat},${lng}`,
|
|
119
188
|
type: "locations",
|
|
120
189
|
sr: 4326,
|
|
121
190
|
limit: 5,
|
|
122
191
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
192
|
+
const data = await fetchJSON<SearchResponse>(url);
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
count: data.results.length,
|
|
195
|
+
results: data.results.map(slimSearchResult),
|
|
196
|
+
});
|
|
126
197
|
}
|
|
127
198
|
|
|
128
199
|
case "get_solar_potential": {
|
|
129
200
|
const lat = args.lat as number;
|
|
130
201
|
const lng = args.lng as number;
|
|
131
|
-
|
|
202
|
+
// Use tight tolerance to get only the closest building
|
|
203
|
+
const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
|
|
132
204
|
const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
133
205
|
geometry: `${lng},${lat}`,
|
|
134
206
|
geometryType: "esriGeometryPoint",
|
|
135
207
|
layers: "all:ch.bfe.solarenergie-eignung-daecher",
|
|
136
208
|
mapExtent: extent,
|
|
137
209
|
imageDisplay: "500,500,96",
|
|
138
|
-
tolerance:
|
|
210
|
+
tolerance: 10,
|
|
139
211
|
sr: 4326,
|
|
140
212
|
returnGeometry: false,
|
|
141
213
|
});
|
|
142
|
-
const data = await fetchJSON<
|
|
143
|
-
|
|
214
|
+
const data = await fetchJSON<IdentifyResponse>(url);
|
|
215
|
+
const roofs = data.results.map(slimSolarResult);
|
|
216
|
+
|
|
217
|
+
// Group by building and summarize
|
|
218
|
+
const buildingMap = new Map<number, typeof roofs>();
|
|
219
|
+
for (const r of roofs) {
|
|
220
|
+
const id = r.buildingId;
|
|
221
|
+
if (!buildingMap.has(id)) buildingMap.set(id, []);
|
|
222
|
+
buildingMap.get(id)!.push(r);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const buildings = [...buildingMap.entries()].map(([buildingId, surfaces]) => ({
|
|
226
|
+
buildingId,
|
|
227
|
+
totalArea_m2: Math.round(surfaces.reduce((s, r) => s + (r.area_m2 ?? 0), 0)),
|
|
228
|
+
totalElectricity_kWh: Math.round(surfaces.reduce((s, r) => s + (r.electricityYield_kWh ?? 0), 0)),
|
|
229
|
+
totalFinancialReturn_CHF: Math.round(surfaces.reduce((s, r) => s + (r.financialReturn_CHF ?? 0), 0)),
|
|
230
|
+
roofSurfaces: surfaces.length,
|
|
231
|
+
bestClass: Math.min(...surfaces.map((r) => r.class).filter((c) => c != null)),
|
|
232
|
+
surfaces: surfaces.slice(0, 5), // Cap at 5 surfaces per building
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
return JSON.stringify({
|
|
236
|
+
count: buildings.length,
|
|
237
|
+
buildings: buildings.slice(0, 10), // Cap at 10 buildings
|
|
238
|
+
source: "Swiss Federal Office of Energy (BFE)",
|
|
239
|
+
});
|
|
144
240
|
}
|
|
145
241
|
|
|
146
242
|
case "identify_location": {
|
|
147
243
|
const lat = args.lat as number;
|
|
148
244
|
const lng = args.lng as number;
|
|
149
|
-
const extent = `${lng - 0.
|
|
245
|
+
const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
|
|
150
246
|
const layers = args.layers ? `all:${args.layers}` : "all";
|
|
151
247
|
const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
152
248
|
geometry: `${lng},${lat}`,
|
|
@@ -158,8 +254,11 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
|
|
|
158
254
|
sr: 4326,
|
|
159
255
|
returnGeometry: false,
|
|
160
256
|
});
|
|
161
|
-
const data = await fetchJSON<
|
|
162
|
-
return JSON.stringify(
|
|
257
|
+
const data = await fetchJSON<IdentifyResponse>(url);
|
|
258
|
+
return JSON.stringify({
|
|
259
|
+
count: data.results.length,
|
|
260
|
+
results: data.results.slice(0, 20).map(slimIdentifyResult),
|
|
261
|
+
});
|
|
163
262
|
}
|
|
164
263
|
|
|
165
264
|
case "get_municipality": {
|
|
@@ -169,8 +268,11 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
|
|
|
169
268
|
sr: 4326,
|
|
170
269
|
limit: 5,
|
|
171
270
|
});
|
|
172
|
-
const data = await fetchJSON<
|
|
173
|
-
return JSON.stringify(
|
|
271
|
+
const data = await fetchJSON<SearchResponse>(url);
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
count: data.results.length,
|
|
274
|
+
results: data.results.map(slimSearchResult),
|
|
275
|
+
});
|
|
174
276
|
}
|
|
175
277
|
|
|
176
278
|
default:
|
package/src/modules/transport.ts
CHANGED
|
@@ -2,6 +2,116 @@ import { fetchJSON, buildUrl } from "../utils/http.js";
|
|
|
2
2
|
|
|
3
3
|
const BASE = "https://transport.opendata.ch/v1";
|
|
4
4
|
|
|
5
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
interface Station {
|
|
8
|
+
id: string | null;
|
|
9
|
+
name: string | null;
|
|
10
|
+
score: number | null;
|
|
11
|
+
coordinate: { type: string; x: number | null; y: number | null };
|
|
12
|
+
distance: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Prognosis {
|
|
16
|
+
platform: string | null;
|
|
17
|
+
arrival: string | null;
|
|
18
|
+
departure: string | null;
|
|
19
|
+
capacity1st: number | null;
|
|
20
|
+
capacity2nd: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Stop {
|
|
24
|
+
station: Station;
|
|
25
|
+
arrival: string | null;
|
|
26
|
+
departure: string | null;
|
|
27
|
+
delay: number | null;
|
|
28
|
+
platform: string | null;
|
|
29
|
+
prognosis: Prognosis | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BoardEntry {
|
|
33
|
+
stop: Stop;
|
|
34
|
+
name: string;
|
|
35
|
+
category: string;
|
|
36
|
+
number: string;
|
|
37
|
+
operator: string;
|
|
38
|
+
to: string;
|
|
39
|
+
passList: Stop[];
|
|
40
|
+
capacity1st: number | null;
|
|
41
|
+
capacity2nd: number | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Section {
|
|
45
|
+
journey: {
|
|
46
|
+
name: string;
|
|
47
|
+
category: string;
|
|
48
|
+
number: string;
|
|
49
|
+
operator: string;
|
|
50
|
+
to: string;
|
|
51
|
+
passList: Stop[];
|
|
52
|
+
capacity1st: number | null;
|
|
53
|
+
capacity2nd: number | null;
|
|
54
|
+
} | null;
|
|
55
|
+
walk: unknown | null;
|
|
56
|
+
departure: Stop;
|
|
57
|
+
arrival: Stop;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface Connection {
|
|
61
|
+
from: Stop;
|
|
62
|
+
to: Stop;
|
|
63
|
+
duration: string;
|
|
64
|
+
transfers: number;
|
|
65
|
+
sections: Section[];
|
|
66
|
+
capacity1st: number | null;
|
|
67
|
+
capacity2nd: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function slimBoardEntry(entry: BoardEntry) {
|
|
73
|
+
return {
|
|
74
|
+
line: `${entry.category}${entry.number}`,
|
|
75
|
+
to: entry.to,
|
|
76
|
+
departure: entry.stop.departure,
|
|
77
|
+
delay: entry.stop.delay,
|
|
78
|
+
platform: entry.stop.platform,
|
|
79
|
+
operator: entry.operator,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function slimConnection(conn: Connection) {
|
|
84
|
+
return {
|
|
85
|
+
from: conn.from.station?.name,
|
|
86
|
+
to: conn.to.station?.name,
|
|
87
|
+
departure: conn.from.departure,
|
|
88
|
+
arrival: conn.to.arrival,
|
|
89
|
+
duration: conn.duration,
|
|
90
|
+
transfers: conn.transfers,
|
|
91
|
+
sections: conn.sections.map((s) => ({
|
|
92
|
+
type: s.journey ? "journey" : "walk",
|
|
93
|
+
line: s.journey ? `${s.journey.category}${s.journey.number}` : undefined,
|
|
94
|
+
from: s.departure.station?.name,
|
|
95
|
+
departure: s.departure.departure,
|
|
96
|
+
platform: s.departure.platform,
|
|
97
|
+
to: s.arrival.station?.name,
|
|
98
|
+
arrival: s.arrival.arrival,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function slimStation(s: Station) {
|
|
104
|
+
return {
|
|
105
|
+
id: s.id,
|
|
106
|
+
name: s.name,
|
|
107
|
+
lat: s.coordinate?.x,
|
|
108
|
+
lon: s.coordinate?.y,
|
|
109
|
+
distance: s.distance,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
114
|
+
|
|
5
115
|
export const transportTools = [
|
|
6
116
|
{
|
|
7
117
|
name: "search_stations",
|
|
@@ -74,6 +184,8 @@ export const transportTools = [
|
|
|
74
184
|
},
|
|
75
185
|
];
|
|
76
186
|
|
|
187
|
+
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
77
189
|
export async function handleTransport(name: string, args: Record<string, unknown>): Promise<string> {
|
|
78
190
|
switch (name) {
|
|
79
191
|
case "search_stations": {
|
|
@@ -83,8 +195,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
|
|
|
83
195
|
y: args.y as number,
|
|
84
196
|
type: args.type as string,
|
|
85
197
|
});
|
|
86
|
-
const data = await fetchJSON<{ stations:
|
|
87
|
-
return JSON.stringify(data.stations
|
|
198
|
+
const data = await fetchJSON<{ stations: Station[] }>(url);
|
|
199
|
+
return JSON.stringify(data.stations.map(slimStation));
|
|
88
200
|
}
|
|
89
201
|
|
|
90
202
|
case "get_connections": {
|
|
@@ -96,8 +208,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
|
|
|
96
208
|
limit: args.limit as number,
|
|
97
209
|
isArrivalTime: args.isArrivalTime ? 1 : undefined,
|
|
98
210
|
});
|
|
99
|
-
const data = await fetchJSON<{ connections:
|
|
100
|
-
return JSON.stringify(data.connections
|
|
211
|
+
const data = await fetchJSON<{ connections: Connection[] }>(url);
|
|
212
|
+
return JSON.stringify(data.connections.map(slimConnection));
|
|
101
213
|
}
|
|
102
214
|
|
|
103
215
|
case "get_departures": {
|
|
@@ -107,8 +219,11 @@ export async function handleTransport(name: string, args: Record<string, unknown
|
|
|
107
219
|
datetime: args.datetime as string,
|
|
108
220
|
type: "departure",
|
|
109
221
|
});
|
|
110
|
-
const data = await fetchJSON<{ station:
|
|
111
|
-
return JSON.stringify({
|
|
222
|
+
const data = await fetchJSON<{ station: Station; stationboard: BoardEntry[] }>(url);
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
station: data.station?.name,
|
|
225
|
+
departures: data.stationboard.map(slimBoardEntry),
|
|
226
|
+
});
|
|
112
227
|
}
|
|
113
228
|
|
|
114
229
|
case "get_arrivals": {
|
|
@@ -118,8 +233,11 @@ export async function handleTransport(name: string, args: Record<string, unknown
|
|
|
118
233
|
datetime: args.datetime as string,
|
|
119
234
|
type: "arrival",
|
|
120
235
|
});
|
|
121
|
-
const data = await fetchJSON<{ station:
|
|
122
|
-
return JSON.stringify({
|
|
236
|
+
const data = await fetchJSON<{ station: Station; stationboard: BoardEntry[] }>(url);
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
station: data.station?.name,
|
|
239
|
+
arrivals: data.stationboard.map(slimBoardEntry),
|
|
240
|
+
});
|
|
123
241
|
}
|
|
124
242
|
|
|
125
243
|
case "get_nearby_stations": {
|
|
@@ -128,8 +246,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
|
|
|
128
246
|
y: args.y as number,
|
|
129
247
|
type: "station",
|
|
130
248
|
});
|
|
131
|
-
const data = await fetchJSON<{ stations:
|
|
132
|
-
return JSON.stringify(data.stations
|
|
249
|
+
const data = await fetchJSON<{ stations: Station[] }>(url);
|
|
250
|
+
return JSON.stringify(data.stations.map(slimStation));
|
|
133
251
|
}
|
|
134
252
|
|
|
135
253
|
default:
|