@mfukushim/map-traveler-mcp
Version:
Virtual traveler library for MCP
550 lines (549 loc) • 31 kB
JavaScript
/*! map-traveler-mcp | MIT License | https://github.com/mfukushim/map-traveler-mcp */
import { createRequire as _createRequire } from "module";
const __require = _createRequire(import.meta.url);
import { Effect, Option, Schema } from "effect";
import { MapService, MapDef } from "./MapService.js";
import { __pwd, DbService, DbServiceLive, env, } from "./DbService.js";
import * as geolib from "geolib";
const dayjs = __require("dayjs");
const utc = __require("dayjs/plugin/utc");
const duration = __require("dayjs/plugin/duration");
const relativeTime = __require("dayjs/plugin/relativeTime");
import { ImageService, widthOut, heightOut } from "./ImageService.js";
import { StoryService } from "./StoryService.js";
import 'dotenv/config';
import { McpLogService } from "./McpLogService.js";
import { AnswerError } from "./mapTraveler.js";
import * as path from "path";
const sharp = __require("sharp");
import * as fs from "node:fs";
import { bodyAreaRatio, bodyHWRatio, bodyWindowRatioH, bodyWindowRatioW, comfy_url, noImageOut, pixAi_key, sd_key, time_scale } from "./EnvUtils.js";
dayjs.extend(utc);
dayjs.extend(duration);
dayjs.extend(relativeTime);
export const useAiImageGen = (pixAi_key ? 'pixAi' : sd_key ? 'sd' : comfy_url ? 'comfyUi' : '');
export const defaultAvatarId = 1;
export const practiceData = [
{
address: "Hakata Station,〒812-0012 Fukuoka, Hakata Ward, 博多駅中央街1−1",
placesPath: "assets/places1.json",
sampleImagePath: "assets/place1.png",
durationSec: 20 * 60,
},
{
address: "Nishitetsu Fukuoka Tenjin Station,〒810-0001 福岡県福岡市中央区天神2−22",
placesPath: "assets/places2.json",
sampleImagePath: "assets/place2.png",
durationSec: 20 * 60,
},
{
address: "Ohori Park,〒810-0051 Fukuoka, Chuo Ward, Ohorikoen, 公園管理事務所",
placesPath: "assets/places3.json",
sampleImagePath: "assets/place3.png",
durationSec: 20 * 60,
},
{
address: "Fukuoka Tower,2 Chome-3-26 Momochihama, Sawara Ward, Fukuoka, 814-0001",
placesPath: "assets/places4.json",
sampleImagePath: "assets/place4.png",
durationSec: 20 * 60,
}
];
export class RunnerService extends Effect.Service()("traveler/RunnerService", {
accessors: true,
effect: Effect.gen(function* () {
const durationScale2 = (time_scale && Number.parseFloat(time_scale)) || 4;
const isShips = (maneuver) => ['ferry', 'airplane'].includes(maneuver || '');
const maneuverIsShip = (step) => isShips(step.maneuver);
const getFacilitiesPractice = (toAddress, includePhoto) => {
return Effect.gen(function* () {
const practiceInfo = practiceData.find(value => value.address === toAddress) || practiceData[0];
const nearFacilities = yield* Effect.async((resume) => fs.readFile(path.join(__pwd, practiceInfo.placesPath), (err, data) => {
if (err) {
resume(Effect.fail(err));
}
resume(Effect.succeed(data));
})).pipe(Effect.andThen(a => Schema.decode(Schema.parseJson(MapDef.GmPlacesSchema))((Buffer.from(a).toString('utf-8')))), Effect.andThen(a => StoryService.placesToFacilities(a)));
const image = includePhoto ? (yield* Effect.async((resume) => fs.readFile(path.join(__pwd, practiceInfo.sampleImagePath), (err, data) => {
if (err) {
resume(Effect.fail(err));
}
resume(Effect.succeed(data));
})).pipe(Effect.andThen(a => ImageService.shrinkImage(a)), Effect.andThen(a => Buffer.from(a)), Effect.orElseSucceed(() => undefined))) : undefined;
return { nearFacilities, image, locText: '' };
});
};
const getFacilities = (loc, includePhoto, abort = false, localDebug = false) => {
return Effect.gen(function* () {
const nearFacilities = yield* StoryService.getNearbyFacilities({
lat: loc.lat,
lng: loc.lng,
bearing: 0
});
const image = includePhoto && env.anyImageAiExist && !noImageOut ? (yield* getStreetImage(loc, abort, localDebug).pipe(Effect.andThen(a => a.buf), Effect.orElseSucceed(() => undefined))) :
includePhoto && !noImageOut ? (yield* getStreetImageOnly(loc).pipe(Effect.orElseSucceed(() => undefined))) : undefined;
return {
nearFacilities,
image,
locText: `current location is below\nlatitude:${loc.lat}\nlongitude:${loc.lng}\nhttps://www.google.com/maps?q=${loc.lat},${loc.lng}\n`
};
});
};
const runningReport = (locText, nearFacilities, image, abort = false, justArrive = false) => {
return Effect.gen(function* () {
const facilityText = nearFacilities && nearFacilities.facilities.length !== 0 ?
`The following facilities are nearby:\n` + nearFacilities.facilities.map(value => value.name + (value.types.length > 0 ? ' (kinds:' + value.types.join(',') + ')' : '')).join('\n') + '\n' :
"There don't appear to be any buildings nearby.";
const infoText = abort ? `I have received a message to discontinue my trip. This time, I will discontinue my trip.\n` :
justArrive ? `We have arrived at our destination.` : '';
const posText = nearFacilities && Option.isSome(nearFacilities.townName) ? `Town name is ${nearFacilities.townName.value}\n` : 'Town name is unknown.\n';
const content = [{
type: "text",
text: infoText + locText + posText + (nearFacilities ? facilityText : '')
}];
if (image) {
content.push({
type: "image",
data: image.toString('base64'),
mimeType: 'image/png'
});
}
return { out: content, address: nearFacilities ? nearFacilities.address : Option.none() };
});
};
const vehicleView = (loc, includePhoto) => {
const maneuver = loc.maneuver;
const vehiclePrompt = maneuver?.includes('ferry') ? '(on ship deck:1.3),(ferry:1.2),sea,handrails' :
maneuver?.includes('airplane') ? '(airplane cabin:1.3),reclining seat,sitting' : '';
const out = [
{
type: "text",
text: `I'm on the ${maneuver} now. Longitude and Latitude is almost ${loc.lat},${loc.lng}`
},
];
if (includePhoto && env.anyImageAiExist && !noImageOut) {
return ImageService.makeEtcTripImage(useAiImageGen, vehiclePrompt, loc.timeZoneId).pipe(Effect.andThen(image => {
out.push({ type: "image", data: image.toString("base64"), mimeType: 'image/png' });
return Effect.succeed(out);
}), Effect.orElse(() => Effect.succeed(out)));
}
return Effect.succeed(out);
};
const hotelView = (timeZoneId, includePhoto, toAddress) => {
const hour = dayjs().tz(timeZoneId).hour();
const out = [
{ type: "text", text: `I am in a hotel in ${toAddress}.` }
];
if (includePhoto) {
return ImageService.makeHotelPict(useAiImageGen, hour, undefined).pipe(Effect.andThen(image1 => {
out.push({
type: "image",
data: image1.toString("base64"),
mimeType: 'image/png'
});
return Effect.succeed(out);
}), Effect.orElse(() => Effect.succeed(out)));
}
return Effect.succeed(out);
};
const getRunStatusAndUpdateEnd = (now) => {
return Effect.gen(function* () {
const status = yield* DbService.getRecentRunStatus().pipe(Effect.orElseFail(() => new AnswerError(`current location not set. Please set the current location address`)));
if (status.status === "stop" && !status.to) {
return yield* Effect.fail(new AnswerError(`current location not set. Please set the current location address`));
}
const endTime = dayjs.unix(status.tilEndEpoch);
const start = dayjs(status.startTime);
const elapseRatio = Math.min((now.diff(start, "seconds")) / (endTime.diff(start, "seconds")), 1);
let justArrive = false;
if (elapseRatio >= 1) {
if (status.status !== "running") {
justArrive = true;
}
resetRunStatus(status, status.to, endTime.toDate(), status.endLat, status.endLng, status.endCountry, status.endTz);
yield* DbService.saveRunStatus(status);
}
return { runStatus: status, justArrive, elapseRatio };
});
};
function getElapsedView(proceedPercent, debugRouteStr) {
const proceed = Math.max(0, Math.min(100, proceedPercent)) / 100;
return Effect.gen(function* () {
const runStatus = yield* DbService.getRecentRunStatus().pipe(Effect.orElseFail(() => new AnswerError(`current location not set. Please set the current location address`)));
let p = 1;
let rs;
if (runStatus.status === 'stop') {
if (!runStatus.to) {
return yield* Effect.fail(new AnswerError(`current location not set. Please set the current location address`));
}
const dest = yield* DbService.getEnvOption('destination');
if (Option.isNone(dest)) {
return yield* Effect.fail(new AnswerError(`destination not set`));
}
if (env.isPractice) {
rs = runStatus;
}
else {
rs = yield* setStart(runStatus, dayjs());
}
p = proceed;
}
else {
if (!runStatus.to || !runStatus.from) {
return yield* Effect.fail(new Error(`getElapsedView running but no from or to`));
}
const endTime = dayjs.unix(runStatus.tilEndEpoch);
const start = dayjs(runStatus.startTime);
const elapseRatio = Math.min((dayjs().diff(start, "seconds")) / (endTime.diff(start, "seconds")), 1);
const remainRatio = (1 - elapseRatio) * (1 - proceed);
p = 1 - remainRatio;
rs = runStatus;
}
const loc = yield* calcCurrentLoc(rs, p, debugRouteStr);
yield* McpLogService.logTrace(`getCurrentView:recalcRatio:${p},start:${rs.startTime},end:${dayjs.unix(rs.tilEndEpoch)},status:${loc.status}`);
const { nearFacilities, image, locText } = yield* getFacilities(loc, true, false);
resetRunStatus(rs, Option.getOrElse(nearFacilities.address, () => rs.to), dayjs().toDate(), loc.lat, loc.lng, Option.getOrElse(nearFacilities.country, () => rs.endCountry), loc.timeZoneId);
yield* DbService.saveRunStatus(rs);
return yield* runningReport(locText, nearFacilities, image, false, true).pipe(Effect.andThen(a => a.out));
});
}
function getCurrentView(now, includePhoto, includeNearbyFacilities, practice = false) {
return Effect.gen(function* () {
const { runStatus, justArrive, elapseRatio } = yield* getRunStatusAndUpdateEnd(now);
return yield* makeView(runStatus, elapseRatio, justArrive && dayjs().isBefore(dayjs.unix(runStatus.tilEndEpoch).add(1, "hour")), includePhoto, includeNearbyFacilities, practice);
});
}
function makeView(runStatus, elapseRatio, showRunning, includePhoto, includeNearbyFacilities, practice = false, debugRoute) {
return Effect.gen(function* () {
let loc;
let viewStatus;
if (practice) {
loc = {
status: runStatus.status,
lat: runStatus.endLat,
lng: runStatus.endLng,
bearing: MapService.getBearing(runStatus.startLat, runStatus.startLng, runStatus.endLat, runStatus.endLng),
timeZoneId: 'Asia/Tokyo',
remainSecInPath: 0,
maneuver: undefined,
isEnd: true,
landPathNo: -1,
};
viewStatus = runStatus.status;
}
else {
loc = yield* calcCurrentLoc(runStatus, elapseRatio, debugRoute);
viewStatus = loc.status;
yield* McpLogService.logTrace(`getCurrentView:elapseRatio:${elapseRatio},start:${runStatus.startTime},end:${dayjs.unix(runStatus.tilEndEpoch)},status:${loc.status}`);
if (showRunning) {
viewStatus = 'running';
}
if (env.moveMode === "skip" && viewStatus === "stop") {
viewStatus = 'running';
}
}
switch (viewStatus) {
case 'vehicle':
return yield* vehicleView(loc, includePhoto);
case 'stop':
return yield* hotelView(practice ? 'Asia/Tokyo' : loc.timeZoneId, includePhoto, runStatus.to);
case "running":
const { nearFacilities, image, locText } = yield* (practice ? getFacilitiesPractice(runStatus.to, includePhoto) : getFacilities(loc, includePhoto, false));
return yield* runningReport(locText, includeNearbyFacilities ? nearFacilities : undefined, image, false, showRunning).pipe(Effect.andThen(a => a.out));
}
}).pipe(Effect.catchAll(e => {
if (e instanceof AnswerError) {
return Effect.fail(e);
}
return McpLogService.logError(`getCurrentView catch:${e},${JSON.stringify(e)}`).pipe(Effect.andThen(() => Effect.fail(new AnswerError("Sorry,I don't know where you are right now. Please wait a moment and ask again."))));
}));
}
function saveCurrentRunnerRoute(avatarId, data) {
return DbService.updateRoute(avatarId, JSON.stringify(data));
}
function loadCurrentRunnerRoute(avatarId) {
return DbService.getAvatar(avatarId).pipe(Effect.andThen(a => a.currentRoute ? Effect.succeed(a.currentRoute) :
Effect.fail(new AnswerError('The route to the destination has not yet been determined.'))), Effect.andThen(a => Schema.decodeUnknownSync(Schema.parseJson(MapDef.RouteArraySchema))(a)), Effect.tapError(cause => McpLogService.logError(`loadCurrentRunnerRoute ${cause}`)));
}
const reNumberSteps = (r) => {
const directionSteps = (r.leg.steps).map((r, idx) => ({
...r,
stepNo: idx,
isRelayPoint: false,
pathNo: -1,
start: -1,
end: -1
}));
directionSteps[directionSteps.length - 1].isRelayPoint = true;
return directionSteps;
};
const calcStepTime = (step) => {
return maneuverIsShip(step) ? step.duration.value : step.duration.value * durationScale2;
};
const routesToDirectionStep = (routeInfo) => {
const all = routeInfo.flatMap((r, idx) => {
return reNumberSteps(r).map(value => {
value.pathNo = idx;
return value;
});
});
all.reduce((p, c) => {
c.start = p;
c.end = p + calcStepTime(c);
p = c.end;
return p;
}, 0);
return all;
};
const calcCurrentStep = (steps, currentRunSec = Number.MAX_SAFE_INTEGER) => {
return Option.fromNullable(steps.find(value => value.start <= currentRunSec && value.end > currentRunSec));
};
function calcCurrentLoc(runStatus, elapseRatio, debugRouteStr) {
return Effect.gen(function* () {
if (runStatus.status === "stop") {
yield* McpLogService.logTrace(`calcCurrentLoc: stopped`);
return {
status: "stop",
lat: runStatus.endLat,
lng: runStatus.endLng,
bearing: MapService.getBearing(runStatus.startLat, runStatus.startLng, runStatus.endLat, runStatus.endLng),
timeZoneId: runStatus.endTz,
remainSecInPath: 0,
maneuver: undefined,
isEnd: true,
landPathNo: -1,
};
}
const runAllSec = dayjs.unix(runStatus.tilEndEpoch).diff(runStatus.startTime, "seconds") * elapseRatio;
yield* McpLogService.logTrace(`calcCurrentLoc: elapseRatio=${elapseRatio},runAllSec=${runAllSec}`);
const fullRoute = debugRouteStr ? yield* Schema.decode(Schema.parseJson(MapDef.RouteArraySchema))(debugRouteStr) : yield* loadCurrentRunnerRoute(runStatus.avatarId);
const allSteps = routesToDirectionStep(fullRoute);
const currentStepOption = calcCurrentStep(allSteps, runAllSec);
if (Option.isNone(currentStepOption)) {
yield* McpLogService.logTrace(`calcCurrentLoc: end`);
return {
status: "stop",
lat: runStatus.endLat,
lng: runStatus.endLng,
bearing: MapService.getBearing(runStatus.startLat, runStatus.startLng, runStatus.endLat, runStatus.endLng),
timeZoneId: runStatus.endTz,
remainSecInPath: 0,
maneuver: undefined,
isEnd: true,
landPathNo: -1,
};
}
const currentStep = currentStepOption.value;
const rat = Math.min((runAllSec - currentStep.start) / (currentStep.end - currentStep.start), 1);
const lat = (currentStep.end_location.lat - currentStep.start_location.lat) * rat + currentStep.start_location.lat;
const lng = (currentStep.end_location.lng - currentStep.start_location.lng) * rat + currentStep.start_location.lng;
yield* McpLogService.logTrace(`calcCurrentLoc: step=${currentStep.pathNo},${currentStep.stepNo},${currentStep.start},${currentStep.end},${runAllSec},${rat},${lat},${lng},${currentStep.maneuver}`);
return {
status: (isShips(currentStep.maneuver) ? "vehicle" : "running"),
lat: lat,
lng: lng,
bearing: geolib.getRhumbLineBearing({
lat: currentStep.start_location.lat,
lng: currentStep.start_location.lng
}, { lat: currentStep.end_location.lat, lng: currentStep.end_location.lng }),
maneuver: currentStep.maneuver,
timeZoneId: yield* MapService.getTimezoneByLatLng(lat, lng).pipe(Effect.orElseSucceed(() => 'Asia/Tokyo')),
isEnd: false,
remainSecInPath: Math.min(currentStep.end - runAllSec, currentStep.end - currentStep.start),
landPathNo: currentStep.pathNo,
};
});
}
function getDestinationAddress() {
return Effect.gen(function* () {
const dest = yield* DbService.getEnvOption('destination');
if (Option.isSome(dest)) {
return dest.value;
}
const runStatus = yield* DbService.getRecentRunStatus();
if (runStatus.status === 'running' && runStatus.to) {
return runStatus.to;
}
return yield* Effect.fail(new AnswerError("The destination has not yet been decided"));
});
}
const sumDurationSec = (destList) => destList.flatMap(v => v.leg).flatMap(a => a.steps)
.map(a => calcStepTime(a)).reduce((p, c) => p + c, 0);
function setDestinationAddress(address) {
return Effect.gen(function* () {
const location = yield* MapService.getMapLocation(address);
if (Option.isNone(location)) {
return yield* Effect.fail(new AnswerError("I don't know where you're talking about. destination location not found"));
}
const { runStatus } = yield* getRunStatusAndUpdateEnd(dayjs());
const destList = yield* MapService.calcMultiPathRoute({
lat: runStatus.endLat, lng: runStatus.endLng, country: runStatus.endCountry || location.value.country
}, [{
lat: location.value.lat, lng: location.value.lng, country: location.value.country
}]);
if (destList.length === 0) {
return yield* Effect.fail(new AnswerError("I can't find a route to my destination."));
}
const durationSec = sumDurationSec(destList);
if (durationSec > 3 * 24 * 60 * 60) {
return yield* Effect.fail(new AnswerError("It will take 3 days to reach your destination. That's too long."));
}
yield* saveCurrentRunnerRoute(defaultAvatarId, destList);
const timeZoneId = yield* MapService.getTimezoneByLatLng(location.value.lat, location.value.lng);
yield* DbService.saveEnv('destination', address);
yield* DbService.saveEnv('destTimezoneId', timeZoneId);
const mesList = [
`The traveler's destination was set as follows: ${address}`,
`The journey takes approximately ${dayjs.duration(durationSec, "seconds").humanize()}.`
];
const listElement = destList[destList.length - 1];
return {
message: mesList.join('\n'),
tilEndSec: durationSec,
destination: listElement.leg.end_location || { lat: location.value.lat, lng: location.value.lng }
};
});
}
const resetRunStatus = (recent, to, endTime, lat, lng, country, timeZone) => {
recent.status = "stop";
recent.startTime = new Date(0);
recent.endTime = endTime;
recent.to = to;
recent.endLat = lat;
recent.endLng = lng;
recent.destination = null;
recent.tilEndEpoch = 0;
recent.durationSec = 0;
recent.distanceM = 0;
recent.startCountry = country;
recent.startTz = timeZone;
recent.currentPathNo = -1;
recent.currentStepNo = -1;
};
function setStart(runStatus, now) {
return Effect.gen(function* () {
const dest = yield* DbService.getEnv('destination');
const destInfo = yield* setDestinationAddress(dest);
runStatus.status = "running";
runStatus.startTime = now.toDate();
runStatus.endTime = dayjs.unix(destInfo.tilEndSec).toDate();
runStatus.destination = "";
runStatus.from = runStatus.to;
runStatus.to = dest;
runStatus.startTz = runStatus.endTz;
runStatus.startLat = runStatus.endLat;
runStatus.startLng = runStatus.endLng;
runStatus.endLat = destInfo.destination.lat;
runStatus.endLng = destInfo.destination.lng;
runStatus.tilEndEpoch = destInfo.tilEndSec + now.unix();
runStatus.endTz = yield* DbService.getEnv("destTimezoneId").pipe(Effect.orElseSucceed(() => runStatus.startTz));
return runStatus;
});
}
function startJourney(practice = false) {
return Effect.gen(function* () {
const now = dayjs();
let rs;
if (practice) {
rs = yield* DbService.practiceRunStatus(true);
}
else {
const { runStatus } = yield* getRunStatusAndUpdateEnd(now).pipe(Effect.tap(a => {
if (["running", "vehicle"].includes(a.runStatus.status)) {
return Effect.fail(new AnswerError(`already start journey.You may stop or continue the journey`));
}
}));
rs = yield* setStart(runStatus, now);
}
const hour = now.tz(rs.startTz).hour();
const image1 = yield* ImageService.makeHotelPict(useAiImageGen, hour).pipe(Effect.andThen(a => Effect.succeed(Option.some(a))), Effect.orElseSucceed(() => Option.none()));
yield* DbService.saveEnv("destination", "");
yield* DbService.saveEnv("destTimezoneId", "");
yield* DbService.saveRunStatus(rs);
return {
text: `We set out on a journey. The departure point is "${rs.from}". I'm heading to "${rs.to}".`,
image: image1
};
});
}
function stopJourney(practice) {
return Effect.gen(function* () {
const now = dayjs();
const { runStatus } = yield* getRunStatusAndUpdateEnd(now);
if (runStatus.status === "stop") {
return yield* Effect.fail(new AnswerError(`The journey has already arrived in "${runStatus.to}".`));
}
let res;
if (practice) {
res = yield* getFacilitiesPractice(runStatus.to, true).pipe(Effect.andThen(a => runningReport(a.locText, a.nearFacilities, a.image, true)));
}
else {
const elapse = Math.min(now.diff(runStatus.startTime, "seconds") / dayjs.unix(runStatus.tilEndEpoch).diff(runStatus.startTime, "seconds"), 1);
const currentInfo = yield* calcCurrentLoc(runStatus, elapse);
const nears = yield* StoryService.getNearbyFacilities({
lat: currentInfo.lat,
lng: currentInfo.lng,
bearing: currentInfo.bearing
});
resetRunStatus(runStatus, Option.getOrElse(nears.address, () => runStatus.to), now.toDate(), currentInfo.lat, currentInfo.lng, Option.getOrElse(nears.country, () => runStatus.endCountry), currentInfo.timeZoneId);
res = yield* getFacilities(currentInfo, true, false).pipe(Effect.andThen(a => runningReport(a.locText, a.nearFacilities, a.image, true)));
}
runStatus.to = Option.getOrElse(res.address, () => runStatus.from);
yield* DbService.saveRunStatus(runStatus);
return res.out;
}).pipe(Effect.provide(DbServiceLive));
}
function getStreetImage(loc, abort = false, localDebug = false) {
return Effect.gen(function* () {
const okLoc = yield* MapService.findStreetViewMeta(loc.lat, loc.lng, loc.bearing, 640, 640);
const baseImage = yield* MapService.getStreetViewImage(okLoc.lat, okLoc.lng, loc.bearing, 640, 640);
const bodyAreaRatioJ = bodyAreaRatio ? { bodyAreaRatio: Number.parseFloat(bodyAreaRatio) } : {};
const bodyHWRatioJ = bodyHWRatio ? { bodyHWRatio: Number.parseFloat(bodyHWRatio) } : {};
const bodyWindowRatioWJ = bodyWindowRatioW ? { bodyWindowRatioW: Number.parseFloat(bodyWindowRatioW) } : {};
const bodyWindowRatioHJ = bodyWindowRatioH ? { bodyWindowRatioH: Number.parseFloat(bodyWindowRatioH) } : {};
return yield* ImageService.makeRunnerImageV3(baseImage, useAiImageGen, abort, { ...bodyAreaRatioJ, ...bodyHWRatioJ, ...bodyWindowRatioWJ, ...bodyWindowRatioHJ }, localDebug).pipe(Effect.andThen(a => Effect.gen(function* () {
const buf = yield* Effect.tryPromise(() => sharp(a.buf).resize({
width: widthOut,
height: heightOut
}).png().toBuffer());
return {
...a,
buf: buf
};
})), Effect.orElse(() => {
return Effect.tryPromise(() => sharp(baseImage).resize({
width: widthOut,
height: heightOut
}).png().toBuffer()).pipe(Effect.andThen(a => ({
buf: a,
shiftX: 0,
shiftY: 0,
fit: false,
append: ''
})));
}));
});
}
function getStreetImageOnly(loc) {
return MapService.findStreetViewMeta(loc.lat, loc.lng, loc.bearing, 640, 640).pipe(Effect.andThen(okLoc => MapService.getStreetViewImage(okLoc.lat, okLoc.lng, loc.bearing, 640, 640)), Effect.andThen(baseImage => Effect.tryPromise(() => sharp(baseImage).resize({
width: widthOut,
height: heightOut
}).png().toBuffer())));
}
return {
getCurrentView,
resetRunStatus,
getDestinationAddress,
setDestinationAddress,
startJourney,
stopJourney,
sumDurationSec,
routesToDirectionStep,
getElapsedView,
makeView,
};
}),
dependencies: [DbServiceLive]
}) {
}
export const RunnerServiceLive = RunnerService.Default;